告别贫血枚举!用C#强类型枚举模式实现真正的面向对象设计

作者:微信公众号:【架构师老卢】
6-7 8:47
10

原生C#枚举就像纸板剪影——只有标签,没有灵魂。需要为每种信用卡类型设置折扣率?准备好面对冗长的switch语句。想添加验证或本地化?你不得不在代码库中搜寻分散的辅助字典。

如果你认为原生枚举"够用",那只是因为还没遇到过需要将业务逻辑塞进巨型switch怪兽的需求场景。

想象一下:用类化的枚举实例取代单调的枚举——每个实例自带数据和行为。想要铂金卡折扣?直接问枚举对象。没有switch,没有魔法数字,没有代码异味。

这就是Milan强类型枚举模式的魔力——将枚举从哑常量升级为一等公民。

1 | 原生枚举的致命缺陷 | 缺陷 | 痛点 | |-------|-------| | 无行为 | 折扣、验证等逻辑需要外部工具类 | | 无扩展数据 | 无法附加显示名称等元数据 | | 难以演进 | 新增枚举值需更新所有switch否则引发bug | | 封装性差 | 逻辑散落各处,枚举无法自治 |

2 | 蓝图:Enumeration基类

public abstract class Enumeration<TEnum> : IEquatable<TEnum>
    where TEnum : Enumeration<TEnum>
{
    public int    Value { get; }
    public string Name  { get; }
    
    protected Enumeration(int value, string name) =>
        (Value, Name) = (value, name);

    public override string ToString() => Name;

    public static IReadOnlyList<TEnum> List { get; }

    static Enumeration()
    {
        List = typeof(TEnum)
            .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
            .Where(f => f.FieldType == typeof(TEnum))
            .Select(f => (TEnum)f.GetValue(null)!)
            .ToList()
            .AsReadOnly();
    }

    public static TEnum FromValue(int value) =>
        List.Single(e => e.Value == value);

    public static TEnum FromName(string name) =>
        List.Single(e => string.Equals(e.Name, name, StringComparison.OrdinalIgnoreCase));

    // 基于Value的相等性比较
    public bool Equals(TEnum? other) => other is not null && Value == other.Value;
    public override bool Equals(object? obj) => obj is TEnum other && Equals(other);
    public override int GetHashCode() => Value;
}
  • 反射机制自动发现所有静态实例
  • 集中式查询方法消除脆弱switch块

3 | 案例:信用卡枚举类 3.1 基础枚举类

public abstract class CreditCard
    : Enumeration<CreditCard>
{
    protected CreditCard(int value, string name) : base(value, name) { }

    public abstract decimal Discount { get; }

    // 静态实例
    public static readonly CreditCard Standard = new StandardCard();
    public static readonly CreditCard Premium  = new PremiumCard();
    public static readonly CreditCard Platinum = new PlatinumCard();

    // 私有子类封装行为
    private sealed class StandardCard : CreditCard
    {
        public StandardCard() : base(1, "Standard") { }
        public override decimal Discount => 0.01m;
    }

    private sealed class PremiumCard : CreditCard
    {
        public PremiumCard() : base(2, "Premium") { }
        public override decimal Discount => 0.05m;
    }

    private sealed class PlatinumCard : CreditCard
    {
        public PlatinumCard() : base(3, "Platinum") { }
        public override decimal Discount => 0.10m;
    }
}

3.2 使用示例

var card = CreditCard.FromValue(1);          // 标准卡
Console.WriteLine($"折扣率: {card.Discount:P}");
// → 折扣率: 1.00 %

var platinum = CreditCard.FromName("Platinum");
Console.WriteLine($"折扣率: {platinum.Discount:P}");
// → 折扣率: 10.00 %

无需switch语句,没有魔法数字,纯多态实现。

4 | 扩展超能力 | 功能 | 实现方式 | |-------|-------| | 附加属性 | 在基类添加如AnnualFee,各子类重写 | | 复杂方法 | 添加virtual/abstract方法如CalculateRewardPoints | | JSON支持 | 编写JsonConverter使用FromValue | | 依赖注入 | 注册CreditCard实例为单例测试桩 |

5 | 测试变得简单

[Theory]
[InlineData(1, 0.01)]
[InlineData(3, 0.10)]
public void Discount_Should_Match_Value(int value, decimal expected)
{
    var card = CreditCard.FromValue(value);
    card.Discount.Should().Be(expected);
}
  • 无需模拟switch语句
  • 新增卡类型自动出现在List中

6 | 权衡与保障 | 考虑因素 | 缓解方案 | |-------|-------| | 反射开销 | 静态构造函数仅应用启动时运行一次 | | 开放集风险 | 标记静态实例为readonly,子类设为private sealed | | 序列化名称 | 使用Name或添加JsonName属性 | | 数据库映射 | 存储Value值,通过FromValue重建 |

🚀 收获

  • 强类型枚举实现数据与行为的结合
  • Enumeration基类提供相等性、查询和反射发现
  • 每个具体枚举值都是类实例——迷你策略模式
  • 消除脆弱switch和辅助字典,增强封装和可测试性
  • 告别贫血枚举,迎接真正面向对象的枚举实现
相关留言评论
昵称:
邮箱:
阅读排行