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

后端 未结 10 1559
旧巷少年郎
旧巷少年郎 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: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

提交回复
热议问题