How do you write code whose logic is protected against future additional enumerations?

后端 未结 10 1531
旧巷少年郎
旧巷少年郎 2021-02-05 08:01

I\'m having a hard time describing this problem. Maybe that\'s why I\'m having a hard time finding a good solution (the words just aren\'t cooperating). Let me explain via cod

相关标签:
10条回答
  • 2021-02-05 08:35

    Most of the time, the "system" runs fine until Grapes are used. Then parts of the system act inappropriately, pealing and/or coring grapes when it's not needed or desired.

    Seems to me like the problem is an introduction of a new data type. You might want to consider modeling your classes using a kind of visitor pattern, particularly since this pattern is intended for related objects with a fixed number of well-defined data types:

    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);
            }
        }
    }
    

    Usage:

    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);
    }
    

    This style is advatageous for three reasons:

    • Future proofing: Lets say I add a Pineapple type and add it to my Match method: Match(..., Func<Pineapple, T> k);. Now I have a bunch of compilation errors, because all current usages of Match pass in 3 params, but we expect 4. The code doesn't compile until fix all usages of Match to handle your new type -- this makes it impossible to introduce a new type at the risk of not being handled in your code.

    • Type safety: The Match statement gives you access to specific properties of sub-types without a runtime type-test.

    • Refactorable: If you don't like delegates as shown above, or you have several dozen types and don't want to handle them all, its perfectly easy to wrap those delegates by a FruitVisitor class, so each subtype passes itself to the appropriate method it the FruitVisitor.

    0 讨论(0)
  • 2021-02-05 08:35

    Use the social factor!

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

    With me it would work (i.e. make me study the application's inner design deep enough not to introduce changes based on "lightweight" assumptions only) :))

    0 讨论(0)
  • 2021-02-05 08:41

    Generally speaking, conditional logic based on types (enumerated or regular) are going to break like this. When a new type is added, the compiler won't force you to update the switch.

    For this example, instead of placing the majority of my logic in a switch, I would use polymorphism. I would pass the enum to a factory method/class, get back a fruit interface (or base fruit type), and execute virtual methods on the interface (/base type). I have to use a switch in the factory, because there is no other way to implement this that isn't logically identical. But because I return an abstract base class, I only have to do this switch in one place. I make sure to wash my hands afterwards :)

    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();
        }
    }
    

    The reason I went for a Prepare design, rather than a Core/Peel/Deseed/Dehusk/Chill/Cut design is that each fruit will need different preparation. With the design that separates preparation methods, you'll have to maintain all calling code (and possibly each implementation) each time you add a new class with different requirements. With the design that hides the specific preparation details, you can maintain each class separately, and adding new fruits doesn't break existing ones.

    See this article for why my design is preferrable:

    C++ FAQ Lite - Virtual Functions

    0 讨论(0)
  • 2021-02-05 08:45

    If I understood your question correctly, the most common practice is to throw an NotSupportedException or NotImplementedException.

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

    As for adding new enum values which would break existing if-not-is logic, I believe using enum is a poor choice in this case. Your items clearly have a distinctively different behavior, they're not like e.g. colors. Perhaps it is best to make the options responsible for deciding how they should be treated. Then you should replace enums with polymorphism.

    0 讨论(0)
  • 2021-02-05 08:49

    I would use types, not enums, for the data structures... E.G. Create an interface IFruit that has the following:

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

    And then I would just call the methods already there for determining whether it needs to be cored or whatnot.

    0 讨论(0)
  • 2021-02-05 08:49

    You cannot hold two pieces of data in one data store. You need to store two pieces of data, and therefore an enum is the wrong data type for this. These should be instances of a Fruit class.

    0 讨论(0)
提交回复
热议问题