在微软的微服务架构案例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.使用枚举
假设你要去买咖啡,正常情况下会有“大、中、小”三种规格,所以一般情况下会让你给出尺寸需求,不同的大小价格自然不同,出于节约,默认是小杯。好了我们应该怎么实现呢?
首先定义一个枚举类型,表明所要分量:
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的附加属性,所以,如果我们能定义一个类集合,那么上面的逻辑就会简化很多。
2.enumeration类
这里作者定义了一个抽象类:
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<T> GetAll<T>() 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<T>();
}
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<T>(int value) where T:Enumeration,new()
{
var matchingItem = Parse<T, int>(value, "value", item => item.Value == value);
return matchingItem;
}
public static T FromValue<T>(string name) where T:Enumeration,new()
{
var matchingItem=Parse<T, string>(name, "name", item => item.Name == name);
return matchingItem;
}
public static T Parse<T,K>(K value,string description,Func<T,bool> predicate) where T:Enumeration
{
var matchingItem=GetAll<T>().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<CoffeeCupType2> 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中的领域模型时很有帮助的,但是并不是什么时候都需要枚举类。在某些简单的场景,还是用普通枚举即可。
3.拓展
值得一提的是,枚举类已经有人实现了这个类库,而且相当强大,这样就不要自己写Enumeration抽象类了。具体可以在github上搜SmartEnum。
这里贴一个官方例子感受一下:
using Ardalis.SmartEnum;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartEnumStudy
{
/// <summary>
/// 可以实现对状态的控制
/// </summary>
public abstract class OrderStatus:SmartEnum<OrderStatus>
{
public static readonly OrderStatus New = new NewStatus();
public static readonly OrderStatus Accepted = new NewStatus();
public static readonly OrderStatus Paid = new NewStatus();
public static readonly OrderStatus Cancelled = new NewStatus();
private OrderStatus(string name, int value) : base(name, value) { }
public abstract bool CanTransitionTo(OrderStatus next);
private sealed class NewStatus : OrderStatus
{
public NewStatus() : base("New", 0)
{
}
public override bool CanTransitionTo(OrderStatus next) =>
next == OrderStatus.Accepted || next == OrderStatus.Cancelled;
}
private sealed class AcceptedStatus : OrderStatus
{
public AcceptedStatus() : base("Accepted", 1)
{
}
public override bool CanTransitionTo(OrderStatus next) =>
next == OrderStatus.Paid || next == OrderStatus.Cancelled;
}
private sealed class PaidStatus : OrderStatus
{
public PaidStatus() : base("Paid", 2)
{
}
public override bool CanTransitionTo(OrderStatus next) =>
next == OrderStatus.Cancelled;
}
private sealed class CancelledStatus : OrderStatus
{
public CancelledStatus() : base("Cancelled", 3)
{
}
public override bool CanTransitionTo(OrderStatus next) =>
false;
}
}
}
Nuget地址:SmartEnum。
本文示例代码:EnumerationClass