When a beginning programmer writes a program, there is one goal: the program must work correctly. However, correctness is only a part of what makes a program good. Another, equally important part is that the program be maintainable.
Perhaps you have experienced the frustration of installing a new version of some software, only to discover that its performance has degraded and one of the features you depend on no longer works. Such situations occur when a new feature changes the existing software in ways that other features did not expect.
Good software is intentionally designed so that these unexpected interactions cannot occur. This chapter discusses the characteristics of well-designed software and introduces several rules that facilitate its development.
Designing for Change
Software development typically follows an iterative approach. You create a version, let users try it, and receive change requests to be addressed in the next version. These change requests may include bug fixes, revisions of misunderstandings of how the software should work, and feature enhancements.
There are two common development methodologies. In the waterfall methodology, you begin by creating a design for the program, iteratively revising the design until users are happy. Then you write the entire program, hoping that the first version will be satisfactory. It rarely is. Even if you manage to implement the design perfectly, users will undoubtedly discover new features that they hadn’t realized they wanted.
In the agile methodology, program design and implementation occur in tandem. You start by implementing a bare-bones version of the program. Each subsequent version implements a small number of additional features. The idea is that each version contains “just enough” code to make the chosen subset of features work.
Both methodologies have their own benefits. But regardless of which methodology is used, a program will go through several versions during its development. Waterfall development typically has fewer iterations, but the scope of each version change is unpredictable. Agile development plans for frequent iterations with small, predictable changes.
The bottom line is that programs always change. If a program doesn’t work the way users expect then it will need to be fixed. If a program does work the way users expect then they will want it to be enhanced. It is therefore important to design your programs so that requested changes can be made easily, with minimal modification to the existing code.
Suppose that you need to modify a line of code in a program. You will also need to modify the other lines of code that are impacted by this modification, then the lines that are impacted by those modifications, and so on. As this proliferation increases, the modification becomes more difficult, time-consuming, and error prone. Therefore, your goal should be to design the program such that a change to any part of it will affect only a small portion of the overall code.
This idea can be expressed in the following design principle. Because this principle is the driving force behind nearly all the design techniques in this book, I call it the fundamental design principle .
The Fundamental Principle of Software Design
A program should be designed so that any change to it will affect only a small, predictable portion of the code.
For a simple illustration of the fundamental design principle, consider the concept of variable scope. The scope of a variable is the region of the program where that variable can be legally referenced. In Java, a variable’s scope is determined by where it is declared. If the variable is declared outside of a class then it can be referenced from any of the class’s methods. It is said to have global scope. If the variable is declared within a method then it can be referenced only from within the code block where it is declared, and is said to have local scope.
The ScopeDemo Class
Why should a programmer care about variable scoping? Why not just define all variables globally? The answer comes from the fundamental design principle. Any change to the definition or intended use of a variable could potentially impact each line of code within its scope. Suppose that I decide to modify ScopeDemo so that the variable y in method f has a different name. Because of y’s scope, I know that I only need to look at method f, even though a variable named y is also mentioned in method g. On the other hand, if I decide to rename variable x then I am forced to look at the entire class.
In general, the smaller the scope of a variable, the fewer the lines of code that can be affected by a change. Consequently, the fundamental design principle implies that each variable should have the smallest possible scope.
Object-Oriented Basics
Objects are the fundamental building blocks of Java programs. Each object belongs to a class, which defines the object’s capabilities in terms of its public variables and methods. This section introduces some object-oriented concepts and terminology necessary for the rest of the chapter.
APIs and Dependencies
The public variables and methods of a class are called its Application Program Interface (or API). The designer of a class is expected to document the meaning of each item in its API. Java has the Javadoc tool specifically for this purpose. The Java 9 class library has an extensive collection of Javadoc pages, available at the URL https://docs.oracle.com/javase/9/docs/api . If you want to learn how a class from the Java library works then this is the first place to look.
The StringClient Class
A class’s API is a contract between the class and its clients. The code for StringClient implies that the class String must have a method length that satisfies its documented behavior. However, the StringClient code has no idea of or control over how String computes that length. This is a good thing, because it allows the Java library to change the implementation of the length method as long as the method continues to satisfy the contract.
If X is a client of Y then Y is said to be a dependency of X. The idea is that X depends on Y to not change the behavior of its methods. If the API for class Y does change then the code for X may need to be changed as well.
Modularity
Treating an API as a contract simplifies the way that large programs get written. A large program is organized into multiple classes. Each class is implemented independently of the other classes, under the assumption that each method it calls will eventually be implemented and do what it is expected to do. When all classes are written and debugged, they can be combined to create the final program.
This design strategy has several benefits. Each class will have a limited scope and thus will be easier to program and debug. Moreover, the classes can be written simultaneously by multiple people, resulting in the program getting completed more quickly.
We say that such programs are modular . Modularity is a necessity; good programs are always modular. However, modularity is not enough. There are also important issues related to the design of each class and the connections between the classes. The design rules later in this chapter will address these issues.
Class Diagrams
Class diagrams belong to a standard notational system known as UML (for Universal Modeling Language). UML class diagrams can have many more features than described here. Each variable and method can specify its visibility (such as public or private) and variables can have default values. In addition, the UML notion of dependency is broader and more nuanced. The definition of dependency given here is actually a special kind of UML dependency called an association . Although these additional modeling features enable UML class diagrams to more accurately specify a design, they add a complexity that will not be needed in this book and will be ignored.
Class diagrams have different uses during the different phases of a program’s development. During the implementation phase, a class diagram documents the variables and methods used in the implementation of each class. It is most useful when it is as detailed as possible, showing all the public and private variables and methods of each class.
During the design phase, a class diagram is a communication tool. Designers use class diagrams to quickly convey the functionality of each class and its role in the overall architecture of the program. Irrelevant classes, variables, methods and arrows may be omitted in order to highlight a critical design decision. Typically, only public variables and methods are placed in these class diagrams. Figure 1-1 is an example of a design-level class diagram: the private variable of type StringClient is omitted, as are the unreferenced methods in String. Given that this book is about design, it uses design-level class diagrams exclusively. Most of the classes we model will have no public variables, which means that the middle section of each class rectangle will usually be empty.
Static vs. Nonstatic
A static variable is a variable that “belongs” to a class. It is shared among all objects of the class. If one object changes the value of a static variable then all objects see that change. On the other hand, a nonstatic variable “belongs” to an object of the class. Each object has its own instance of the variable, whose value is assigned independently of the other instances.
The StaticTest Class
Methods can also be static or nonstatic. A static method (such as getX in StaticTest) is not associated with an object. A client can call a static method by using the class name as a prefix. Alternatively, it can call a static method the conventional way, prefixed by a variable of that class.
Because a static method has no associated object, it is not allowed to reference nonstatic variables. For example, the print method in StaticTest would not make sense as a static method because there is no unique variable y that it would be able to reference.
A Banking Demo
Listing 1-4 gives the code for a simple program to manage a fictional bank. This program will be used as a running example throughout the book. The code in Listing 1-4 consists of a single class, named BankProgram, and is version 1 of the demo.
Version 1 of the Banking Demo
The program’s run method performs a loop that repeatedly reads commands from the console and executes them. There are seven commands, each of which has a corresponding method.
The Single Responsibility Rule
The BankProgram code is correct. But is it any good? Note that the program has multiple areas of responsibility—for example, one responsibility is to handle I/O processing and another responsibility is to manage account information—and both responsibilities are handled by a single class.
Multipurpose classes violate the fundamental design principle. The issue is that each area of responsibility will have different reasons for changing. If these responsibilities are implemented by a single class then the entire class will have to be modified whenever a change occurs to one aspect of it. On the other hand, if each responsibility is assigned to a different class then fewer parts of the program need be modified when a change occurs.
This observation leads to a design rule known as the Single Responsibility rule.
The Single Responsibility Rule
A class should have a single purpose, and all its methods should be related to that purpose.
A program that satisfies the Single Responsibility rule will be organized into classes, with each class having its own unique responsibility.
The Version 2 Bank Class
Similarly, the deposit method is not responsible for asking the user for the deposit amount. Instead, it expects the caller of the method (i.e., BankClient) to pass the amount as an argument.
The authorizeLoan method eliminates both input and output code from the corresponding version 1 method. It expects the loan amount to be passed in as an argument and it returns the decision as a boolean.
The getBalance method corresponds to the select method of version 1. That method is primarily concerned with choosing a current account, which is the responsibility of BankClient. Its only bank-specific code involves obtaining the balance of the selected account. The Bank class therefore has a getBalance method for select to call.
The showAll method in version 1 prints the information of each account. The bank-specific portion of this method is to collect this information into a string, which is the responsibility of Bank’s toString method.
The addInterest method in version 1 has no input/output component whatsoever. Consequently, it is identical to the corresponding method in Bank.
The Version 2 BankClient Class
The Version 2 BankProgram Class
Note that version 2 of the banking demo is more easily modifiable than version 1. It is now possible to change the implementation of Bank without worrying about breaking the code for BankClient. Similarly, it is also possible to change the way that BankClient does its input/output, without affecting Bank or BankProgram.
Refactoring
One interesting feature of the version 2 demo is that it contains nearly the same code as in version 1. In fact, when I wrote version 2 I began by redistributing the existing code between its three classes. This is an example of what is called refactoring .
In general, to refactor a program means to make syntactic changes to it without changing the way it works. Examples of refactoring include: renaming a class, method, or variable; changing the implementation of a variable from one data type to another; and splitting a class into two. If you use the Eclipse IDE then you will notice that it has a Refactor menu, which can automatically perform some of the simpler forms of refactoring for you.
Unit Testing
Earlier in this chapter I stated that one of the advantages to a modular program is that each class can be implemented and tested separately. This begs the question: How can you test a class separate from the rest of the program?
The answer is to write a driver program for each class. The driver program calls the various methods of the class, passing them sample input and checking that the return values are correct. The idea is that the driver should test all possible ways that the methods can be used. Each way is called a use case .
The BankTest Class
Testing the BankClient class is more difficult, for two reasons. The first is that the class calls a method from another class (namely, Bank). The second is that the class reads input from the console. Let’s address each issue in turn.
How can you test a class that calls methods from another class? If that other class is also in development then the driver program will not able to make use of it. In general, a driver program should not use another class unless that class is known to be completely correct; otherwise if the test fails, you don’t know which class caused the problem.
A Mock Implementation of Bank
The best way to test a class that takes input from the console is to redirect its input to come from a file. By placing a comprehensive set of input values into a file, you can easily rerun the driver program with the assurance that the input will be the same each time. You can specify this redirection in several ways, depending on how you execute the program. From Eclipse, for example, you specify redirection in the program’s Run Configurations menu.
The class BankProgram makes a pretty good driver program for BankClient. You simply need to create an input file that tests the various commands sufficiently.
Class Design
A program that satisfies the Single Responsibility rule will have a class for each identified responsibility. But how do you know if you have identified all the responsibilities?
The short answer is that you don’t. Sometimes, what seems to be a single responsibility can be broken down further. The need for a separate class may become apparent only when additional requirements are added to the program.
For example, consider version 2 of the banking demo. The class Bank stores its account information in a map, where the map’s key holds the account numbers and its value holds the associated balances. Suppose now that the bank also wants to store additional information for each account. In particular, assume that the bank wants to know whether the owner of each account is foreign or domestic. How should the program change?
The Version 3 BankAccount Class
The Version 3 Bank Class
As a result of these changes, obtaining information from an account has become a two-step process: A method first retrieves a BankAccount object from the map; it then calls the desired method on that object. Another difference is that the methods toString and addInterest no longer get each account value individually from the map keys. They instead use the map’s values method to retrieve the accounts into a list, which can then be examined.
The Version 3 BankClient Class
This relatively minor change to BankClient points out the advantage of modularity. Even though the Bank class changed how it implemented its methods, its contract with BankClient did not change. The only change resulted from the added functionality.
Encapsulation
An Alternative BankAccount Class
Although this alternative BankAccount class is far more compact, its design is far less desirable. Here are three reasons to prefer methods over public variables.
The first reason is that methods are able to limit the power of clients. A public variable is equivalent to both an accessor and a mutator method, and having both methods can often be inappropriate. For example, clients of the alternative BankAccount class would have the power to change the account number, which is not a good idea.
The second reason is that methods provide more flexibility than variables. Suppose that at some point after deploying the program, the bank detects the following problem: The interest it adds to the accounts each month is calculated to a fraction of a penny, but that fractional amount winds up getting deleted from the accounts because the balance is stored in an integer variable.
Note that getBalance no longer returns the actual balance of the account. Instead, it returns the amount of money that can be withdrawn from the account, which is consistent with the earlier API. Since the API of BankAccount has not changed, the clients of the class are not aware of the change to the implementation.
The third reason to prefer methods over public variables is that methods can perform additional actions. For example, perhaps the bank wants to log each change to an account’s balance. If BankAccount is implemented using methods, then its setBalance method can be modified so that it writes to a log file. If the balance can be accessed via a public variable then no logging is possible.
The desirability of using public methods instead of public variables is an example of a design rule known as the rule of Encapsulation.
The Rule of Encapsulation
A class’s implementation details should be hidden from its clients as much as possible.
In other words, the less that clients are aware of the implementation of a class, the more easily that class can change without affecting its clients.
Redistributing Responsibility
The classes of the version 3 banking demo are modular and encapsulated. Nevertheless, there is something unsatisfactory about the design of their methods. In particular, the BankAccount methods don’t do anything interesting. All the work occurs in Bank.
For example, consider the action of depositing money in an account. The bank’s deposit method controls the processing. The BankAccount object manages the getting and setting of the bank balance, but it does so under the strict supervision of the Bank object.
In this version, Bank no longer knows how to do deposits. Instead, it delegates the work to the appropriate BankAccount object.
Which version is a better design? The BankAccount object is a more natural place to handle deposits because it holds the account balance. Instead of having the Bank object tell the BankAccount object what to do, it is better to just let the BankAccount object do the work itself. We express this idea as the following design rule, called the Most Qualified Class rule.
The Most Qualified Class Rule
Work should be assigned to the class that knows best how to do it.
The BankAccount class now has methods that correspond to the deposit, toString, and addInterest methods of Bank. The class also has the method hasEnoughCollateral, which (as we shall see) corresponds to Bank’s authorizeLoan method . In addition, the class no longer needs the setBalance method.
The Version 4 Bank Class
As previously discussed, the bank’s deposit method is no longer responsible for updating the account balance. Instead, the method calls the corresponding method in BankAccount to perform the update.
The bank’s toString method is responsible for creating a string representation of all bank accounts. However, it is no longer responsible for formatting each individual account; instead, it calls the toString method of each account when needed. The bank’s addInterest method is similar. The method calls the addInterest method of each account, allowing each account to update its own balance.
The bank’s authorizeLoan method is implemented slightly differently from the others. It calls the bank account’s hasEnoughCollateral method, passing in the loan amount. The idea is that the decision to authorize a loan should be shared between the Bank and BankAccount classes. The bank account is responsible for comparing the loan amount against its balance. The bank then uses that information as one of its criteria for deciding whether to authorize the loan. In the version 4 code, the collateral information is the only criterion, but in real life the bank would also use criteria such as credit score, employment history, and so on, all of which reside outside of BankAccount. The BankAccount class is responsible only for the “has enough collateral” criterion because that is what it is most qualified to assess.
The Version 4 BankAccount Class
Dependency Injection
When the class creates its Scanner object it uses System.in as the source, indicating that input should come from the console. But why choose System.in? There are other options. The class could read its input from a file instead of the console or it could get its input from somewhere over the Internet. Given that the rest of the BankClient code does not care what input its scanner is connected to, restricting its use to System.in is unnecessary and reduces the flexibility of the class.
A similar argument could be made for the bank variable. Suppose that the program gets modified so that it can access multiple banks. The BankClient code does not care which bank it accesses, so how does it decide which bank to use?
The point is that BankClient is not especially qualified to make these decisions and therefore should not be responsible for them. Instead, some other, more qualified class should make the decisions and pass the resulting object references to BankClient. This technique is called dependency injection .
The Version 4 BankClient Class
The Version 4 Bank Class
The Version 4 BankProgram Class
It is interesting to compare versions 3 and 4 of the demo in terms of when objects get created. In version 3 the BankClient object gets created first, followed by its Scanner and Bank objects. The Bank object then creates the account map. In version 4 the objects are created in the reverse order: first the map, then the bank, the scanner, and finally the client. This phenomenon is known as dependency inversion— each object is created before the object that depends on it.
Note how BankProgram makes all the decisions about the initial state of the program. Such a class is known as a configuration class . A configuration class enables users to reconfigure the behavior of the program by simply modifying the code for that class.
The idea of placing all dependency decisions within a single class is powerful and convenient. In fact, many large programs take this idea one step further. They place all configuration details (i.e., information about the input stream, the name of the stored data file, etc.) into a configuration file. The configuration class reads that file and uses it to create the appropriate objects.
The advantage to using a configuration file is that the configuration code never needs to change. Only the configuration file changes. This feature is especially important when the program is being configured by end users who may not know how to program. They modify the configuration file and the program performs the appropriate configurations.
Mediation
The BankClient class in the version 4 banking demo does not know about BankAccount objects . It interacts with accounts solely through methods of the Bank class. The Bank class is called a mediator .
Mediation can enhance the modularity of a program. If the Bank class is the only class that can access BankAccount objects then BankAccount is essentially private to Bank. This feature was important when the version 3 BankAccount class was modified to produce version 4; it ensured that the only other class that needed to be modified was Bank. This desirability leads to the following rule, called the rule of Low Coupling.
The Rule of Low Coupling
Try to minimize the number of class dependencies.
This rule is often expressed less formally as “Don’t talk to strangers.” The idea is that if a concept is strange to a client, or difficult to understand, it is better to mediate access to it.
Design Tradeoffs
The Low Coupling and Single Responsibility rules often conflict with each another. Mediation is a common way to provide low coupling. But a mediator class tends to accumulate methods that are not central to its purpose, which can violate the Single Responsibility rule.
The banking demo provides an example of this conflict. The Bank class has methods getBalance, deposit, and setForeign, even though those methods are the responsibility of BankAccount. But Bank needs to have those methods because it is mediating between BankClient and BankAccount.
Would this new design be an improvement over the version 4 banking demo? Neither design is obviously better than the other. Each involves different tradeoffs: Version 4 has lower coupling, whereas the new design has simpler APIs that satisfy the Single Responsibility rule better. For the purposes of this book, I chose to go with version 4 because I felt that it was important for Bank to be able to mediate access to the accounts.
The point is that design rules are only guidelines. Tradeoffs are almost always necessary in any significant program. The best design will probably violate at least one rule somehow. The role of a designer is to recognize the possible designs for a given program and accurately analyze their tradeoffs.
The Design of Java Maps
As a real-life example of some design tradeoffs, consider the Map classes in the Java library. The typical way to implement a map is to store each key-value pair as a node. The nodes are then inserted into a hash table (for a HashMap object) or a search tree (for a TreeMap object). In Java, these nodes have the type Map.Entry.
Typical Uses of a HashMap
Unfortunately, this mediation can lead to inefficient code. The loop in Listing 1-19 is such an example. The keySet method traverses the entire data structure to acquire all the keys. The get method then has to repeatedly access the data structure again to get the value of each key.
Accessing Map Entries Directly
Making the Map.Entry nodes visible to clients increases the complexity of programs that use maps. Clients need to know about two classes instead of just one. Moreover, the API of Map.Entry now cannot be changed without impacting the clients of HashMap. On the other hand, the complexity also makes it possible to write more efficient code.
The designers of HashMap had to take these conflicting needs into consideration. Their solution was to keep the complexity for people who need it but to make it possible to ignore the complex methods if desired.
Summary
The Single Responsibility rule states that a class should have a single purpose, and its methods should all be related to that purpose.
The Encapsulation rule states that a class’s implementation details should be hidden from its clients as much as possible.
The Most Qualified Class rule states that work should be assigned to the class that knows best how to do it.
The Low Coupling rule states that the number of class dependencies should be minimized.
These rules are guidelines only. They suggest reasonable design decisions for most situations. As you design your programs, you must always understand the tradeoffs involved with following (or not following) a particular rule.