|< Free Open Study >|
11.4 Adding Behavior to Our Object Model
The first problem that many new designers face is one of understandability. Who starts everything in a decentralized system? In a centralized system, the flow of control is obvious. In a decentralized system, it is more hidden. In a decentralized system, the flow of control is started by something outside of the system. This something is either the main function of a C++ application, the SmallTalk environment, the CLOS environment, or more popularly, the constructor of a large containing class that wraps the entire system. The latter solution is often more useful because if the current system is to be reused as a component of a new larger system, it is a simple matter of adding operations to the large containing class. In the other solutions, some reengineering will have to be enacted in order to capture the desired information. Such a containing class is considered to be just outside the domain of the object-oriented model. In this example, we might define a class called FinancialSystem to wrap the entire object model. The constructor of this class builds Bank and ATM objects and wires them together with referential attributes. This is not the god class problem we spoke of earlier in this text, since the class is outside the system and is used only to build the top-level objects in our domain.
Now, let us examine as a first-use case the withdrawal of money from an ATM. Someone puts a card in the card reader, which detects that a card has been inserted and that it is a valid bank card and not an appropriately sized piece of plastic. Once this has been done, who sends a message to whom and what is the message? This is an important question to keep in mind. Invariably, designers will say, "Now we have to verify a PIN number." That is too vague. By forcing specific questions at specific places in design, a designer must justify exactly what he or she is doing. A common first choice at this stage of design is to have the card reader send a message to the display screen asking the display to put up an Enter PIN: prompt (see Figure 11.3). This is inevitably followed by a message from the card reader to the keypad to get a key or PIN number, etc.
This design violates Heuristic 4.14, which states that "objects that share lexical scope should not have uses relationships between them." Why not? We can examine our lexical scope pattern from Chapter 10 to answer that question. First, card readers have nothing to do with display screens except when in the domain of an ATM. We have now rendered the card reader unreusable in domains that do not possess a display screen. If I want to build a security door that also has a card reader, then I need to put a display screen in my security door. Second, and more important, I have added complexity to my design without motivation. The ATM already contains a card reader and display screen; therefore, it has implied uses relationships to them. The containing class can always accomplish any interaction between its pieces with the existing uses relationships. The added relationship is not required. In this example, the ATM should clearly handle the coordination between its pieces. We will see the reason a number of designers fall into this trap a little later in this design discussion.
It is important to note the relationship between heuristics and design patterns here. It is easy to determine when a heuristic is being violated. It is also easy to remember the heuristics since they are rarely more than two sentences of information. Patterns, on the other hand, can be quite long. I do not believe that designers will know intuitively when to select a particular pattern or how to combine them in interesting ways during the design process. Heuristics can provide the glue to help pull this information together.
Having gone this far in the design of the ATM system, a designer is tempted to state, "The card reader should inform the ATM that it has a card, then the ATM can send a message to its display screen to display a prompt" (see Figure 11.4).
The problem with this design is that the card reader is now aware of its container, a violation of a design heuristic whose intention is to make the contained class more reusable (Heuristic 4.13). We do not want card readers to be dependent on ATMs. Again, a solution around this problem can be found in the patterns from Chapter 10. Ideally, we can change the interrupt-driven nature of our design to a polling architecture. Why not let the ATM poll the card reader, asking it if it has a card? If the ATM has nothing else to do, then it can simply block on the card reader. If the ATM has other task, such as monitoring the bank for any interesting information it might be sending asynchronously, then the ATM should run the polling loop. Note that this last point is simply arguing where the polling should be performed, in the ATM or in the card reader.
There may be designs where an interrupt architecture cannot be turned into a polled architecture for physical design reasons, namely, time efficiency. In this case, we should at least make the card reader aware of a more general architecture, say a SecureDevice class. The ATM then inherits from the SecureDevice class, which has no data and a pure polymorphic card_available() method. The old restriction was that card readers could not be reused outside the domain of ATM. The new restriction is that card readers cannot be reused outside the domain of some SecureDevice. Since SecureDevice has no data and only a pure polymorphic method, it is an easy class from which to inherit. Yes, this solution can lead to multiple inheritance in some designs. Proponents of the technique of using callback functions to solve this problem can convince themselves that the proposed inheritance solution is a more strongly typed and less flexible solution than theirs. The callback function solution states that when a card reader is built, it is given the address of a function to call when a card becomes available. In the proposed inheritance solution, we restrict the address of the callback (polymorphic) function to a base class method pointer, which increases robustness.
Our ATM asks the card reader if it has a card, and eventually the answer is yes. The ATM then asks its display screen to put up the prompt for getting a PIN number. Now what happens? The naive designer will state that the ATM sends a get_pin() message to its keypad object. At first glance this sounds useful; however, the specification stated that after every key is pressed, an asterisk appears on the display screen. This may have looked trivial when first reading the specification, but it creates a complex issue in the design. The ATM is now forced to get a single key from the keypad, then tell its display to put up an asterisk, then get the second key pressed, put up an asterisk, ad nauseam. Many designers argue that the ATM is far too important an entity to be dealing with such picky issues. They wish to distribute the intelligence of the ATM to its pieces. Unfortunately, in order to distribute the intelligence of the system to the pieces of the ATM, it appears that the keypad will have to talk to the display screen (or vice versa). Designers often find themselves with this dilemma. A containing class (e.g., ATM) wants to distribute its intelligence to its pieces, but that intelligence is distributed over several of its objects. There is an interesting solution to this problem. What the designer needs is a new containing class, which sits between the original containing class and the pieces in question. Consider the role of the super keypad created in Figure 11.5.
The ATM can now simply tell its SuperKeypad object to fetch it a PIN number. It is oblivious to the details. In a large design project, this form of containment is useful for hiding the complexity of our model. We can go on with design, stating, that "The ATM gets a PIN number from the SuperKeypad and then it …." How does the SuperKeypad get the PIN number? That is someone else's problem.
It is important to note that we had to place a display_msg() operation on the SuperKeypad because the ATM needs to put up its greeting message when first created and after every session with a customer. This operation simply passes through to the display_msg() operation on the display object. This constitutes noncommunicating behavior on the part of the SuperKeypad, that is, behavior that uses a proper subset of the data members of SuperKeypad. The noncommunicating behavior acts as a counterforce in creating these third-party containing classes. Too much noncommunicating behavior suggests that the cohesiveness of the class is low, implying that the class needs to be dissolved into its two pieces. For example, if the public interface of the SuperKeypad class consisted solely of display_msg(), enable(), disable(), and get_key(), then it is a useless abstraction with very low cohesion in its data. It is operations like get_pin() that increase the cohesion of the class and make it a worthwhile addition to our system.
Containing classes of this type cannot be found by data-driven analysts. They can be found only by examining the behavior of the classes in a given system. Examining classes that have more than six data members is a good, mindless method for finding good candidates for this type of problem. This heuristic is useless for showing a designer where in these classes an intermediate containing class might be useful. For this second step, we need to discuss the behavior of the class with respect to its use cases.
The next step in our design revolves around verifying the PIN number given to us by the customer with the PIN on the card. Several popular and equivalent designs address this problem. Some designers state that the ATM should get a PIN number from the card reader (which gets it off of the card), get the PIN from the Super-Keypad, and compare them. Others state that the ATM should get a PIN number from the card reader, give it to the SuperKeypad, and let the SuperKeypad verify the user. These designers then argue whether the ATM or SuperKeypad should perform the looping of three chances before ordering the card confiscated. Still others argue that the ATM should get a PIN number from the SuperKeypad and give it to the card reader who then verifies the number. All of these solutions are valid. The arguments all revolve around the question of how the intelligence should be distributed. Since there is no quantitative metric for measuring complexity (and never will be), this issue is left for debate on qualitative, and therefore subjective, grounds. It is usually desirable to distribute the system intelligence away from a containing class, leaving it with just the coordination activities. However, it often happens that one of the pieces, such as the SuperKeypad, starts picking up too much behavior with respect to the containing class. We can then argue that we want some of the work pushed back onto the containing class. The debate of these three solutions can go on for quite some time during a design critique, with no party getting an upper edge on the other.
I have seen designers create a PIN_Validator object whose purpose is to get the two PIN numbers and verify them. The argument is that policy information is no longer encapsulated in a class like ATM or SuperKeypad, making them more reusable outside the domain. I claim that such controller classes consist only of behavior (often only one piece of behavior) and are the artificial separation of data and behavior. They violate the heuristic of keeping data and behavior in one place, as well as turning an operation into a class. What do PIN_Validator objects do? They validate PIN numbers. This is an operation, not an object abstraction. In addition, if we exploit controller classes, it is true that our other classes are more reusable outside the domain. But why? Because they are typically brain-dead chunks of data with a public interface of accessor methods. This is not object-oriented design; it is simply hiding our data structures behind a wall of gets and sets in one place and putting the behavior of that data elsewhere.
An interesting side issue to this design problem is the realization that the card reader must now know how to get PIN numbers. Is this something card readers do, in general? Of course not. A card reader, in general, reads the information off the back of a magnetically encoded card. We have just made our card reader less reusable by giving it knowledge of the ATM domain. We could put this policy of parsing information outside of the card reader, but this leaves the ATM as the candidate for implementing this parsing. The ATM argues that it is doing too much to warrant it handling the parsing. What is a designer to do about this problem? This is very similar to the problem we had with the ATM and its display screen and keypad. However, in this example, only one piece of the containing class is involved. We can use a wrapper around a physical card reader (the general, reusable component). The wrapper translates the general functionality of the physical card reader into the more ATM-specific requirements (see Figure 11.6). The physical card reader is reusable, and the card reader is domain-specific with the purpose of distributing system intelligence away from the ATM. The result is that everyone is happy!
This solution applies to numerous side arguments that erupt in critiquing this example. Should a display screen have a display_msg() operation, or should it have operations like display_pinprompt(), display_mainmenu(), etc.? Should a keypad have a get_key() operation or operations like get_digit(), get_transactionnumber(), etc.?
|< Free Open Study >|