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

后端 未结 10 1676
旧巷少年郎
旧巷少年郎 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 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);
            }
        }
    }
    

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

    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 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.

提交回复
热议问题