如何编写其逻辑受到保护以防止未来其他枚举的代码?

我很难描述这个问题。 也许这就是为什么我很难找到一个好的解决方案(这些话只是不合作)。 让我通过代码解释:

// original code enum Fruit { Apple, Orange, Banana, } ... Fruit fruit = acquireFruit(); if (fruit != Fruit.Orange && fruit != Fruit.Banana) coreFruit(); else pealFruit(); eatFruit(); 

现在假装多年的发展与这三种类型。 上述逻辑的不同风格在存储过程,SSIS包,Windows应用程序,Web应用程序,Java应用程序,perl脚本等中传播….

最后:

 // new code enum Fruit { Apple, Orange, Banana, Grape, } 

大多数时候,“系统”运行正常,直到使用Grapes。 然后,当不需要或不需要时,系统的某些部分会不恰当地起作用,剥离和/或取芯葡萄。

你坚持什么样的指导方针,以避免这些混乱? 我的偏好是旧代码如果没有重构以考虑新的枚举,则抛出exception。

我在黑暗中想出了一个镜头:

#1避免像这样“不在逻辑中”

 // select fruit that needs to be cored select Fruit from FruitBasket where FruitType not in(Orange, Banana) 

#2需要时使用精心构造的NotIn()方法

 internal static class EnumSafetyExtensions { /* By adding enums to these methods, you certify that 1.) ALL the logic inside this assembly is aware of the * new enum value and 2.) ALL the new scenarios introduced with this new enum have been accounted for. * Adding new enums to an IsNot() method without without carefully examining every reference will result in failure. */ public static bool IsNot(this SalesOrderType target, params SalesOrderType[] setb) { // SetA = known values - SetB List seta = new List { SalesOrderType.Allowance, SalesOrderType.NonAllowance, SalesOrderType.CompanyOrder, SalesOrderType.PersonalPurchase, SalesOrderType.Allotment, }; setb.ForEach(o => seta.Remove(o)); // if target is in SetA, target is not in SetB if (seta.Contains(target)) return true; // if target is in SetB, target is not not in SetB if (setb.Contains(target)) return false; // if the target is not in seta (the considered values minus the query values) and the target isn't in setb // (the query values), then we've got a problem. We've encountered a value that this assembly does not support. throw new InvalidOperationException("Unconsidered Value detected: SalesOrderType." + target.ToString()); } } 

现在,我可以安全地使用这样的代码:

 bool needsCoring = fruit.IsNot(Fruit.Orange, Fruit.Banana); 

如果这个代码在整个系统中传播,那么当Grape进入城镇时会抛出exception(qa会抓住它们)。

无论如何,那是计划。 这个问题似乎应该很常见,但我似乎无法在谷歌上找到任何东西(可能是我自己的错)。

你们是怎么处理这个的?

更新:

我觉得这个问题的答案是创建一个“捕获其他所有”的机制,停止处理并提醒测试人员和开发人员注意新枚举需要考虑的事实。 如果你拥有它,那么“切换…默认”是很棒的。

如果C# 没有 switch …默认,我们可能会对上面的代码如下:

 Fruit fruit = acquireFruit(); if (fruit != Fruit.Orange && fruit != Fruit.Banana) coreFruit(); else if(fruit == Fruit.Apple) pealFruit(); else throw new NotSupportedException("Unknown Fruit:" + fruit) eatFruit(); 

免责声明:

你真的不应该使用上面的任何伪代码。 它可能(?)编译甚至工作,但它确实是可怕的代码。 如果你正在寻找一个基于OOP的方法,我在这个post中看到了很多很好的解决方案。 当然,一个好的解决方案是将所有切换和检查放在一个集中的方法中(工厂方法让我感到震惊)。 此外,还需要进行同行代码审查。

如果我正确理解了您的问题,最常见的做法是抛出NotSupportedExceptionNotImplementedException

 switch (fruit.Kind) { case Fruit.Apple: Bite(fruit); break; case Fruit.Banana: FeedToMonkey(fruit); break; default: throw new NotSupportedException("Unknown fruit."); } 

至于添加新的枚举值会破坏现有的if-not-is逻辑,我相信在这种情况下使用枚举是一个糟糕的选择。 你的物品显然有明显不同的行为,它们不像是颜色。 也许最好让选项负责决定如何对待它们 。 然后你应该用多态替换枚举。

我会使用类型而不是枚举来表示数据结构… EG创建一个具有以下内容的接口IFruit

 interface IFruit { bool NeedsCoring(); void GetEaten(Person by); // etc. } 

然后我会调用已经存在的方法来确定它是否需要核心或诸如此类。

大多数时候,“系统”运行正常,直到使用Grapes。 然后,当不需要或不需要时,系统的某些部分会不恰当地起作用,剥离和/或取芯葡萄。

在我看来,问题是引入了一种新的数据类型。 您可能需要考虑使用一种访问者模式对类进行建模,特别是因为此模式适用于具有固定数量明确定义的数据类型的相关对象:

 public abstract class Fruit { public abstract T Match(Func f, Func g, Func h); public class Apple { // apple properties public override T Match(Func f, Func g, Func h) { return f(this); } } public class Banana { // banana properties public override T Match(Func f, Func g, Func h) { return g(this); } } public class Grape { // grape properties public override T Match(Func f, Func g, Func h) { return h(this); } } } 

用法:

 public void EatFruit(Fruit fruit, Person p) { // prepare fruit fruit.Match( apple => apple.Core(), banana => banana.Peel(), grape => { } // no steps required to prepare ); p.Eat(fruit); } public FruitBasket PartitionFruits(List fruits) { List apples = new List(); List bananas = new List(); List grapes = new List(); foreach(Fruit fruit in fruits) { // partition by type, 100% type-safe on compile, // does not require a run-time type test fruit.Match( apple => apples.Add(apple), banana => bananas.Add(banana), grape => grapes.Add(grape)); } return new FruitBasket(apples, bananas, grapes); } 

这种风格是有利的,原因有三:

  • 未来validation:假设我添加Pineapple类型并将其添加到我的Match方法: Match(..., Func k); 。 现在我有一堆编译错误,因为Match所有当前用法都传递了3个参数,但是我们期望4.代码不会编译,直到修复Match所有用法来处理你的新类型 – 这使得它无法引入一种新类型,有可能无法在您的代码中处理。

  • 类型安全性Match语句使您无需运行时类型测试即可访问子类型的特定属性。

  • 可重构 :如果您不喜欢上面显示的委托,或者您有几十种类型并且不想全部处理它们,那么通过FruitVisitor类很容易包装这些委托,因此每个子类型都将自己传递给适当的方法它是FruitVisitor。

这是一个有效的问题,并且当您针对在您的控件之外定义的枚举编写代码时,这种代码最常出现,并且可能独立于您的代码而发展。 我指的是操作系统或执行环境定义的枚举,例如.NET本身。 .NET(策略)明确允许枚举在引用的程序集的修订版本之间进行扩展,这不算是一个重大变化。

最常见的方法之一就是编写代码,如果它收到一个它没有准备好的枚举,则抛出exception。 这是一个简单的解决方案,但根据您使用枚举的内容,它可能有些过分。

如果你使用枚举来决定执行哪一组固定的操作,那么如果遇到意外的枚举,你就没有多少选择。

但是,如果您使用枚举来决定是否应该在默认行为之上执行其他专门操作,那么意外枚举不一定是世界末日的情况。

这是一个判断调用,只有你可以在你所做的事情和enum实际代表的语境中做出。 如果您可以编写您对所有枚举案例都可以接受的高可信度的默认行为,那么您可以决定接受将来的枚举扩展而无需重写代码。 这个判断调用还需要评估你的信念水平,即enum的所有者会通过不将完全不相关的东西扔进需要完全不同处理的枚举来适当地扩展enum。

虽然我没有大量的C#经验,但是这个Java,我要做的是创建一个接口IPeelable和另一个ICoreable ,并让水果类实现这些。 然后,你可以简单地检查你所获得的水果是否实现了任何接口 – 这样,你可以添加未来的水果,实现可剥离和可核心的水果,如甜瓜。

您不能在一个数据存储中保存两个数据。 您需要存储两个数据,因此枚举是错误的数据类型。 这些应该是Fruit类的实例。

很多人都有很好的建议,但是让我添加另外一个不需要完全重新设计代码或者支持对象/类的地方(比如sql)显然不支持这些东西。

你说:

 Fruit fruit = acquireFruit(); if (fruit != Fruit.Orange && fruit != Fruit.Banana) coreFruit(); else pealFruit(); eatFruit(); 

如果引入Grapes,这将以意想不到的方式绝对打破。 我认为更好的方法是:

 Fruit fruit = acquireFruit(); Boolean fruitPrepared = false; if (fruit == Fruit.Orange || fruit == Fruit.Banana) { pealFruit(); fruitPrepared = true; } if (fruit == Fruit.Apple) { coreFruit(); fruitPrepared = true; } if (!fruitPrepared) throw new exception(); eatFruit(); 

第三种方法非常相似:

 Fruit fruit = acquireFruit(); switch(fruit) { case Fruit.Orange: case Fruit.Banana: pealFruit(); break; case Fruit.Apple: coreFruit(); break; default: throw new exception('unknown fruit detected'); break; } 

当你超出你明确编码的内容时,上述每一个都会以明确的方式中断。 要带走的主要是你明确地为一个已知的条件做某事,而不是默认为一个未知条件的东西。 这可能是一种更好的措辞方式。

使用社交因素!

 enum Fruit { Apple, Orange, Banana, // add Grape here, and I'll shoot you // not kidding. } 

对我来说它会起作用(即让我深入研究应用程序的内部设计,不要仅根据“轻量级”假设引入更改):))

你问的是错误的问题。 你正在寻找编码工作来解决什么是不合适的设计。 您希望以最小的麻烦添加到枚举中。

我喜欢你使用enum作为水果类型的想法,但我将它作为Fruit类中的一个字段。

我会使用一个类而不是一个接口。 你想要捕捉水果的概念。 这是具体的事情。 如果你想添加“果实”的行为或“质量”,OTOH的界面会很好。 你想拥有很多不同类型的“水果”(一个类),你不会在其他非水果的东西中添加“水果能力”(界面)。

不是为每种水果都有一个基础“Fruit”类和子类,只需要为该类型设置一个实例变量 – 并使其成为枚举。 否则,子类的数量可能会失控。 现在每种都是“水果”。 “类型”字段告诉我们什么样的。

现在我们已经有了Fruit类的基本思想,将果皮/核心理念添加为另一个领域。 如果只有这两个选项,则字段可以是布尔值,“isPeelable”。 如果在其他地方,或将来可能存在其他选择,如“粉碎”或“采摘”,现在枚举可能是一个好主意,就像它是水果类型字段一样。 我想类的实例字段可能被称为“prepToEat”?

从这里开始变得有趣。 您的设计需要灵活适应新的水果类型。 此外,您看起来每个“prepToEat”值都有一个方法。 我们将不会在上面的#1,#2中描述那些“exception编码”废话。

因为每个水果都有几个动态部分,我们将创建一个工厂类。 这将所有不同类型的细节 – 更重要的是,未来的代码更改 – 整合到一个类中。

关键设计元素使用“prepToEat”方法的委托。 这再次阻止我们在向我们的repitore添加水果时直接修改Fruit类。

  public class FruitEater { ArrayList myFruit; FruitFactory myFruitMaker; public FruitEater() { this.myFruit = new Arraylist(); this.myFruitMaker = new FruitFactory(); } public static void Main( args[] stuff ) { myFruit.Add( myFruitMaker.Create( FruitType.Orange )); myFruit.Add( myFruitMaker.Create( FruitType.Apple )); foreach ( Fruit a in myFruit ) { a.eat(); //FINALLY!!!! } } } //FruitEater class public class Fruit { public delegate void PrepareToEatDelegate(); protected FruitType type; protected PrepType prepType; // pretend we have public properties to get each of these attributes // a field to hold what our delegate creates. private PrepareToEatDelegate prepMethod; // a method to set our delegate-created field public void PrepareToEatMethod( PrepareToEatDelegate clientMethod ) { prepMethod = clientMethod; } public void Eat() { this.prepMethod(); // do other fruit eating stuff } public Fruit(FruitType myType ) { this.type = myType; } } public class FruitFactory { public FruitFactory() { } public Fruit Create( FruitType myType ) { Fruit newFruit = new Fruit (myType); switch ( myType ) { case FruitType.Orange : newFruit.prepType = PrepType.peel; newFruit.PrepareToEatMethod(new Fruit.PrepareToEatDelegate(FruitFactory.PrepareOrange)); break; case FruitType.Apple : newFruit.prepType = PrepType.core; newFruit.PrepareToEatMethod( new Fruit.PrepareToEatDelegate( FruitFactory.PrepareApple ) ); break; default : // throw an exception - we don't have that kind defined. } return newFruit; }// Create() // we need a prep method for each type of fruit this factory makes public static void PrepareOrange() { // whatever you do } public static void PrepareApple() { // apple stuff } }// FruitFactory public enum FruitType { Orange ,Apple ,Grape } public enum PrepType { peel ,core ,pluck ,smash } 

一般来说,基于类型(枚举或常规)的条件逻辑将会像这样破坏。 添加新类型时,编译器不会强制您更新交换机。

对于这个例子,我不是将大部分逻辑放在一个开关中,而是使用多态。 我将枚举传递给工厂方法/类,返回一个水果接口(或基础水果类型),并在接口上执行虚方法(/ base类型)。 我必须在工厂中使用一个开关,因为没有其他方法可以实现这在逻辑上不相同。 但是因为我返回一个抽象基类,所以我只需要在一个地方进行这种切换。 我确保事后洗手:)

 using System; enum FruitType { Apple, Banana, Pineapple, } interface IFruit { void Prepare(); void Eat(); } class Apple : IFruit { public void Prepare() { // Wash } public void Eat() { // Chew and swallow } } class Banana : IFruit { public void Prepare() { // Peel } public void Eat() { // Feed to your dog? } } class Pineapple : IFruit { public void Prepare() { // Core // Peel } public void Eat() { // Cut up first // Then, apply directly to the forehead! } } class FruitFactory { public IFruit GetInstance(FruitType fruitType) { switch (fruitType) { case FruitType.Apple: return new Apple(); case FruitType.Banana: return new Banana(); case FruitType.Pineapple: return new Pineapple(); default: throw new NotImplementedException( string.Format("Fruit type not yet supported: {0}" , fruitType )); } } } class Program { static FruitType AcquireFruit() { // Todo: Read this from somewhere. A database or config file? return FruitType.Pineapple; } static void Main(string[] args) { var fruitFactory = new FruitFactory(); FruitType fruitType = AcquireFruit(); IFruit fruit = fruitFactory.GetInstance(fruitType); fruit.Prepare(); fruit.Eat(); } } 

我之所以选择Prepare设计,而不是Core / Peel / Deseed / Dehusk / Chill / Cut设计,是因为每种水果都需要不同的准备。 使用分离准备方法的设计,每次添加具有不同要求的新类时,都必须维护所有调用代码(可能还有每个实现)。 通过隐藏特定准备细节的设计,您可以单独维护每个类,添加新的成果不会破坏现有的成果。

请参阅此文章,了解我的设计更适合的原因:

C ++ FAQ Lite – 虚函数