C#: 枚举反模式

7

有人说枚举类型违反了Clean Code原则,因此我正在寻找人们最喜欢的枚举反模式以及这些反模式的替代方案。

例如,我见过以下代码:

switch(enumValue) {
    case myEnum.Value1:
        // ...
        break;
    case myEnum.Value2:
        // ...
        break;
}

这种方法比使用魔法字符串的switch语句更好一步,但可能可以通过工厂、容器或其他模式更好地解决问题。

甚至可以像这样使用老式代码:

if(enumValue == myEnum.Value1) {
   // ...
} else if (enumValue == myEnum.Value2) {
   // ...
}

你遇到过哪些枚举反模式和更好的实现方式?

4
比枚举更好的字典?你能解释一下为什么吗? - Oren A
5
你混合了所有东西。switch与枚举本身并不是不好的。有时它是必要的,比如在提到的工厂中。 - Andrey
1
你应该使用枚举类型作为字典的键。这样你就可以同时获得强类型的字典和不再需要冗长的 switch 语句。 - VitalyB
一个字典如何根据值执行不同的功能? - cjk
1
@ck Dictionary<myEnum,Action<> > -> actionDictionary[enumValue](); - Pasi Savolainen
显示剩余2条评论
5个回答

11

我认为枚举非常有用。我写了一些扩展方法来增强它的价值。

首先,有一个Description扩展方法。

public static class EnumExtensions
{
    public static string Description(this Enum value)
    {
        var entries = value.ToString().Split(ENUM_SEPERATOR_CHARACTER);
        var description = new string[entries.Length];
        for (var i = 0; i < entries.Length; i++)
        {
            var fieldInfo = value.GetType().GetField(entries[i].Trim());
            var attributes = (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
            description[i] = (attributes.Length > 0) ? attributes[0].Description : entries[i].Trim();
        }
        return String.Join(", ", description);
    }
    private const char ENUM_SEPERATOR_CHARACTER = ',';
}

这将使我能够像这样定义一个枚举:

 public enum MeasurementUnitType
 {
    [Description("px")]
    Pixels = 0,
    [Description("em")]
    Em = 1,
    [Description("%")]
    Percent = 2,
    [Description("pt")]
    Points = 3
 }

通过执行以下代码可以获取标签:var myLabel = rectangle.widthunit.Description()(无需使用switch语句)。

如果rectangle.widthunit = MeasurementUnitType.Pixels,则会返回"px";如果rectangle.widthunit = MeasurementUnitType.Pixels | MeasurementUnitType.Em,则会返回"px,em"。

然后,这里有一个

    public static IEnumerable<int> GetIntBasedEnumMembers(Type @enum)
    {
        foreach (FieldInfo fi in @enum.GetFields(BindingFlags.Public | BindingFlags.Static))
            yield return (int)fi.GetRawConstantValue();
    }

哪种方法可以让我遍历任何具有基于int值的枚举并返回int值本身。

我认为这些在已经有用的概念中非常有用。


我们为什么不使用Dictionary<TEnum, TDescString>来缩短和简化代码呢?上述代码有什么价值? - Seb Nilsson
3
@Seb:有几个原因:首先,如果你使用字典,描述与声明相邻,而不是放在其他地方。其次,枚举类型始终附带描述,这会导致...最后,该类型可以被导入到另一个程序集中,枚举值及其描述可以被反射并呈现给用户(对编辑器很有用,我也做过这样的事情)。 - Skizz
感谢Skizz,省了我很多时间;)做得好。 - danijels
@Skizz 很好的观点。这都是哲学问题。有人可能会说另一个程序集不知道这个属性,但他们可以查字典。如果它们彼此相关,也许枚举和查找方法应该由一个共同的类包装起来。但再次强调,所有的观点都是有效的,谢谢。 - Seb Nilsson
一个通用的方法 在这里 - nawfal

2

这完全取决于您对枚举类型的使用目的。

  1. If you are trying to stop your developers from passing magic numbers into your operations and you want to keep the data referential integrity intact with your DB then, YES! Use T4-Templates (using your ORM) to go to your MeasurementUnitTypes table and generate a enum with the ID, Name and Description columns matching the enum’ int, Enum_Name and Description Attribute (nice approach for additional field\data to enum @danijels) as suggested above. If you add a new Measurement Type to your MeasurementUnitTypes table you can just right click and run the T4-Template and the enum code is generated for that new row added in the table. I don’t like hard-coded data in my application that doesnt link to my DB hence the mention of the T4-Template approach. It is not extensible otherwise...what if some other external system wants to retrieve our Measurement Criteria used in our system, then it is hard-coded in the system and you can't expose it to the client via a service. That left there.

  2. If the purpose is not data related and you have some logic assigned to a specific enum then NO! this violates the SOLID (Open close principle) as you would somewhere in your application apply a switch or bunch of Ifs to action the logic per enum, ALSO if you did it REALLY bad these switches or Ifs are all over the show....good luck adding a new enum... so it is not open for extension and closed for modification as you need to modify existing code, as per the SOLID principle.

    If your choice is 2 then I suggest then to replace your enum with the following using the example from @danijels comment:

    public interface IMeasurementUnitType
    {
        int ID { get; }
    
        string Description { get; }
    
        // Just added to simulate a action needed in the system
        string GetPrintMessage(int size);
    }
    
上述代码定义了每个测量应遵循的接口(代码契约)。现在让我们定义百分比和像素测量:
    public class PixelsMeasurementUnitType : IMeasurementUnitType
    {
        public int ID => 1;

        public string Description => "Pixel";

        public string GetPrintMessage(int size)
        {
            return $"This is a {Description} Measurement that is equal to {size} pixels of the total screen size";
        }
    }

    public class PercentMeasurementUnitType : IMeasurementUnitType
    {
        public int ID => 2;

        public string Description => "Persentage";

        public string GetPrintMessage(int size)
        {
            return $"This is a {Description} Measurement that is equal to {size} persent of total screen size (100)";
        }
    }

我们定义了两个类型,我们会在代码中使用它们,如下所示:

    var listOfMeasurmentTypes = AppDomain.CurrentDomain.GetAssemblies()
                        .SelectMany(s => s.GetTypes())
                        .Where(p => typeof(IMeasurementUnitType).IsAssignableFrom(p) 
                                    && !p.IsInterface)
                        .ToList();

这里我们获取所有继承IMeasurementUnitType接口的类型,而不是接口本身。现在我们可以使用Activator创建类的实例来填充我们的UI控件:
    public IEnumerable<IMeasurementUnitType> GetInstantiatedClassesFromTypes(List<Type> types)
    {
        foreach (var type in types)
        {
            yield return (IMeasurementUnitType)Activator.CreateInstance(type);
        }
    }

您可以将上面的代码更改为通用类型,现在情况发生了变化,客户提出了一个名为“Point”的新测量单位类型作为新需求,我不需要更改任何代码,只需添加新类型(扩展代码而非修改)。新类型将自动在应用程序中被识别。

    public class PointMeasurementUnitType : IMeasurementUnitType
    {
        public int ID => 3;

        public string Description => "Point";

        public string GetPrintMessage(int size)
        {
            return $"This is a {Description} Measurement that is equal to {size} points of total screen size";
        }
    }

一个好的想法是在启动应用程序时缓存您的类型以获得性能优势,或尝试使用您选择的DI容器。
此外,可以说你的应用程序中某个地方需要区分类型,我同意,但你想让苹果和苹果保持一致。因此,请尽可能应用相同的原则用于这些类型。如果该类型用于某种图形处理器(例如)类,则具有IGraphicsProcessor并且具有用于区分这些类型的具体类,例如PersentageAndPixelGraphicsProcessor(扩展自IGraphicsProcessor),或者如果它仅区分一种类型,则称其为PersentageGraphicsProcessor。
很抱歉我写了一个巨大的SA,但我真的很喜欢枚举,但是当您尝试使用枚举分离逻辑时,它是一个强烈的反模式。
欢迎评论,

1
这不是一个答案,而是为 Enum 反模式列表做出贡献。
今天早上在代码审查中,我遇到了一个类似于以下情况的案例,全部出现在同一个类中。
两种情况:
  1. 喝酒前
  2. 喝酒后

..

    public enum ListEnum 
    { 
        CategoryOne,
        CategoryTwo,
        CategoryThree,
        CategoryFour
    }


    public class UIELementType
    {
        public const string FactoryDomain = "FactoryDomain";
        public const string Attributes = "Attributes";
    }

0

在编程中使用枚举并不是反模式。在一些关于重构的书籍中,这段代码被用来演示如何用多态性来替换它。当你在代码中过度使用枚举时,这种做法就没问题了。


1
那不是说的内容。我是在询问涉及枚举类型的反模式。 - Seb Nilsson

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接