枚举什么时候不是代码气味?


17

困境

我已经阅读了很多有关面向对象实践的最佳实践书籍,几乎我读过的每一本书都有一部分内容,他们说枚举是一种代码味道。我认为他们已经错过了解释枚举何时有效的部分。

因此,我正在寻找准则和/或用例,其中枚举不是代码的味道,而实际上是有效的构造。

资料来源:

“根据经验,枚举是代码的味道,应将其重构为多态类。[8]” Seemann,Mark,依赖注入,.Net,2011年,第2页。342

[8] Martin Fowler等人,《重构:改进现有代码的设计》(纽约:Addison-Wesley,1999年),第82页。

语境

我陷入困境的原因是交易API。通过以下方法,他们给了我Tick数据流:

void TickPrice(TickType tickType, double value)

哪里 enum TickType { BuyPrice, BuyQuantity, LastPrice, LastQuantity, ... }

我尝试过围绕该API进行包装,因为打破更改是该API的生活方式。我想跟踪包装器上最后收到的刻度线类型的值,并且通过使用ticktypes字典来做到这一点:

Dictionary<TickType,double> LastValues

在我看来,如果将它们用作键,这似乎是对枚举的正确使用。但是我有第二个想法,因为我确实有一个地方可以根据这个集合做出决定,而且我想不出一种消除消除switch语句的方法,我可以使用一个工厂,但是那个工厂仍然会有一个在某处切换语句。在我看来,我只是在移动东西,但仍然闻起来。

找到枚举的不容易,但是要做到的却不那么容易,如果人们可以分享他们的专业知识,利弊,我将不胜感激。

第二个想法

有些决定和行动是基于这些决定和行动的TickType,我似乎想不出消除枚举/切换语句的方法。我能想到的最干净的解决方案是使用工厂并返回基于的实现TickType。即使那样,我仍然会有一个switch语句,该语句返回接口的实现。

下面列出的是示例类之一,在其中我怀疑自己可能使用的枚举错误:

public class ExecutionSimulator
{
  Dictionary<TickType, double> LastReceived;
  void ProcessTick(TickType tickType, double value)
  {
    //Store Last Received TickType value
    LastReceived[tickType] = value;

    //Perform Order matching only on specific TickTypes
    switch(tickType)
    {
      case BidPrice:
      case BidSize:
        MatchSellOrders();
        break;
      case AskPrice:
      case AskSize:
        MatchBuyOrders();
        break;
    }       
  }
}

26
我从来没有听说过枚举是代码的味道。您能提供参考吗?我认为它们对于数量有限的潜在价值非常有意义
Richard Tingle

25
什么书在说枚举是代码的味道?获得更好的书。
JacquesB 2015年

3
因此,问题的答案将是“大部分时间”。
gnasher729

5
一个好的类型安全值可以传达含义吗?这样可以确保在Dictionary中使用正确的键吗?什么时候有代码气味?

7
如果没有上下文,我认为一个更好的措辞将是enums as switch statements might be a code smell ...
pllee

Answers:


31

枚举用于用例,当您逐字地枚举变量可能采用的每个可能值时。曾经 考虑用例,例如一周中的几天或一年中的月份或硬件寄存器的配置值。既高度稳定又可以用简单值表示的事物。

请记住,如果要制作一个反腐败层,由于要包装的设计,就无法避免在某处使用switch语句,但是如果操作正确,则可以将它限制在一个地方,在其他地方使用多态。


3
这是最重要的一点-向枚举添加额外的值意味着在代码中找到对该类型的每次使用,并且可能需要为该新值添加新的分支。与多态类型相比,在多态类型中添加新的可能性仅意味着创建一个实现该接口的新类-更改集中在一个位置,因此更容易实现。如果您确定永远不会添加新的滴答类型,则枚举很好,否则应将其包装在多态接口中。
2015年

那是我一直在寻找的答案之一。当我包装的方法使用一个枚举并且目标是将这些枚举集中在一个地方时,我只是无法避免。我必须进行包装,以便如果API更改了这些枚举,则只需要更改包装,而不必更改包装的使用者。
2015年

@Jules-“如果您确定永远不会添加新的刻度类型...”在许多级别上,该语句都是错误的。尽管每种类型都具有完全相同的行为,但每种类型的类都与一种可能的“合理”方法相去甚远。直到您有不同的行为并开始需要在其中画线的“ switch / if-then”语句为止。当然,这不是基于我将来是否可以添加更多的枚举值。
Dunk

@dunk我想你误解了我的评论。我从未建议过创建具有相同行为的多个类型-这太疯狂了。
2015年

1
即使您“枚举了所有可能的值”,这也不意味着该类型永远不会在每个实例中具有其他功能。甚至Month之类的东西也可以具有其他有用的属性,例如天数。立即使用枚举可以防止扩展您正在建模的概念。基本上,这是一种反OOP机制/模式。
戴夫·库西诺

17

首先,代码气味并不意味着出了什么问题。这意味着可能有问题。enum会因为经常滥用而闻到气味,但这并不意味着您必须避免它们。只是您发现自己打字enum,停下来检查更好的解决方案。

最常发生的特殊情况是,当不同的枚举对应于具有不同行为但具有相同接口的不同类型时。例如,与不同的后端交谈,渲染不同的页面等。使用多态类自然可以更自然地实现这些功能。

在您的情况下,TickType不对应于不同的行为。它们是事件的不同类型或当前状态的不同属性。因此,我认为这是枚举的理想场所。


您是在说枚举包含名词作为条目(例如颜色)时,它们可能被正确使用了。当它们是动词(连接,渲染)时,是否表示它被滥用了?
2015年

2
@stromms,语法是软件体系结构的糟糕指南。如果TickType是接口而不是枚举,那么方法将是什么?我什么也看不到,这就是为什么对我来说这是一个枚举案例。在其他情况下,例如状态,颜色,后端等,我可以提出方法,这就是为什么它们是接口。
温斯顿·埃韦特

@WinstonEwert并进行编辑,看来它们不同的行为。

哦 所以,如果我能想到枚举的方法,那么它可能是接口的候选对象?那可能是一个很好的观点。
2015年

1
@stromms,您可以共享更多开关示例吗,所以我可以更好地了解您如何使用TickType?
温斯顿·埃韦特

8

传输数据枚举时没有代码异味

恕我直言,当传输数据enums用于指示某个字段可以具有受限(很少变化)的一组值时,IMHO 是好的。

我认为它比传输任意strings或更好ints。字符串可能会因拼写和大写字母的不同而引起问题。INTS允许范围内的值传递出来,并没有什么语义(例如,我收到3从您的交易服务,这是什么意思?LastPriceLastQuantity?别的东西吗?

传输对象和使用类层次结构并不总是可能的。例如,不允许接收端区分已发送的类。

在我的项目中,该服务使用类层次结构来实现操作效果,就在通过DataContract对象进行传输之前,该类层次结构中的对象被复制到union-like对象中,该对象包含一个enum用于指示类型的对象。客户端接收,DataContract并使用的enum值在层次结构中switch创建类的对象,并创建正确类型的对象。

之所以不希望传输类的对象的另一个原因是,对于所传输的对象(例如LastPrice),服务可能需要与客户端完全不同的行为。在这种情况下,不希望发送类及其方法。

switch语句不好吗?

恕我直言,根据调用不同构造函数的单一 switch语句enum不是代码味道。它不一定比其他方法更好或更坏,例如基于类型名的反射;这要看实际情况。

enum到处都有开关是一种代码味道,提供了通常更好的替代方法:

  • 使用具有已覆盖方法的层次结构中不同类别的对象;即多态性。
  • 当层次结构中的类很少更改并且(许多)操作应松散地耦合到层次结构中的类时,请使用访问者模式。

实际上,TickType是作为电线通过电线传输的int。我的包装是TickType从接收到的使用的包装器int。多个事件使用带有各种变化的签名的此ticktype,这些签名是对各种请求的响应。int连续使用不同的功能是常见的做法吗?例如TickPrice(int type, double value)使用1,3和6,typeTickSize(int type, double value使用2,4和5?将它们分为两个事件甚至有意义吗?
2015年

2

如果使用更复杂的类型该怎么办:

    abstract class TickType
    {
      public abstract string Name {get;}
      public abstract double TickValue {get;}
    }

    class BuyPrice : TickType
    {
      public override string Name { get { return "Buy Price"; } }
      public override double TickValue { get { return 2.35d; } }
    }

    class BuyQuantity : TickType
    {
      public override string Name { get { return "Buy Quantity"; } }
      public override double TickValue { get { return 4.55d; } }
    }
//etcetera

那么您可以从反射加载类型或自己构建类型,但是这里要做的主要事情是您坚持SOLID的Open Close Principle


1

TL; DR

为了回答这个问题,我现在很难思考时间枚举在某种程度上不是代码的味道。他们有一定的意图要有效地声明(此值存在明显的界限,数量有限的可能性),但是它们固有的封闭性使其在结构上次于标准。

对不起,当我重构大量遗留代码时。/叹气; ^ D


但为什么?

很好的评论,为什么LosTechies就是这种情况:

// calculate the service fee
public double CalculateServiceFeeUsingEnum(Account acct)
{
    double totalFee = 0;
    foreach (var service in acct.ServiceEnums) { 

        switch (service)
        {
            case ServiceTypeEnum.ServiceA:
                totalFee += acct.NumOfUsers * 5;
                break;
            case ServiceTypeEnum.ServiceB:
                totalFee += 10;
                break;
        }
    }
    return totalFee;
} 

这与上面的代码具有所有相同的问题。随着应用程序的变大,拥有类似分支语句的机会将会增加。另外,当您推出更多高级服务时,您将不得不不断修改此代码,这违反了开放式封闭原则。这里也有其他问题。计算服务费的功能不需要知道每个服务的实际金额。那就是需要封装的信息。

除了一点:枚举是一个非常有限的数据结构。如果您没有使用枚举来表示真正的整数,则需要一个类来对抽象进行正确建模。您可以使用Jimmy很棒的Enumeration类使用这些类来将它们也用作标签。

让我们重构它以使用多态行为。我们需要的是抽象,它将允许我们包含计算服务费用所需的行为。

public interface ICalculateServiceFee
{
    double CalculateServiceFee(Account acct);
}

...

现在,我们可以创建接口的具体实现,并将其附加到帐户。

public class Account{
    public int NumOfUsers{get;set;}
    public ICalculateServiceFee[] Services { get; set; }
} 

public class ServiceA : ICalculateServiceFee
{
    double feePerUser = 5; 

    public double CalculateServiceFee(Account acct)
    {
        return acct.NumOfUsers * feePerUser;
    }
} 

public class ServiceB : ICalculateServiceFee
{
    double serviceFee = 10;
    public double CalculateServiceFee(Account acct)
    {
        return serviceFee;
    }
} 

另一个实施案例...

底线是,如果您的行为依赖于枚举值,为什么不使用类似接口或父类的不同实现来确保该值存在呢?就我而言,我正在根据REST状态代码查看不同的错误消息。代替...

private static string _getErrorCKey(int statusCode)
{
    string ret;

    switch (statusCode)
    {
        case StatusCodes.Status403Forbidden:
            ret = "BRANCH_UNAUTHORIZED";
            break;

        case StatusCodes.Status422UnprocessableEntity:
            ret = "BRANCH_NOT_FOUND";
            break;

        default:
            ret = "BRANCH_GENERIC_ERROR";
            break;
    }

    return ret;
}

...也许我应该将状态代码包装在类中。

public interface IAmStatusResult
{
    int StatusResult { get; }    // Pretend an int's okay for now.
    string ErrorKey { get; }
}

然后,每次我需要一种新的IAmStatusResult类型时,我都会对其进行编码...

public class UnauthorizedBranchStatusResult : IAmStatusResult
{
    public int StatusResult => 403;
    public string ErrorKey => "BRANCH_UNAUTHORIZED";
}

...现在,我可以确保早期的代码意识到它具有IAmStatusResult作用域并引用它,entity.ErrorKey而不是更复杂的,死胡同_getErrorCode(403)

而且,更重要的是,每次添加新类型的返回值时,都无需添加其他代码即可处理它。Whaddya知道,enumswitch可能是代码气味。

利润。


例如,如果我有一个多态类,并且想通过命令行参数配置我正在使用哪个类,那该怎么办?命令行->枚举->开关/映射->实现似乎是一个非常合理的用例。我认为关键是枚举用于编排,而不是作为条件逻辑的实现。
蚂蚁P

@AntP为什么在这种情况下不使用该StatusResult值?您可能会争辩说,在该用例中,枚举可用作令人难忘的快捷方式,但我可能仍会称其为代码味道,因为有很多不需要封闭集合的替代方法。
鲁芬

还有什么选择?弦吗?从“枚举应该被多态替换”的角度来看,这是一个任意的区别-两种方法都需要将值映射到类型;在这种情况下避免枚举不会有任何效果。
蚂蚁P

@AntP我不确定这里的电线在哪里交叉。如果在接口上引用属性,则可以在需要的任何地方使用该接口,并且在创建该接口的新(或删除现有)实现时永远不要更新该代码。如果您有一个enum每次添加或删除一个值(可能还有很多地方)时,都必须更新代码,具体取决于代码的结构方式。使用多态性代替枚举有点像确保代码采用Boyce-Codd Normal Form
鲁芬

是。现在假设您有N个这样的多态实现。在Los Techies文章中,它们只是对所有这些对象进行迭代。但是,如果我只想基于命令行参数有条件地应用一种实现,该怎么办?现在,我们必须定义从某些配置值到某种可注入实现类型的映射。这种情况下的枚举与任何其他配置类型一样好。这是没有更多机会用多态替换“脏枚举”的情况的示例。按抽象分支只能带您走那么远。
Ant P

0

使用枚举是否有代码味道取决于上下文。如果您考虑表达问题,我认为您可以从一些想法中得到答案。因此,您具有不同类型的集合以及对其进行操作的集合,并且需要组织代码。有两个简单的选项:

  • 根据操作组织代码。在这种情况下,您可以使用枚举标记不同的类型,并在每个使用标记数据的过程中使用switch语句。
  • 根据数据类型组织代码。在这种情况下,您可以用接口替换枚举,并为该枚举的每个元素使用一个类。然后,您可以将每个操作作为每个类中的方法来实现。

哪种解决方案更好?

正如Karl Bielefeldt所指出的,如果您的类型是固定的,并且您希望系统主要通过在这些类型上添加新的操作来增长,那么使用枚举并使用switch语句是一个更好的解决方案:每次您需要一个新的操作时,只需实现一个新过程,而使用类,则必须为每个类添加一个方法。

另一方面,如果您希望有一套相当稳定的操作,但是您认为随着时间的推移将不得不添加更多的数据类型,那么使用面向对象的解决方案会更加方便:由于必须实现新的数据类型,因此您只需继续添加实现同一接口的新类,而如果使用枚举,则必须使用该枚举更新所有过程中的所有switch语句。

如果您无法通过以上两个选项中的任何一个来对问题进行分类,则可以查看更复杂的解决方案(例如,再次参见 上面引述的Wikipedia页面,以进行简短讨论,并提供一些参考资料以供进一步阅读)。

因此,您应该尝试了解应用程序的发展方向,然后选择合适的解决方案。

由于您所参考的书涉及面向对象的范例,因此它们偏爱使用枚举就不足为奇了。但是,面向对象的解决方案并不总是最好的选择。

底线:枚举不一定是代码气味。


请注意,该问题是在OOP语言C#的上下文中提出的。同样,按操作或按类型分组是同一枚硬币的两个面很有趣,但是我认为您所描述的一个并不比另一面“更好”。结果代码量是相同的,并且OOP语言已经可以方便地按类型进行组织。它还会产生实质上更直观(易于阅读)的代码。
Dave Cousineau '18

@DaveCousineau:“我认为您所描述的一个并不比另一个更好”:我并不是说一个总是比另一个更好。我说过,根据具体情况,一个比另一个要好。例如,如果操作或多或少固定的,并且计划增加新的类型(例如一个GUI固定paint()show()close() resize()操作和自定义部件),那么面向对象的方法是更好地在这个意义上,它允许添加新的类型,而不会影响太大许多现有代码(您基本上实现了一个新类,这是本地更改)。
乔治

认为您所描述的都不是“更好” 意思是我不认为这取决于情况。在两种情况下,您必须编写的代码量完全相同。唯一的区别是,一种方法与OOP(枚举)相反,而另一种方法则并非如此。
Dave Cousineau '18

@DaveCousineau:您必须编写的代码量可能是相同的,位置(您必须更改和重新编译的源代码中的位置)急剧变化。例如在OOP中,如果您向接口添加新方法,则必须为实现该接口的每个类添加一个实现。
乔治

这不仅仅是宽度VS深度的问题吗?将1个方法添加到10个文件或将10个方法添加到1个文件。
Dave Cousineau '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.