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

Rob*_* H. 19 c# metaprogramming

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

// original code
enum Fruit
{ 
    Apple,
    Orange,
    Banana,
}

...

Fruit fruit = acquireFruit();
if (fruit != Fruit.Orange && fruit != Fruit.Banana)
    coreFruit();
else
    pealFruit();
eatFruit();
Run Code Online (Sandbox Code Playgroud)

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

最后:

// new code
enum Fruit
{ 
    Apple,
    Orange,
    Banana,
    Grape,
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

#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<SalesOrderType> seta = new List<SalesOrderType>
        {
            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());
    }
}
Run Code Online (Sandbox Code Playgroud)

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

bool needsCoring = fruit.IsNot(Fruit.Orange, Fruit.Banana);
Run Code Online (Sandbox Code Playgroud)

如果这个代码在整个系统中传播,那么当Grape进入城镇时会抛出异常(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();
Run Code Online (Sandbox Code Playgroud)

免责声明:

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

Dan*_*mov 10

如果我正确地理解了你的问题,最常见的做法是扔一个NotSupportedExceptionNotImplementedException.

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

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


Ric*_*III 9

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

interface IFruit
{
     bool NeedsCoring();
     void GetEaten(Person by);
     // etc.
}
Run Code Online (Sandbox Code Playgroud)

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

  • 所以所有类型的"水果"都可以吃"人"吗? (6认同)

Jul*_*iet 5

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

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

public abstract class Fruit {
    public abstract T Match(Func<Apple, T> f, Func<Banana, T> g, Func<Grape, T> h);

    public class Apple {
        // apple properties
        public override T Match(Func<Apple, T> f, Func<Banana, T> g, Func<Grape, T> h) {
            return f(this);
        }
    }
    public class Banana {
        // banana properties
        public override T Match(Func<Apple, T> f, Func<Banana, T> g, Func<Grape, T> h) {
            return g(this);
        }
    }
    public class Grape {
        // grape properties
        public override T Match(Func<Apple, T> f, Func<Banana, T> g, Func<Grape, T> h) {
            return h(this);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

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<Fruit> fruits)
{
    List<Apple> apples = new List<Apple>();
    List<Banana> bananas = new List<Banana>();
    List<Grape> grapes = new List<Grape>();

    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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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