|< Free Open Study >|
5.19 A Problem with No Optimal Solution
All of the problems discussed thus far have a satisfactory solution(s). The following problem occurs often in design but has no satisfactory solution; all known solutions have some major drawback. Consider our previous example of a fruit basket (in Section 5.9) that can contain any number of apples, oranges, and bananas. We discussed the role of polymorphism in this example, and why it was to our advantage to design the fruit basket as containing a mixed list of fruit. It often occurs that this mixed-list design is useful for many operations defined for fruit baskets. However, at some later date we decide that one of the derived classes has a special behavior that the others do not. The designer wishes to iterate over all of the fruit in the list for some operations, but then would like to look only at the objects of a particular derived class. For example, consider the case where apples know how to core themselves, but "core" has no relevant meaning to the other types of fruit. Let us also assume that the designer wishes to iterate over the fruit list, telling just the apples to core themselves. How can this be accomplished? We cannot iterate over all the fruit, telling them to core themselves, since only certain fruit know the meaning of that operation. The two most popular solutions either warp the model of fruit by forcing all fruit to know how to core themselves or force the user of the Fruit hierarchy to perform a large amount of bookkeeping (see Figure 5.49).
It is important to be sure that the problem actually exists before discussing the possible solutions. This problem often occurs due to naming problems of methods. Consider the Fruit hierarchy shown in Figure 5.50.
If the designer wants to send the apples the core message, the oranges the section message, and the bananas the peel message, the real problem is found in the naming of the messages. A better solution would be to state that all fruit know how to prepare themselves, but there exists no good default method (i.e., prepare is a pure polymorphic function). The core message of apple, the section message of orange, and the peel message of banana are all renamed to prepare, and the problem is solved (see Figure 5.51).
Assuming that the problem really exists, our options are limited. A naive solution is to have each class in the hierarchy maintain a list of messages to which it can respond. At runtime, the developer asks each object for that list of messages. If the appropriate message is in the list, then the developer can send the right message. In this example, the developer would send each fruit object in the fruit basket a "tell me what you can do" message. He or she then checks to see if core is in the list. If it is, the core message can be sent to the object. The maintenance of the "tell me what you can do" method is the Achille's heel of this solution. Implementors will never be able to keep up with changes to the class due to the implicit, accidental complexity introduced by this solution. This solution is sometimes used appropriately in domains where all objects are restricted as to what operations they can perform/add.
A second, and to many designers the best, solution is to define a core method on the fruit class which is defined as a NOP (see Figure 5.52). The designer can now iterate over all of the fruit in the fruit basket, telling each object to core itself. If a fruit does not know how to core itself, it gets a NOP function for free. If it does know how to core itself, it overrides the default NOP function with its own core function.
While this solution is easy to implement, it has several problems. First, what if someone adds a pineapple, which also knows how to core itself. The original requirement was to walk through a fruit basket and core all of the apples. In this model, the pineapples would also be cored (unexpectedly). This is certainly an undesirable side effect of our design.
The second and larger problem with this design is that we have warped our model (the fruit hierarchy) to satisfy a warped user of the hierarchy. When the designer of the fruit basket decided to mix all of the fruit in a polymorphic list, he stated that he did not want to know the exact type of fruit at runtime and that he would use polymorphic functions of fruit to distinguish the different behaviors of each fruit type. In the next breath he reneged on this statement and stated that he needed to know which of the mixed fruit were apple objects, a major violation of his original model. By forcing Fruit to know about core, we are warping the polymorphic model for one user: Our base class now has derived class information (a clear violation of Heuristic 5.2). If many designers use this model, it is conceivable that each designer will run into a similar situation. If resolved in this manner, the Fruit class will become hopelessly convoluted with derived class-specific functions all defaulted to NOPs. This design choice is often called the "fat interface" solution.
Another solution is to force the designer of the FruitBasket class to take the responsibility for misusing the fruit abstraction. He or she must maintain a list of apples in addition to the list of mixed fruit in the fruit basket (see Figure 5.53). This maintains the proper abstraction of the Fruit hierarchy and puts the extra effort in the hands of the hierarchy users梩hose who are responsible for using it in a way for which it was not intended. However, this design is not without its problems. First of all, the bookkeeping job can get very complicated when several lists must be maintained. This can lead to errors of improper objects being added to the wrong lists or objects getting lost in the fruit basket. Second, how does the fruit basket know when the fruit object being added is an apple instead of an orange or a banana? The method add_item is taking a reference to a fruit as an argument, not an apple. One possibility is to create another method on the FruitBasket class called add_apple. The add_item method is responsible for all fruits other than apples. The problem here is that there is nothing to stop a user of a fruit basket from adding an apple via the add_item method, thereby putting the fruit basket in an invalid state with respect to its semantics. The alternative solution is to ask each fruit being added to the fruit basket its type. If its type needs special handling, as apples do in this example, then the fruit basket can handle that processing. Of course, this leads to explicit case analysis on the type of an object, another maintenance headache (and a violation of Heuristic 5.12).
The moral to the story is that there is no optimal solution. All solutions have problems that need to be addressed. I tend to use the latter design where possible only because I do not believe that users of a hierarchy should cause them to be poorly designed. Many people choose the NOP method solution because it is the easiest to implement (no bookkeeping/case analysis), especially when they own the hierarchy and the class that uses it. In this case its a trade-off between warping their code or warping their code, so they choose the warping easiest to implement. Physical design may play a role in selecting the method. Imagine that the fruit basket is stored in a database where retrieval of each piece of fruit is slow. Clearly, the solution of maintaining a separate list of the apples is more appropriate since we do not want to waste the time of extracting a fruit from the database only to find out that it is an orange or a banana.
|< Free Open Study >|