|< Free Open Study >|
11.9 Other Miscellaneous Issues
There are a number of loose ends to tie up in this design. Looking back on our first object model, we see a number of classes we have not examined. The first, and most interesting, is the Network class. We put it in our object model because it is clearly an interesting noun in our requirement specification. If we consider it a key abstraction, then we will use it when we decide to process a Transaction object, recognizing that the Network is an agent between the ATM object and the Bank object. In this scenario, we end up with the ATM object getting a transaction for the Super-Keypad, sending the Network object a process method with the Transaction object as an explicit argument. The Network object would then send the Bank a process method, again passing the Transaction object as an explicit argument. The Bank would process the Transaction as previously discussed, returning good or bad to the Network object. The Network object would then return this return value of good or bad to the ATM object. Most designers will quickly realize that the network is a useless agent between the ATM and Bank and that it should be eliminated in favor of a direct uses relationship. Later in design, when we discuss the ATM and Bank proxies, we realize that they are heavily dependent on the physical network. Not wanting these proxy classes to be tightly coupled with a particular network, we create a wrapper class called Network to isolate the physical network from the proxies that use it. This Network class is completely different from the first network we proposed. The first network was a candidate for a key abstraction, which we threw out of the system because it was a useless agent. The second network is an implementation class, which is of no interest to logical designers. We should consider implementation classes only during physical design.
Another implementation class is one that will wrap the particular storage of account objects, most likely some database. At high-level design, we consider the account object to live in some AccountList object in memory. In reality, the AccountList is really querying some database to get the necessary information to build an Account object in memory. Since we do not want our model dependent on the physical storage of accounts, we hide this information in some Database object. It is the Database object that contains all implementation-dependent information and translations.
A case study attesting to this type of architecture can be found in the insurance industry. In one application with which I had some contact, a group of designers were creating an insurance claim system on an IBM-compatible 486 machine. The actual claim records were stored in a relational database on a mainframe computer. In the first pass of design, the methods of the classes that made up the object-oriented model on the 486 side of the application were riddled with function calls to collect information on the IBM mainframe side of the application (e.g., network connects, SQL query calls). I asked a couple of the designers what they would do if I told them that the claim records would no longer be stored on a mainframe but would live on the local drive of the 486 machine. They quickly realized that a full system rewrite would be the result. Every class in their domain would be affected by this change. The solution was to create a database class with which the classes in the model could send a message to fetch a claim (given a claim number) and through some magic a claim object would be returned. The magic in their case was to run a network to a mainframe database and collect the necessary information to build a Claim object. If, at some later date, the database was to be stored on a local disk drive of the 486, the development team had a central location for any necessary changes. Their object-oriented model remained oblivious to the physical design change. The physical location of the records became the concern of one class, not the entire model.
What about the Customer class? It is true that the end user is providing external stimuli to the ATM domain, but the ATM domain never uses the customer class. This is the hallmark of a class outside the system. The BankCard and Receipt classes are also considered outside the system. They exist as tangible entities but do not provide useful service to the domain model. The Cash class could also be considered outside the system, but it is slightly different than the other three in that the amount of cash must be kept as a value within the cash dispenser. Cash is a good example of a class that has been reduced to an attribute of some other class, in this case the CashDispenser.
What about Savings and Checking account? Should these two entities be derived classes of Account or is the type of account simply an attribute of the Account class? The answer to this question is, "In this domain, do checking and savings accounts behave differently?" The answer, in this example, appears to be no; therefore Checking and Savings are simply values of a descriptive attribute of the Account class. Let us add a use case to the ATM domain that does distinguish the two types of accounts. Let us state that checking accounts bounce but savings accounts do not. That is, it is possible to send a Checking account object a bounce method, passing the number of days as an explicit argument to the method. This method returns the total amount of money in bounced checks the account has suffered during that time period. The FDIC would like to send the bank a bounce method with a number of days to find out how much money in bounced checks the bank has suffered. How do we implement such a method? The first attempt is to state that the Bank runs down its list of accounts, sending each account the bounce method. The problem here is that the Savings accounts in the list do not know what you mean. In fact, accounts in general do not know how to bounce. What can we do? We need to recognize that this is the "core the apple" problem we discussed in Chapter 5. In that example, we had a FruitBasket (the bank) that contained a list of different Fruit objects (the accounts). Apples (the checking account) had an extra method called core (bounce). The question in that example was, "How do we core the apples?" The solution in the Fruit domain was either to keep the Apples in a separate list within the FruitBasket (requiring case analysis on the type of an object) or to add a core method to the Fruit class, which does nothing by default (requiring the placement of derived class information in the base class). In our example, our choice is to keep a separate list of the Checking accounts within the Bank object or to place a Bounce method, which returns zero dollars on the Account class. We might be tempted by the latter solution because it is easier to implement, but when we consider physical design, we see that accessing an Account object is expensive (since the accounts are stored on a disk). This physical design information will argue that the first solution of keeping a separate list of checking accounts is much faster and often more preferable. For example, there may be 2 million accounts but only 800,000 are checking accounts. Why retrieve 2 million accounts when all we need to look at is 800,000? This example shows the interest in patterns and heuristics. Once you realize which problem you have via a heuristics violation, its related patterns offer you the necessary solutions for free.
And last, but not least, how do we handle the Cancel key? If we isolate Cancel to the period in which we collect information, then the SuperKeypad object can detect that the keystroke was the pressing of the Cancel button and will return Cancel to the ATM object, which in turn performs any necessary clean-up. If we wish to cancel after a transaction has been sent to the Bank, then things get a bit more complex. The ATM cannot block on the Bank proxy when it sends the process method. It simply returns and continues to poll the SuperKeypad for a Cancel return value and the Bank proxy for a done return value. If the SuperKeypad returns Cancel to any of the ATM's queries, then the ATM must inform the Bank proxy that the end user has cancelled the transaction. The Bank will then be notified via its proxy and perform any necessary rollback of the transaction. In all cases, the Cancel option does not add much complexity to the design or its implementation.
|< Free Open Study >|