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

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

    A lot of people have good suggestions, but let me add one other that doesn't require a complete redesign of code or the support of objects/classes in places (like sql) that obviously have no support for such things.

    You stated:

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

    Which will absolutely break in unexpected ways if Grapes are introduced. I think a better approach would be:

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

    A third approach is very similar:

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

    Each of the above breaks in well defined ways when you've gone outside of what you've explicitly coded for. The main thing to take away is that you are explicitly doing something for a known condition as opposed to defaulting to something on an unknown condition. There's probably a better way of phrasing that.

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

    This is a valid concern, and comes up most often when you're writing code against enums that are defined outside of your control, and which may evolve independently of your code. I'm referring to enums defined by the operating system or your execution environment, such as .NET itself. .NET (policy) explicitly allows enums to be extended across revisions of referenced assemblies, and this is not considered a breaking change.

    One the the most common approaches is simply to write your code to throw an exception if it ever receives an enum it isn't prepared for. This is a simple solution, but it may be overkill depending on what you're using the enums for.

    If you're using the enum to decide which of a fixed set of operations to perform, you don't have much choice but to throw if you encounter an unexpected enum.

    However, if you're using the enum to decide if you should perform additional specialized operations on top of default behavior, then it's not necessarily the case that unexpected enums are the end of the world.

    It's a judgment call that only you can make in the context of what your doing and in the context of what the enum actually represents. If you can write default behavior that you have high confidence should be acceptable for all enum cases, then maybe you can decide to accept future enum extensions without having to rewrite your code. This judgment call also requires evaluating your level of faith that the owner of the enum will extend the enum appropriately by not throwing completely unrelated stuff into the enum that requires completely different handling.

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

    You're asking the wrong questions. You're looking for coding work arounds for what is inadequate design. You want to add to your enumeration with minimum hassle.

    I like your idea for using an enum for fruit types, but I'd have that as a field in a Fruit class.

    I would use a class rather than an interface. You want to capture the concept of a fruit. It's a concrete thing. OTOH an interface would be good if you wanted to add the behavior or "quality" of being "fruitable". You want to have lots of different types of "Fruit" (a class), you are not adding "fruit-ability" (an interface) to an otherwise non-fruit thingy.

    Rather than having a base "Fruit" class and sub-classing for every kind of fruit, just have an instance variable for the type - and make it an enumeration. Otherwise the number of sub-classes can get out of hand. Now every kind is a "Fruit". The "type" field tells us what kind.

    Now that we have the fundamental idea of a Fruit class, add the peel/core idea as another field. If there are only these two choices maybe the field could be a boolean, "isPeelable". If there where, or could be in the future, other choices like "smash" or "pluck", now an enumeration might be a good idea, just like it is for the fruit type field. I suppose the class' instance field might be called "prepToEat"?

    From here it gets interesting. Your design needs to be flexible to accommodate new fruit types. Also, it looks like you have a method for each "prepToEat" value. And we shall have none of that "exception coding" crap you describe in #1, #2 above.

    Because each fruit has several dynamic parts we will create a factory class. This puts all the details for making all the different kinds - and more importantly, future code changes - into one class.

    A KEY DESIGN ELEMENT is using a delegate for the "prepToEat" method. This again, prevents us from having to modify the Fruit class directly when we add fruits to our repitore.

      public class FruitEater
      {
         ArrayList<Fruit> 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
      }
    
    0 讨论(0)
  • 2021-02-05 09:02

    While I don't have a ton of C# experience, were this Java, what I would instead do is create an interface IPeelable and another ICoreable, and have the fruit classes implement those. Then, instead of avoiding not logic, you could simply check to see if the fruit you've gotten implements either of the interfaces -- in this way, you can add future fruits which implement both peelable and coreable, like the muskmelon.

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