在微软的微服务架构案例eshopOnContainer中,对于订单的状态是这样的:
public class OrderStatus: Enumeration
{public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant());public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant());public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant());public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant());public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant());public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant());public OrderStatus(int id, string name): base(id, name){}
}
微软没有直接使用传统的Enum关键字,而是自定了以一个抽象类:Enumeration。最开始,我有些不解,为什么么要“舍近求远”?其实微软官方在此文中提到了枚举类,理由是启动面向对象语言的所有丰富功能。解释的很晦涩,虽然给出了代码案例,但是还是让人很难体会到其中奥妙。直到最近又看了这个博客,似乎才深入理解了一些。结合一个例子说明,可能更好!
目录
1.使用枚举
2.enumeration类
3.拓展
假设你要去买咖啡,正常情况下会有“大、中、小”三种规格,所以一般情况下会让你给出尺寸需求,不同的大小价格自然不同,出于节约,默认是小杯。好了我们应该怎么实现呢?
首先定义一个枚举类型,表明所要分量:
public enum CoffeCupType{Middle,Large,SuperLarge}
然后我们定义咖啡类如下:
public class Coffee{const double CoffeBeans = 10.0;public CoffeCupType Type { get; set; } = default;public double Pay(){return Type switch{CoffeCupType.Middle => CoffeBeans * 1.0,CoffeCupType.Large => CoffeBeans * 1.5,CoffeCupType.SuperLarge => CoffeBeans * 2.0,_ => 0,};}public void SetType(int t){Type = (CoffeCupType)t;}}
可以看到我们设计了一个很简单的咖啡类,咖啡的费用需要根据类型判断,看起来好像没什么问题,但是还是有些不完美的地方:
添加新的枚举值,就需要在Pay函数中添加新的Switch分支,让默认分支变得有防御性,但是新的枚举值可能导致错误的逻辑。
其实,对于上面的例子,我们将杯子的属性和价格倍率拆分了,使得在Coffe类中显得耦合性过强。其实价格倍率本身应该是CoffeeCupType的附加属性,所以,如果我们能定义一个类集合,那么上面的逻辑就会简化很多。
这里作者定义了一个抽象类:
public abstract class Enumeration{private readonly int _value;private readonly string _name;protected Enumeration(){ }protected Enumeration(int value, string name){_value = value;_name = name;}public int Value { get { return _value; } }public string Name { get { return _name; } }public override string ToString(){return Name;}public static IEnumerable GetAll() where T:Enumeration{var type=typeof(T);var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly);return fields.Select(f=> f.GetValue(null)).Cast();}public override bool Equals(object? obj){if (obj is not Enumeration otherObj)return false;var typeMatcher=GetType().Equals(obj.GetType());var valueMatcher=_value.Equals(otherObj.Value);return typeMatcher && valueMatcher;}public override int GetHashCode(){return Value.GetHashCode();}public static int AbsoluteDifference(Enumeration firstValue,Enumeration secondValue){var absoluteDifference=Math.Abs(firstValue.Value - secondValue.Value);return absoluteDifference;}public static T FromValue(int value) where T:Enumeration,new(){var matchingItem = Parse(value, "value", item => item.Value == value);return matchingItem;}public static T FromValue(string name) where T:Enumeration,new(){var matchingItem=Parse(name, "name", item => item.Name == name);return matchingItem;}public static T Parse(K value,string description,Func predicate) where T:Enumeration{var matchingItem=GetAll().FirstOrDefault(predicate);if(matchingItem == null){throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");}return matchingItem;}public int CompareTo(object other)=>Value.CompareTo(((Enumeration)other).Value);}
具体代码我就不讲解了,成员属性都很好理解。有了上面的枚举类,我们可以将CoffeeCupType类派生于此,然后定义三个静态成员,以此限定范围。
public class CoffeeCupType2:Enumeration{public static readonly CoffeeCupType2 Middle = new CoffeeCupType2(0, "中杯",1.0);public static readonly CoffeeCupType2 Large = new CoffeeCupType2(1, "大杯",1.5);public static readonly CoffeeCupType2 SuperLarge = new CoffeeCupType2(2, "特大杯",2.0);public CoffeeCupType2() { }public CoffeeCupType2(int value,string displayName,double rate):base(value, displayName){_rate = rate; }private readonly double _rate;public static IEnumerable List()=>new[] {Middle,Large,SuperLarge};public static CoffeeCupType2 FromName(string name){var state= List().Single(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase));if(state==null){throw new InvalidDataException("Invalid Name!");}return state;}public static CoffeeCupType2 FromValue(int value){var state = List().Single(s => s.Value==value);if (state == null){throw new InvalidDataException("Invalid Value!");}return state;}public double GetRate()=>_rate;public void EntryWord() => Console.WriteLine($"你现在换成了{Name}");}
这下,就可以将枚举当成一个完整的类来使用,这个类很像DDD中定义的“值对象”,然后获取支付订单的函数就变为:
public double GetPay(){return CoffeCupType_2!.GetRate() * CoffeBeans;}
优化支付函数,避免Switch分支语句只是其中的一个好处,你还可以在CoffeCupType2中添加一些行为函数,让其作为应用中作为“状态”时,变成“充血模型”。
你还可以创建子类型:
private class GlassBottle:CoffeeCupType2{public GlassBottle() : base(3, "畅饮杯", 3.0) { }public new void EntryWord() => Console.WriteLine($"只有尊贵的VIP才可以买{Name}装咖啡,可以无限续杯");}public static readonly CoffeeCupType2 Free = new GlassBottle();
这个类型不会暴露给外面。如果你看过里面的代码,就注意到了我还添加了一个函数:EntryWord,也就是带有一定内建行为,我想在替换杯子类型时,能够出发这个行为:
因此在Coffe中,给类型赋值时会调用这个函数:
private CoffeeCupType2? _coffeCupType2;public CoffeeCupType2? CoffeCupType_2 {get { return _coffeCupType2; }set{if(value is CoffeeCupType2 c){_coffeCupType2 = c;_coffeCupType2.EntryWord();}else{_coffeCupType2 = CoffeeCupType2.Middle;}} }
枚举类在你需要构建充血模型时很有用,你可以让这些枚举值附带特定的行为逻辑,这对于构建DDD中的领域模型时很有帮助的,但是并不是什么时候都需要枚举类。在某些简单的场景,还是用普通枚举即可。
值得一提的是,枚举类已经有人实现了这个类库,而且相当强大,这样就不要自己写Enumeration抽象类了。具体可以在github上搜SmartEnum。
Nuget地址:SmartEnum。
本文示例代码:EnumerationClass