How to Use Objects: Code and Concepts (2016)
Part IV: Responsibility-Driven Design
Chapter 11. Responsibility-Driven Design
Design can be a rather elusive subject. Very often, one meets people who claim that some design is “elegant” or “crap,” as the case may be, and who seek to substantiate that claim with rather abstract concepts such as “loose coupling” or the need for “shotgun surgery,” respectively. It is not uncommon to have heated debates about, and almost religious beliefs in, certain design approaches.
From a more practical, down-to-earth point of view, design can be understood just as structuring the solution to a problem. From this perspective, we all design every day because we simply cannot avoid it. Before we start typing out any code, we make a plan, however briefly, about what we are going to type. For common problems, we reexecute time-proven plans from our existing repertoire. For larger problems that we have not solved previously, we may stand back and discuss alternative strategies, either by ourselves or within our team. For really long-term goals, we may take the time to explore existing systems and solutions.
Design is necessary because the solution to any practical problem is usually so complex that we, as human beings, cannot cope with all of its details at once. We have to summarize and abstract over some aspects to focus on and think clearly about others. The design helps us to place the current aspect into the larger picture. It keeps the overall software in shape while we work on a specific class, method, or internal data structure.
But design does not stop at creating just any solution. Usually, we look for a solution that is particularly “good” in some sense. It should be as simple as possible to reduce the implementation effort. At the same time, it may need to allow for easy extensions by new functionality or it may have to be reusable in several products. Necessarily, such extra demands also cost extra implementation effort, and this is where the disputes begin: Which design will offer the best cost/benefit ratio? Which one will be advantageous in the long run? Since such questions can be answered only with hindsight, we often have to rely on experience. Unfortunately, humans tend to prefer their own experience to that of others—quite often, squabbles about design are just another case of the not-invented-here syndrome.
To create and discuss designs, we have to create a suitable language for the task. Saying that objects contain data and the associated operations is certainly correct, but it remains close to the implementation. As a result, it becomes very hard to forget one object’s details to focus on another object. There are many approaches to object-oriented design and they all come with their own terminology—the language that we choose tends to shape the way we think about a given problem.
32,264,55,263,262 For this book, we have chosen responsibility-driven design, because it has been very influential to the point where its terminology is now found throughout the literature on software engineering. The presentation is split into two chapters. This chapter gives an overview of designing with responsibilities. After introducing the core concepts in Section 11.1, we look more closely at the central strategy of assigning one main responsibility to each object in Section 11.2. To get more details, we then create a self-contained example application, a plotter for function graphs, in Section 11.3. Having mastered the technicalities, we finish by looking at a few indispensable strategies that help to find “good” designs in Section 11.5. While this chapter keeps at the level of individual objects and small groups of objects, the next chapter will look at building blocks at the architectural level and at more abstract design strategies.
11.1 The Core: Networks of Collaborating Objects
We started our investigation of object-oriented software development with 1.1 the central assumption that objects are small and cheap entities, so that we can have as many as we like to solve a given problem. Having explored and mastered the technical details of the language, design-by-contract, and event-driven software, we are now in the position to ask: Which objects do I create to solve a given task? Which methods do they have?
Of course, it is very hard to come up with general recipes: Software development is and probably will always be a creative activity that involves experience, decisions, and sometimes even taste (although the last facet is grossly overrated—often it is merely a way of referring to one’s accumulated experience without being able to pinpoint specific incidents or examples). 263,261 The literature gives many good guidelines on finding objects. They are most helpful when one already has some previous design experience. To gain this experience quickly, we will look at the design of Eclipse’s New Class 100 wizard: To become a good designer, there is nothing like imitating experienced professionals.
The presentation in this section proceeds in three steps. First, we look at the objects and their behavior in a concrete usage scenario to understand the overall approach. Second, we analyze the principles underlying the design. Finally, we examine a few general guidelines about the design process itself.
Software is a network of collaborating objects.
Let us plunge into the topic of design with an everyday example. Eclipse’s New Wizard in Fig. 11.1(a) is reachable through the New/Other . . . entries of the Package Explorer’s context menu. (It is shown with indications of the internal structure for later reference.) The user can create virtually any kind of relevant file, complete with a sensible initial content. We will consider the example of creating a new Java class. When we select the Class entry in Fig. 11.1(a), the content of the dialog changes to the well-known class creation wizard in Fig. 11.1(b). The use case is simple enough, but how does the New Wizard accomplish the task?
Figure 11.1 The New Wizard
When we look at class NewWizard, we find that it has something like 170 lines of code, including whitespace and comments. So all the complex functionality must reside somewhere else, and the NewWizard is only one piece in a larger design that provides the required functionality.
Digging through the different classes and objects involved reveals the structure shown in Fig. 11.2. Admittedly, it seems a bit complex, but the nine objects between them must accomplish the overall task of creating a new Java class through the dialog in Fig. 11.1. We will say that the objects collaborate toward that common goal. Collaboration usually consists 264,32,109 of simple method calls. To enable these calls, each object holds references to relevant other objects, so that the objects form the network (or graph structure) shown in Fig. 11.2. The idea of a network here also encompasses temporary references passed as parameters or stored in local variables.
Figure 11.2 Structure of the New Wizard
We will now explain Fig. 11.2 briefly and will also link the explanation to the sources. The subsequent analysis of the underlying principles will justify the perceived complexity. We encourage you to refer back to Fig. 11.2 throughout the explanation, to validate the connections in detail.
The design must be traceable to the implementation.
It is a general principle that a good design is truly valuable only if it is 11.5.4 also reflected in the code. After all, if the design does not explain how the code works, why bother with design in the first place? The term traceability 7,24 covers such relationships between different artifacts in software engineering processes.
Design creates mechanisms in which each object has its place.
To explain the overall network, it is best to follow the steps of the New Wizard chronologically, along the user experience. At the top of Fig. 11.2, the 9.3.4 NewWizardAction can be placed in the menu or the toolbar. When clicked, it creates a generic WizardDialog, which is built to contain any kind of IWizard. Leaving out the irrelevant code, the action’s run() method is this:
org.eclipse.ui.actions.NewWizardAction
public void run() {
NewWizard wizard = new NewWizard();
Shell parent = workbenchWindow.getShell();
WizardDialog dialog = new WizardDialog(parent, wizard);
dialog.open();
}
9.3The framework of generic wizards is provided in the JFace layer. A JFace Wizard object is basically a container for pages and manages the switching between these pages. It also contains the code that gets executed once the user clicks the Finish button, in a methodperformFinish(). Each page can check whether the current entries are admissible and complete; also, each page is contained in one wizard, and it can dynamically compute the next page. The interface IWizardPage captures just this design:
org.eclipse.jface.wizard.IWizardPage
public interface IWizardPage extends IDialogPage {
public boolean canFlipToNextPage();
public boolean isPageComplete();
public IWizard getWizard();
public IWizardPage getNextPage();
. . . simple properties for name and previous page
}
3.2.7The interface IWizard abstracts over the Wizard class to to decouple subsystems. Almost all concrete wizards in the Eclipse platform derive from Wizard.
The NewWizard is one particular wizard and extends Wizard. It organizes the overall process, as seen in its addPages() method in the next code snippet. First, it requests all available kinds of new files. Internally, these are termed—unfortunately—“new wizards,” because technically they are, again, wizards. In the current context, they are represented by objects implementing 1.3.8 IWizardDescriptor, which are held in a singleton NewWizard Registry 12.3.3.2 (which in turn collects them as extensions to a special extension point in the Eclipse IDE). The NewWizard contains a single page New WizardSelectionPage, which displays the available wizard types, as seen in Fig. 11.1.
org.eclipse.ui.internal.dialogs.NewWizard
public void addPages() {
IWizardRegistry registry =
WorkbenchPlugin.getDefault().getNewWizardRegistry();
IWizardCategory root = registry.getRootCategory();
IWizardDescriptor[] primary = registry.getPrimaryWizards();
. . .
mainPage = new NewWizardSelectionPage(workbench,
selection, root,
primary, projectsOnly);
addPage(mainPage);
}
The NewWizardSelectionPage delegates the actual display to a helper class NewWizardNewPage. It creates that helper when it sets up its display 7.1 in the createControl() method:
org.eclipse.ui.internal.dialogs.NewWizardSelectionPage
public void createControl(Composite parent) {
newResourcePage = new NewWizardNewPage(this, wizardCategories,
primaryWizards, projectsOnly);
Control control = newResourcePage.createControl(parent);
. . .
}
The crucial interaction occurs when the user selects a particular wizard from the list and clicks the Next button. At this point, the NewWizard asks the IWizardDescriptor to create a wizard—in the current scenario a NewClassCreationWizard, which has a singleNewClassWizardPage (see Fig. 11.2). All of this happens behind the scenes of the NewWizard Registry. Since that class is all about the larger goal of extensibility, we postpone its treatment and get on with the work: All that matters now is 12.3.3.3 that we have aNewClassCreationWizard.
The link between the NewWizardSelectionPage and the selected follow-up wizard happens through the mechanisms inside WizardDialog. As seen in the interface IWizardPage, the dialog will ask each page whether it is OK to switch to the next page and what that page will actually be. The wizard selection page employs this mechanism to compute the desired page dynamically, from the selected follow-up wizard.
org.eclipse.jface.wizard.WizardSelectionPage
public boolean canFlipToNextPage() {
return selectedNode != null;
}
org.eclipse.jface.wizard.WizardSelectionPage
public IWizardPage getNextPage() {
IWizard wizard = selectedNode.getWizard();
if (!isCreated) {
// Allow the wizard to create its pages
}
return wizard.getStartingPage();
}
1.4.1Both of these methods are inherited from a generic WizardSelectionPage, but this is irrelevant at this point.
We now quickly summarize what happens when the user clicks the Finish button, leaving out the code for brevity. First, the wizard dialog invokes performFinish() on its current wizard. That wizard may be either a New Wizard or a NewClassCreationWizard, depending on whether the user has already clicked Next. The first case merely delegates to the second, so the call ends up with the NewClassCreationWizard, which through several indirections invokes the following method. There, finally, fPage is a New ClassWizardPage (see Fig. 11.2), which performs the actual creation of the source file.
org.eclipse.jdt.internal.ui.wizards.NewClassCreationWizard
protected void finishPage(IProgressMonitor monitor)
throws InterruptedException, CoreException {
fPage.createType(monitor);
}
The alert and suspicious reader will note that overall setup for creating a class source 9.1 file actually breaks model-view separation: The code for creating the Java class is contained in the user interface plugin org.eclipse.jdt.ui. Since the class creation code 12.1 is tightly linked to the options available on the wizard page, it would seem sensible to keep both together: If the options change, the code can easily change as well. The situation would be different if the Eclipse JDT included a full-fledged code generation component. That component would certainly have to be model-level, and then class creation would merely pick the right tools from the general functionality.
We have reached the end of this first part, in which we followed the workings of the class creation wizard once through the user experience. It has certainly proved to be a rather complex mechanism. Subsequently, we will see why the complexity is necessary and which general principles explain the chosen design.
Each object is a busy and efficient clerk in a larger machinery.
Even though the objects in Fig. 11.2 together accomplish the larger task of creating a new source class, the individual objects are not aware of this fact and do not have to be aware of it: Each one simply does its own job. 264,32 It is like a clerk who has a certain set of responsibilities. The overall “administration” is set up such that the machinery runs smoothly and all daily tasks are accomplished as long as each clerk fulfills its own responsibilities, or performs its own duties efficiently.
For instance, the NewClassWizardPage is responsible for creating a new source class, by first querying the user for the required parameters and then creating the source file. It does not have to be aware of the context of bringing up the pop-up dialog, selecting a file type, and so on. Indeed, it is this focus on its own responsibilities that allows the class to be reused as 12.4 part of several other wizards. Likewise, the WizardDialog is responsible for managing pages and enabling the user to switch between them. It does not care about the content of the pages, nor is it concerned with validating the data the user enters: Those aspects are the responsibility of single pages.
The idea of “clerks” and “responsibilities” may at first sound a bit too abstract to be helpful. But it is really just a metaphor: By saying that “an object is like a busy and efficient person,” we can transfer our understanding of busy and efficient persons to the behavior of objects. If we can imagine how a person would handle a task, we can better imagine how an intangible object would approach it. Metaphors are, in fact, very common in software engineering. For example, design-by-contract transfers 4.1 our understanding of legal contracts to method calls. A level further down, we perceive method calls and code execution as a “behavior” triggered by “messages” to an object. In 1.4.1 the area of network programming, a “socket” is a point for attaching network connections. The term “connection,” in turn, visualizes the perceived result of sending and receiving sequences of data packets using the TCP protocol. In short, we use metaphors to link the unknown, invisible software world to our everyday experience, thereby transferring our understanding of one to the other.
Some objects perform managerial duties.
We have used the term “clerk” to invoke the image of a diligent, efficient worker that goes about its daily tasks in a predictable manner. However, the tasks themselves need not always be humble or basically stupid. In fact, many objects display a great deal of “intelligence” in their implemented logic. Also, many objects are more like managers, although their behavior is still determined by strict rules. For instance, the NewWizard arranges for things to happen, but it does not do any of these things itself. Such objects will usually make decisions on behalf of and about other objects. 1.8.1
Roles are sets of responsibilities taken on by different objects.
Once we start thinking in terms of responsibilities, we quickly find that different objects can take on the same set of responsibilities in different situations. In the example, the WizardDialog can contain many kinds of wizards, but all of those wizards must fulfill the responsibilities defined by the interfaces IWizard and IWizardPage: The wizard must create and manage a set of pages; each page must perform validation and must compute the next page, and so on.
A role is a set of related responsibilities that can be fulfilled by different 263,211 objects. At the code level, roles usually map to interfaces or abstract base 3.2.10 classes, where the abstract methods capture the expected behavior of the objects, to be filled in later by the concrete classes. For the purposes of design, it is, however, better to postpone the definition of the interface and to focus on the conceptual responsibilities instead. That is, before deciding how a given service will be accessed, one has be very clear about what the service should actually be. Introducing too many implementation details too early tends to lead to premature restrictions of the design.
A role can serve as an abstraction over a number of special cases, such as for the wizards described earlier. The intuition is shown in Fig. 11.3(a), where the concrete collaborator of an object can be exchanged without the 12.3 object noticing. Roles then lead to extensibility, because new objects fitting 12.4 the role can be introduced at any time. They can also lead to reusability, because an object accessing a collaborator defined by a role can work with different collaborators in different scenarios.
Figure 11.3 Illustration of Roles
211 A second intuition of roles is that they create slices of an object’s behavior, as shown in Fig. 11.3(b). Usually a role does not prescribe the entire behavior of an object, but rather captures some aspect that is relevant in the current context. For instance, the NewClassWizardPageof the example contains all the logic for creating a new source file, yet the WizardDialog will be interested only in its ability as an IWizardPage: When the user 3.2.2 clicks the Next button, the dialog requests the next page. This view on roles links back to the idea of client-specific interfaces, which carve out a particular aspect of an object’s behavior for special usage scenarios.
org.eclipse.jface.wizard.WizardDialog
protected void nextPressed() {
IWizardPage page = currentPage.getNextPage();
if (page == null) { return; }
showPage(page);
}
Fig.11.3 might remind you of the illustration of the Adapter pattern in Fig. 2.10. In both cases, an object accesses a collaborator through a predefined interface. However, in the case of roles it is usually the object itself, rather than some artificial adapter, that provides the required methods. Also, the definition of the role usually predates that of the object. The link between the two situations is this: An adapter becomes necessary if an object fits the conceptual description of a role’s responsibilities, but not its technical realization as an interface definition.
Design decisions must be based on concrete goals.
Before we examine the process of designing, let us reevaluate the setup in Fig. 11.2. When we started, it certainly seemed unnecessarily complex: Why not create a NewClassCreationDialog, in a single class, and be done with it? We could even use the WindowBuilder to assemble this dialog in a few 7.2 minutes! Why bother to invent so many objects and so many intermediate steps that it becomes hard to see how the overall task is accomplished?
The reason is very simple: The particular chosen distribution of responsibilities between objects attains several goals, which together make the Eclipse code base as a whole less complex, more maintainable, and more extensible.
• As a pervasive goal, the individual classes remain small because each 11.2 object deals with only one main task. As a result, the maintenance developer has to understand less code when trying to debug or modify some functionality.
• At the same time, the objects have disjoint responsibilities, so that the 11.5.2 maintenance team knows where to look if some aspect of the behavior needs fixing.
• The WizardDialog is a generic piece of infrastructure, which can be 12.4.2 reused in many places. A special-purpose dialog assembled with the WindowBuilder could be used only once.
• The NewWizardRegistry takes care of gathering contributed 12.3.3.3 wizards for quite diverse file formats and other artifacts, such as example projects, from throughout the Eclipse platform. New plugins can offer new types of files by simply implementing the interfaceINewWizard and registering the implementation with extension point 12.3.3 org.eclipse.ui.newWizards.
Properties such as extensibility and reusability characterize aspects of a software’s internal quality that are quite independent of the quality perceived by the users. Nonfunctional properties (or nonfunctional requirements) capture precisely those aspects 24 5.4.6 of a system that are not covered by the use cases, which describe the system’s reactions to external stimuli. Other nonfunctional properties include the perceived speed of the answers or the stability of the system. Many of these properties cannot be addressed by 24,59,218 software design alone, but fall into the realm of software architecture.
Design must always be driven by such concrete demands; it is never an aim in itself. If someone tells you that his or her approach is simply “more elegant” or “cuter,” be deeply suspicious: Chances are that the person is trying to sell you an overly complex solution that does not yield benefits in return for the greater implementation effort. Another trap is to make 12.4 objects “reusable” by introducing “general” mechanisms right from the start, even if no reuse is planned for the foreseeable future and no one knows as yet the details of a possible reuse scenario. In all such situations, simply insist on evaluating the concrete mid-term savings of the approach. A good guideline is the “rule of three”: You should have three concrete applications before setting out to define an abstraction. If the answer to these questions 1.2.2 is insufficient, placate the team by pointing out that refactoring can always 92 be done later to implement their ideas. Avoid speculative generality to stay productive.
Unfortunately, the human mind—and in particular the developer’s mind—likes complex and creative solutions: When confronted with a simple everyday problem that looks rather dull, why not spice it up with a bit of intricate design? Try to avoid this temptation as far as humanly possible.
Objects know information, perform tasks, and make decisions.
We have introduced the approach of responsibility-driven design and have explored the need for explicit designs that also anticipate later reuse and changes. How, then, does one create such designs?
1.8.1We have already examined the basic contributions of objects to a system briefly from a technical perspective. Let us now review them in the light 263 of responsibilities. In general, the responsibilities of an object can be split into three categories:
• An object may have to know information.
• It may have to perform tasks, mostly for others.
• It may have to make decisions affecting other objects.
Many objects have responsibilities from all three categories, although the last one is perhaps less frequent: Usually an object’s decisions concern its own duties and its own internals. At the other end, objects that only hold 92 information are very rare and should be avoided, since operations on the 1.1 4.1 information are more challenging to code and cannot be encapsulated.
In the example, the NewWizardRegistry holds information about available file types, but it also gathers information from the various extensions and wraps it into convenient IWizardDescriptors. The NewClassWizard Page 2.4 holds the information about the new class’s characteristics, allows the user to edit that information, and creates the source file. The Wizard dialog holds no “information” relevant to the user, but it decides when data must be validated and when pages are flipped. It does so on behalf of the contained wizard and its pages, which are relieved from making these decisions and can concentrate on their own contributions.
Having such categories helps in describing objects. For each object, ask yourself what it has to know, what it contributes to the system’s functionality, and in which way it is a manager making decisions of more strategic importance.
Going one step further, objects often have mostly responsibilities from 261 only one category, which leads to the concept of role stereotypes: information holders, service providers, controllers, structurers, interfacers, and coordinators. Their descriptions have been given earlier. 1.8.1
Identify responsibilities and assign them to individual objects.
Responsibility-driven design then works from the tasks to be solved. In the end, the application has to implement some desired functionality. That functionality is often given by use cases, which describe possible interactions 147 47 of the users with the system: The users stimulate the system by their input and expect some observable behavior in return. As a result, the software 7.1 10.1 becomes event-driven, which ties in with the view of objects responding to 1.4.1 messages.
Designing then means breaking the functionality down into responsibilities—first into broad, comprehensive responsibilities that we assign to the system’s modules, and then into small-grained ones describing individual steps or aspects of the overall solution. Those responsibilities get assigned to individual objects.
Invent objects to take on responsibilities where necessary.
In the beginning, there are, of course, no objects that could take on the responsibilities. During the design process, newly discovered responsibilities may not fit in with any existing object. Inventing objects is therefore a central activity in design. The basic idea is this:
If something needs doing, somebody has to do it.
Compare this to the real world: If the kitchenette in the office needs regular cleaning, then someone must be responsible for it. There must be a well-established mechanism that triggers the cleaning, because otherwise the room quickly starts looking quite messy.
Let us try our hand at the example, by designing a File Creation Wizard from scratch. This wizard must show a list of file types, as well as Next and Previous buttons, and must gather the correct extra information required for a specific file type. Fig. 11.4 shows a first attempt. The File CreationWizard manages the overall process. It uses a helper FileType List for the first step, because that is a self-contained task. Since the entry of the extra information depends on the file type, we create a role ExtraInformationPage (the italics in the figure indicate that it is a role). Finally, 9.1 knowing the benefits of model-view separation, we invent a role Initial ContentCreator, again with one kind of creator for each file type. The FileCreationWizard is responsible for transferring the entered extra information from the view-levelExtraInformationPage to the model-level InitialContentCreator.
Figure 11.4 First Attempt at a FileCreationWizard
Of course, this design is less elaborate than Fig. 11.2, but then we did not aim for reuse of the wizard dialog. Even so, we managed to observe 9.2.2 model-view separation, so that the content creator can be tested on its own, without clicking through the wizard.
To sharpen the perception of what is specifically “object-oriented,” let us stand back for a minute. All programming paradigms, such as object-oriented, procedural, or purely functional approaches, provide some way of subdividing the implementation of functionality. The crucial difference between the paradigms arises from the shape of subtasks that single implementation units can take on. In procedural programming, 1.8.4 subtasks are algorithmic steps toward a solution. In purely functional programming, they are transformations of (mostly tree-shaped) values. In object-oriented programming, they are reactive entities that communicate via messages. To fully exploit objects, it is therefore best to postpone concerns about algorithmics and data structures during design: While it is necessary that the design can be implemented, and efficiently too, the focus must be on a sensible task assignment that can be understood on its own, without looking at any code.
Do not assign the same responsibility to different objects.
The metaphor of “responsibilities” has a second important aspect: If a person is responsible for a given task, then the individual fulfills that task completely and reliably, and does not share that task with other people. Likewise, an object should be made the sole authority for its responsibilities. The obvious reason for this goal is that you will not have to write code twice or garble your design by copy-and-paste programming. One level deeper, the exact details of responsibilities are likely to change. Confining 11.2.3 the changes to a single place keeps the software maintainable. As we will 11.5.5 see, however, this simple goal involves some rather subtle implications and challenges.
Plan for collaboration to keep the responsibilities small.
Throughout the design, components and objects will rely on collaboration to fulfill their responsibilities: An object never does things that another object can already do. While identifying individual responsibilities, we will therefore also plan for the necessary collaborations: If an object requires help in its tasks, it must have a reference to the object that can provide the help. The overall goal is to keep objects so small that their behavior can be subsumed under a single, well-defined responsibility. 11.2
In the running example from Fig. 11.2, the NewWizard relies on the NewWizardRegistry to interact with the Eclipse extensions mechanism. 12.3.3 It delegates the task of displaying all found file types to the NewWizard SelectionPage.
At this point, it is important to recall the object-oriented view on methods. A method 1.4.1 call is not merely a technical invocation of a subroutine implementing a specific algorithm. It should instead be read as a message to which the receiver reacts appropriately, at its own discretion. Since the ultimate responsibility for the reaction lies with the receiver, it is also the receiver that decides which reaction will best suit the request.
Mechanisms structure sequences of individual collaborations.
Just as each object has few and small responsibilities, each individual collaboration 11.2 between objects usually concerns a tiny aspect of the overall application’s functionality. It often takes the accumulated effect of many collaborations between several objects to exhibit some desirable, externally visible behavior.
We will use the term mechanism for a sequence of logically connected collaboration steps that together achieve a common goal. Mechanisms induce a switch of perspective: Rather than asking what a particular object does and how it reacts, we ask how a particular overall reaction is achieved through collaborations among possibly several objects. In UML, mechanisms 47 are rendered as sequence diagrams. The comprehensive treatment there underlines the importance of the concept.
In the example, one may rightly ask: How does a wizard-like dialog ensure that the Next button is enabled only if the current page is filled in with complete and valid data? The solution is that the current page observes its input fields and invokes updateButtons() on its wizard container, which in turn calls the page method canFlipToNextPage(). But this is not yet the whole story. Many concrete pages derive from Wizard Page, 3.1.4 and that base class provides some infrastructure. For instance, a flag isPageComplete captures the current status and updating the flag links in with the raw mechanism:
org.eclipse.jface.wizard.WizardPage
public void setPageComplete(boolean complete) {
isPageComplete = complete;
if (isCurrentPage()) {
getContainer().updateButtons();
}
}
Since the chosen mechanisms determine to a large degree the complexity of the implementation, it is important to consider alternatives. In the example, one could require the pages to act as Java beans and to inform PropertyChangeListeners about data modifications.2.1.4 The wizard container would then simply listen to the changes. However, there would always be only a single observer, and the page would have to translate SWT events to PropertyChangeEvents. The special-purpose communication through update Buttons()certainly reduces the overall implementation effort.
9.4.3Another example of a typical mechanism is seen in the task of updating the screen after a change in the model. Fig. 9.14 (on page 500) explains how the discovery of a data modification after several intermediate steps 7.8 leads to a callback to paintControl(). The mechanism is designed in this complex fashion to keep it general and to enable the window system to optimize the painted region in between.
Responsibilities and collaborations must make sense locally.
Mechanisms are necessary to structure and explain sequences of collaborations. However, they incur the danger that individual collaborations cannot be understood at all without also considering the larger picture. As a result, maintenance can easily become a nightmare: Having no time to read through the design documents or having mislaid these documents a long time ago, the maintenance developer must trace through the collaborations using a debugger to reconstruct the intentions of individual method calls.
The only way to avoid this problem is to constantly switch between the global perspective of mechanisms and the local perspective of responsibilities and collaborations of individual objects. It is a good exercise to “write” 11.2.1 a mental documentation for individual objects from time to time: Can you still say in one or two sentences what the object or a particular method does without referring to the subsequent reactions of its collaborators? Only such localized descriptions ensure that the individual classes of the system can still be understood by the maintenance developer.
In the example, the mechanism for revalidating the data and enabling or disabling the wizard buttons can be documented locally. In the description of IWizardPage, and the base class WizardPage, one would have to (1) say that method canFlipToNextPage() must validate the data and (2) leave a remark that the object must invoke updateButtons() whenever the data has changed.
In the Graphical Editing Framework, one can still describe an individual 214 “edit part” (i.e., the unit of composition of drawings) without understanding entire sequences of collaborations. It is necessary to understand only the edit part’s expected reactions to specific stimuli, mostly sent as Request objects.
Disciples of other paradigms often bash object-oriented programming because of the necessity of combining several collaborations to achieve some overall effect. They complain that one never sees where anything particular gets done because so many objects are involved and each contributes so little. This perception has two possible sources. First, the particular design might be so bad that it is indeed impossible to understand the individual objects separately. Second, the developer in question might not be prepared to take the leap to the object-oriented way of thinking, where one focuses on individual objects and trusts subsequent collaborations to occur as would be expected. A similar case occurs for novices in functional programming, where many tasks are approached by (structural) recursion. A fundamental insight on recursion is this: Never think about a recursive function recursively. One does not understand a recursion by unfolding the calls a few levels deep. One has to focus on the contribution of the individual step, trusting the recursive calls to deliver the expected results reliably. Similarly, one understands a loop not by unrolling a few iterations, but rather by seeing 4.7.2 144 the overall picture, as expressed in the loop invariant. In all three cases, the particular way of “seeing the point” in a solution requires some practice and experience with the particular units used for subdividing tasks.
CRC cards help to start designing.
Let us now turn to the design process itself. Design in many places is a creative, formally largely unconstrained activity. To introduce some structure, you can use CRC cards. The abbreviation stands for “class, responsibilities, 32 and collaborators.”
Here is how to start with CRC cards: You buy a few hundred medium-sized 11.3.2 index cards and use one for each object or role that you identify. Each card gets subdivided as shown in Fig. 11.5. At the top of the card, you write the class name. A small right margin holds the collaborators, just to capture the overall graph structure from Fig. 11.2. The remaining area holds the responsibilities.
Figure 11.5 Example CRC Cards
The cards in Fig. 11.5 capture the essence of the design: WizardDialog collaborates with the contained wizard and its pages, as well as with the outer Shell. It has to provide the outer frame for the actual wizard, and it initiates and decides about the flipping between pages. Also, it triggers data validation, by calling canFlipToNextPage() and isComplete() on pages. Each IWizardPage then has to create SWT widgets for data entry and must validate the data. It must dynamically provide the next and previous pages in the sequence. Finally, it has to react to changes in the entered data by asking the container for revalidation.
Since CRC cards are cheap, lightweight, and easy to manipulate, they invite you to sketch designs quickly, and to throw away superseded versions of objects without regrets. Also, the limited area on the index cards forces you to split large classes—there is simply not enough space to describe too many or too complex responsibilities.
CRC cards establish a useful shape for thinking about design. Experienced designers and developers will often find that CRC cards assemble themselves in their heads and that they can type out the classes, or at least their public methods, immediately. 32 CRC cards are an effective way of learning object-oriented design, but you must not feel constrained by the format for all times and in all situations.
Start from your idea of a solution.
11.5.2One important aspect of design is that it always captures one way of thinking about the given problem and one approach to solving it. Different people will understand a challenge differently, and they will arrive at different designs. One key contribution of a good design is that it captures the team’s common understanding of the intended solution.
Before embarking on a design, you must have at least a general understanding of the overall solution strategy. Likewise, figuring out the details of the design is a good time for elaborating one’s idea of the solution.
Design, evaluate, improve.
Design is always an iterative process. There is rarely one optimal solution, and in any case, one tends to forget important details when first designing networks of objects. As a result, one has to sketch out a few possibilities before deciding on their relative merits. CRC cards are just the tool for this kind of lightweight iteration.
Here are some evaluation criteria. Most fundamentally, one has to establish that the design solves the given problem: Will all the use cases be covered by the envisaged collaborations and mechanisms? Do all objects have access to the required collaborators? Beyond that, one can evaluate the match with object-oriented principles: Are the individual objects small and 11.2 self-contained? Are they really reactive entities or are some objects merely 92 storing information that other objects work on? Finally, there are possible strategic goals: Can the crucial components be rigorously unit-tested, by 5.1 placing them in a test fixture? Does the design respect model-view separation? 9.1 Can the implementation be extended to cover modified or additional 12.3 use cases? Can parts of the implementation be reused in different contexts? 12.4
Having the design available in some external form, such as CRC cards, enables us to point to special aspects and match them against our idea of desirable designs as well as against the given problem. Also, any implicit assumptions not yet worked out properly will be uncovered immediately.
The goal of these evaluations is to expose possible flaws early on, while changes are still quick and cheap. If we find that a missed use case does not fit the existing design only after completing substantial parts of the implementation, the effort to integrate it by changing the code structure will be much higher. Take the opportunity to seriously challenge your own designs through CRC cards.
Become aware of the decisions you make.
Designing always involves making decisions: Which object is responsible for which aspect of some reaction? Is a responsibility small enough to be carried by a single object or should it be split and distributed among collaborators? Do we need to generalize a solution to enable reuse later on while incurring some implementation overhead now?
To become good designers, we have to become aware of which decisions we make for which reasons. If we fail to see the necessity of a decision, 11.5 we are likely to blunder on in one direction without even being aware of possibly smoother and shorter paths. If, in contrast, we have a bunch of alternatives right at our fingertips, we are more likely to come up with a useful design. We also gain the consolation that if we have taken the wrong turn at some point, we are not stuck but still have a number of alternative and promising paths left to explore.
11.2 The Single Responsibility Principle
170 172If you are looking for a single guideline that will make a design come out “truly object-oriented,” it is certainly the Single Responsibility Principle, abbreviated as SRP. Its diligent application will lead to small classes that 1.1 collaborate in well-defined ways, and to software that is easy to change and to maintain.
11.2.1 The Idea
The Single Responsibility Principle states that each class should have only one responsibility and should delegate any tasks not related to that responsibility to its collaborators. For an illustration, let us go back to the 11.1 wizard creating new files in Eclipse. With little time and little experience, we might just start on the user interface with the WindowBuilder and add 7.1 the functionality to the event handlers as we go along. We will probably end up with the design in Fig. 11.6. The FileCreationWizard object displays 12.3.3 a window with buttons for navigation; it looks through the registered file types; it allows the user to select a type; and finally it creates a wizard page to gather the information specific to the file type. Only that last task is delegated, and only by necessity: Because the information is different, we need different screens to collect it. Basically, this is an instance of the 1.3.4 STRATEGY pattern.
Figure 11.6 A FileCreationWizard with Too Many Responsibilities
Make each object responsible for one thing.
Obviously, the FileCreationWizard in Fig. 11.6 will be a huge class. We have surrounded its responsibilities with dotted lines to indicate that each will require several fields and methods in the implementation.
Suppose now that we “explode” this huge class into separate classes, one for each responsibility. We arrive at the design in Fig. 11.7. Basically, we 11.1 have moved each thing that needs doing to a separate object that actually does it. Only the ExtraInformationPage retains two responsibilities, because in this way it can apply the entered information directly during file creation. The similarity to Eclipse’s own design in Fig. 11.2 (on page 571) is really quite remarkable: Although one could argue that we were bound to come up with the same design because we knew the original, the tasks in Fig. 11.6 really needed doing, and now we have done them.
Figure 11.7 Refactored FileCreationWizard
The SRP keeps objects small and understandable.
A major benefit of applying the Single Responsibility Principle is that the code base of an object remains small: If independent tasks are distributed over several objects, then each of them has a simpler implementation. The code also becomes easier to understand, because the reader knows that any particular snippet must somehow fit in with the goal of fulfilling the object’s one responsibility. This tight logical connection between an object’s 12.1.3 different code pieces is also called cohesion.
The SRP keeps objects maintainable.
The new design also clarifies the later implementation in two respects. First, it is clear where to look for the implementation of a specific piece of functionality that may need fixing or changing. This helps greatly in maintenance work. Second, the dependencies between the different tasks become clearer: Within the huge object in Fig. 11.6, any piece of code may access and manipulate any field, and any method may call any other method. In contrast, in the “exploded” design of Fig. 11.7, each object can access only 1.1 the public methods of other objects; the fields remain encapsulated.
A deeper reason that makes the “exploded” design desirable is that the clarification 4.1 of dependencies leads to smaller and less complex class invariants. The fields needed for each responsibility in Fig. 11.6 are linked by consistency constraints (i.e., by class 4.1 invariants). The invariant of the entire class combines these into a huge assertion. Each public method must prove that it establishes that assertion in the end. The developer must therefore understand the invariant completely to exclude any possible interactions, even if a public method touches only a very few fields.
Give each object a coherent overall purpose.
One question arises immediately: How can an object with only one responsibility ever get anything significant done? And how can a system consisting of only such minuscule objects ever fulfill its users’ demands?
The answer is that the actual power associated with a responsibility depends largely on the granularity and abstraction level chosen for expressing the responsibility. The unit “responsibility” is a conceptual measure, not a technical one. For instance, the responsibility to gather existing extensions in the designs of Fig. 11.7 and Fig. 11.2 involves a fair bit of data structures and boilerplate code, as well as some nontrivial knowledge about the Eclipse API for accessing extensions. Still, it is one self-contained responsibility that the object NewWizardRegistry fulfills.
The Single Responsibility Principle is about logical cohesion: All tasks 12.1.3 236 that an object undertakes must be subsumed under its responsibility, but this does not mean that there is only a single task from a technical perspective. It might be better to say that an object has a single purpose, which also captures a specific reason why the object must be present in the 92(“Lazy Class”) system.
Learn to tell the stories of your objects.
Another helpful intuition is that of telling “stories.” When we ask, “What’s the story behind this?” we actually mean this: What is a convincing explanation? Why should I believe the author? Why should I “buy” the argument? A story is often related to a message: When a blog entry, for instance, tells a good story, we mean that it has a point and states it clearly and convincingly.
Telling “the story” of an object well means explaining what it does, why it is a useful member of the community, why the implementation effort is justified, and why its public API is exactly right. Imagine you have to sell your objects to your teammates and you have only a few minutes to convince them you did a great job. Which aspects would you emphasize?
The purpose of this exercise is to form a clear idea of each object’s goals, as well as of its limitations and the tasks it delegates to its collaborators: Someone who knows his or her own expertise is better in focusing on this area. It may take some time to find “the story” of your object, but it is always worthwhile, since it helps you to focus, to find the “simplest implementation,” 28 and to communicate the results within the team.
Describe an object’s purpose in a single sentence.
To check whether you have succeeded in assigning a single responsibility to an object, try to summarize its purpose in one sentence. Here are some examples from the original new wizard (Fig. 11.2):
• The NewWizardRegistry collects the declared extensions into a conveniently accessible data structure.
• The NewClassWizardPage enables the user to enter the parameters necessary for creating a new source file and then creates the file.
• An IWizardDescriptor encapsulates a declared file type to simplify setting up a wizard to handle the file type.
• A WizardDialog acts as a container for a concrete wizard with several pages.
An important point is that the one sentence must be a plain, short specimen. Once you start stringing together subclauses joined by “and,” “if,” and “but,” you defeat the purpose of the exercise. Finding a sentence meeting this criterion is actually quite a challenge. The author admits freely that he had to fiddle with the formulation of the given examples to get there.
If you cannot come up with a short sentence that captures the object’s purpose, the design may be flawed. A case in point is the NewClassWizard Page: 11.1 Its description does contain an “and”—and we have already found that it violates model-view separation.
Condense an object’s single responsibility into the class name.
1.2.3A single-sentence purpose is nice, but it is better still to condense the purpose even more by expressing it in the class name. For example, a Wizard Dialog is a dialog containing a wizard; a NewClassCreationWizard is a wizard responsible for creating a new source file with a Java class. Similarly, knowing that a “registry” in Eclipse parlance is something that manages specific objects, we see that the NewWizardRegistry manages extensions offering “new wizards.” Renaming NewWizardNewPage to NewWizard SelectionPanel 7.5 would clarify that it is a composite widget offering several “new wizards” for selection.
11.2.2 The SRP and Abstraction
The Single Responsibility Principle has more benefits than just keeping the code base of individual classes small and coherent. An important aspect is that it guides developers toward creating useful abstractions.
Assigning single responsibilities means understanding the solution.
11.2.1You will quickly note that it is actually very hard to come up with a single sentence that describes an object well and comprehensively. The reason is simple: You have to identify the object’s really important aspect from which all other aspects follow logically. In other words, you have to arrange your possibly many ideas about what the object does into an overall structure.
Once you succeed, however, you gain a better understanding of your object. The object is no longer a flat collection of tasks; instead, these tasks are interrelated. You know which ones are important and which ones are mere technical details; which ones are crucial and which ones are incidental; and which ones characterize your object and which ones it may share with other objects.
The SRP helps to abstract over the technical details.
The Single Responsibility Principle also makes it simpler to understand the 11.1 application’s overall network of objects. Because you can summarize each object in one sentence, you can skip quickly from one object to the next without cluttering your mind with the technical details of each. Once your mind is freed from these details, it can hold more of the actual design.
The SRP helps in taking strategic design decisions.
Strategic insights and decisions are possible only at the level of abstraction created by brief summaries of objects. To make decisions, you have to juggle in your mind the design as it is, and most minds are not capable of holding too many details at a time. Brief summaries, in turn, are possible only if each object takes on only a small task, a single responsibility.
The SRP is indispensable in communicating the design.
There is nothing more boring and ineffective than listening to teammates expounding on the internals of their classes at the slightest provocation. Many developers fall for the temptation of boasting about their technical exploits.
The Single Responsibility Principle enables a team to communicate effectively: Because each team member prepares a one-sentence summary of his or her objects in advance, answers become shorter. Because the answers link directly to the overall system purpose, other team members can pick them up and store them away quite easily. In the end, the whole team gets 28 a pretty good overview of the system, without having to understand the technical details of every class.
The SRP also applies to class hierarchies.
The first association that springs to mind in connection with abstraction is inheritance. Since responsibility-driven design focuses on objects, rather than classes, we will look only at the basics. The Single Responsibility Principle applies to arranging code into classes, so it applies to hierarchies as well. From this point of view, each subclassing step and each level within 11.4 the hierarchy has its own purpose and its own responsibilities. For an extended example, you can look at the hierarchy of editors in Eclipse and 12.2.1.2 in particular the chain EditorPart,AbstractTextEditor, StatusText Editor, and AbstractDecoratedTextEditor.
11.2.3 The SRP and Changeability
It has been noted from the infancy of computer science that modularization is necessary because software probably needs to be modified during its lifetime. In his seminal paper, Parnas postulates: “The essence of information 205 11.5.1 hiding is to hide design decisions that are likely to change, and to make modules communicate through interfaces.” It must be said that—given Parnas’s words were written in 1972—“design” means mostly design of data structures and algorithms. Still, the point is there: We make decisions now that we are pretty sure we will have to revise later on. How can we do this without breaking our product?
The SRP helps in restricting the impact of necessary changes.
172,170 In his presentation of the Single Responsibility Principle, Martin defines the term responsibility itself as “one reason to change.” If each object has a single responsibility, then there is one kind of change that the object will absorb, one kind of change from which it insulates the rest of the system. The unit of task assignment coincides with the unit of changeability.
205 It is interesting to note that Parnas, in 1972, also made this connection: “In this context ‘module’ is considered to be a responsibility assignment rather than a subprogram.” It seems that the metaphor of “responsibilities” is, indeed, very appropriate for reasoning about good software organization.
The Single Responsibility Principle does not immediately guarantee that changes will not ripple through the system. This outcome can be achieved 11.5.1 12.2 only by striving for information hiding and designing for flexibility throughout. However, the Single Responsibility Principle lays the foundation: When an object acquires a responsibility, it can and should become zealous about it and should not allow other objects to meddle with its own implementation decisions. Guarding these decisions as a private secret is the right step to take.
The SRP helps to locate the point of change.
At the same time, the Single Responsibility Principle helps one find the 11.2.1 place that needs changing. Assigning a responsibility to an object also means that no other object will take on the same responsibility. If some behavior needs changing, we can quickly find the class that contains the code, through its one-sentence summary of its purpose.
Once we start looking into a class, we are sure that all the code we see is potentially affected by the change, simply because the class takes on only a single, coherent responsibility.
The challenge in design is to anticipate the probable changes.
How then, do we assign responsibilities? So far, we have proceeded largely by intuition. Now, we get a new criterion: Each responsibility should enclose one area where the software is likely to change. Anticipating such changes is far from simple. While we strive to find a solution for a given set of requirements, we have to think at the same time about similar solutions for similar requirements. A few heuristics will help to identify the critical points in the design.
• Experience in building a number of similar systems gives us a pretty good idea of the variability between different instances. 70
• The insecurity we might feel in formulating and solving a specific problem can be a hint that we might have to revisit the code later on.
• If we have made a deliberate and well-informed decision between several alternatives, as professionals, we must be well aware that the others may come in handy later on.
• We are bringing in a library with which we have little prior experience. To make it simple to switch to a competitor, we hide all accesses in objects.
• Sometimes we deliberately build a suboptimal solution, perhaps even a mock object as a temporary stand-in for the the real and complex 5.3.2.1 implementation.
From a language perspective, these guidelines are all about encapsulation 1.1 11.5.1 of private elements behind the public facade of an object. Specifically, the remainder of the system can keep on using the same external interface as long as the internal implementation fulfills the established contracts. 4.1
The two notions of responsibility coincide naturally.
Fortunately, there is also a direct link between the intuitive approach of chopping up given requirements into individual responsibilities and the search for responsibilities as spots of likely change: Changes to a software are usually triggered by changes in the requirements. If these requirements vary only slightly, then chances are that they will fall under the same one-sentence summary of the object currently handling the requirement. The change then concerns only the internals of that object, and these internals are encapsulated within the object through its public interface. As a result, the remainder of the system is not affected, and the impact of the changing requirements is absorbed gracefully by the existing software structure.
11.3 Exploring Objects and Responsibilities
Having mastered the concepts and basics of responsibility-driven design as well as the all-important Single Responsibility Principle, we will now try out this approach on a self-contained project: a simple function plotter.
11.3.1 Example: A Function Plotter
Fig. 11.8 shows the overall application we will implement. In the lower-left corner, the user enters a formula, which then gets plotted in the main area above. In the lower-right part, the user selects the region of coordinates and the type of axis (simple or grid).
Figure 11.8 The Function Plotter Example
The whole thing seems simple enough, and you have probably implemented something like it just for fun in an hour or so. The challenge and goal here is not so much the functionality, but the design: How can we reuse the shift-reduce parser from the INTERPRETER pattern and the MiniXcel 2.3.3 9.4 application? How can we keep individual aspects such as the certainly suboptimal choice of the plot region changeable? How can we build the application from small individual objects that collaborate only in well-defined ways? How do we shape those collaborations so that the objects remain as independent from one another as possible? 12.1
We will see that to answer these questions satisfactorily, we have to tie together many individual design principles, design patterns, and elements of language usage that we have so far treated separately. Seeing them connected in a comprehensive example gives us the opportunity to explore their interconnections and synergies. At the same time, we will introduce more details about the known concepts.
The subsequent presentation focuses on the concrete code of the function plotter and justifies the design decisions from the resulting code structure. Afterward, we will give a brief summary of the development. To keep 11.3.4 the wider view, you may wish to peek ahead at Fig. 11.11 on page 617, which depicts the main objects introduced and highlights their principal relationships.
11.3.2 CRC Cards
Before we can start, we need a better handle on design. The box-and-line drawings used in the presentation so far are all very well, but they hardly constitute a clean format for larger designs. At the other end of the spectrum, a semi-formal language like UML can easily stifle our insights47,198 and creative impulses.
Class, responsibilities, collaborators—CRC.
The insight that objects are characterized by their responsibilities and their collaborations has led to the proposal of writing down the class, the responsibilities, 32 and the collaborations onto small colorful index cards—in short, CRC cards. We have seen the key idea of CRC cards already. In particular, 11.1 Fig. 11.5 (on page 584) gives two simple examples. Now, we will add a few remarks on how to use them.
Use CRC cards to lay out candidate designs quickly.
Writing class names and responsibilities onto index cards may not seem such a great breakthrough at first. However, it turns out that the medium of CRC cards helps to push the design process in the right direction.
• Since CRC cards are small, there is no space to write up many or complex responsibilities. The medium enforces a sensible subdivision and drives us toward the Single Responsibility Principle. 11.2
• CRC cards are cheap and easy to manipulate. This encourages you to write up and explore alternative designs, and to discard those that prove wrong on closer inspection.
• CRC cards appeal to our visual and tactile understanding of situations. You can arrange tightly linked objects near each other, and stack a class and its helpers, or a role and its concrete realizations. 11.1 You might also want to choose different colors for roles and concrete 1.8.7 objects, for different types of objects such as boundary objects, or for 9.1 the model-level and view-level objects.
• CRC cards can be moved around freely. This enables you to explore collaborations—for instance, by picking up one object and moving it temporarily near to its collaborator while describing a message, and then back to its original place in the design. You can also explore various configurations and layouts of the cards on a table and see which ones best reflect your intuition about the relationships between the objects.
• CRC cards are fun and lightweight. If design is a creative process, then its impetus must not be smothered under heavyweight design tools with strict usage guidelines and thick manuals. With CRC cards, you can just start designing the minute you have an idea or a problem.
11.1In some sense, CRC cards play a role similar to that of metaphors in software engineering: Because software objects are invisible, intangible, and therefore largely abstract entities, it is good to link them to the concrete reality, with which most of us have extensive experience.
11.3.3 Identifying Objects and Their Responsibilities
Finding objects is not a simple task in general. It is not sufficient to look at the user interface or to model mere data structures from the application 11.1 domain. We have to come up with a whole collection of objects that together 263 set up a software machinery to fulfill the users’ expectations. This also means that the ultimate source of all responsibilities of the system’s 11.1 objects is the set of use cases and the specified reactions: If something needs doing, somebody must do it. But the reverse is also true: If somebody does something, then the effort should better be justified by some concrete reaction that the system must implement.
Experienced designers carry with them a comprehensive catalog of their previous designs and model their new objects based on successful precursors. They know which kinds of tasks objects are likely to perform well and which objects are likely to be useful.
We will therefore provide some common justifications for why particular objects are present in a system and which kinds of purpose they fulfill. Part I 1.8 has given a similar overview from a technical perspective, and it may be useful to revisit it briefly. Now, we will reexamine the question from the perspective of responsibilities, rather than class structure.
The presentation here proceeds from conceptual simplicity to conceptual abstraction—from the objects that are obviously necessary and easy to recognize to the objects that emerge from some previous analysis. We deliberately refrain from giving an overall picture up front, as we did for11.1 the introductory example. Instead, we follow the reasoning steps as they could take place in the design process. In many steps, we make explicit the connections to the general structuring principles introduced earlier on. We hope that this arrangement will help you to apply similar reasoning within your own projects. A summary of the development is given in Section 11.3.4.
11.3.3.1 Modeling the Application Domain
Any successful design will finally lead to software that fulfills all given requirements. For instance, one can start with use cases to trace the system’s 11.1 11.3.2 reaction through the object collaborations. Alternatively, one may start from the central business logic and the core algorithmic challenges. In any case, the first objects that spring to mind emerge from the analysis of the application domain—they are domain objects.
Represent concepts from the application domain as objects.
In the example, when we think about “plotting a function graph” as shown in Fig. 11.8, we will at some point talk about a “plot region” as the ranges in the x- and y-coordinates that are displayed on the screen. Translating this concept into an object yields a simple bean that keeps the minimal and 1.3.3 maximal coordinates:
fungraph.PlotRegion
public class PlotRegion {
private double x0;
private double x1;
private double y0;
private double y1;
. . . constructor, getters, and setters
}
Another example is the function that gets plotted. Ideally, we would like to plot functions f : → ∪ {⊥}, functions from the reals to the reals where some values are undefined (value ⊥). Of course, we use double to represent . But beyond that, what is “the best” representation for the function itself? A syntax tree? Some kind of stack machine code? Furthermore, do 2.3.4 we have to implement the formula representation ourselves or will we find a full-grown, reusable library? To defer such decisions, we introduce a role Plottable Function: We simply specify how the function will behave without deciding right now on its concrete class. The method funValue() in the following interface captures the function f, with an exception signaling ⊥. Adding a self-description method is always useful for displaying information to the user.
computation.PlottableFunction
public interface PlottableFunction {
String getDescription();
double funValue(double x) throws Undefined;
}
One question is whether an explicit getDescription() method is useful, given that every object in Java has a toString() method that can be overridden. According to its API specification, that method is supposed to return a “a concise but informative representation that is easy for a person to read.” Certainly, that statement is rather 44(Item 10) vague and open to interpretation. The general advice is to include all information useful for the human reader, if this is possible without returning unduly long strings. Then the object can be printed usingSystem.out.println(), String.format(), and so on, and it 1.8.4 will also display nicely in debuggers. For simple value objects, such as numbers, it may 44(Item 10) even be possible to provide a parsing function to recreate an equivalent object from the string representation. We would argue that toString() should be geared toward tracing and debugging scenarios: The end user is not concerned with objects, so a “representation of an object” is not useful in this situation. Instead, user interfaces require specialized 9.1 external representations, so it is better to keep the two forms of “readable representation” separate in the code.
Starting with the application domain often means starting with the model.
9.1In the context of model-view separation, we have advocated starting the application development with the functional core—that is, with its model. By focusing on the application domain, you will likely start with the business logic and therefore the model.
The application domain is a dangerous source of inspiration.
92The PlotRegion object defined earlier is “lazy”: It does not do anything, but merely holds some data. Such objects often result from a superficial analysis of the application domain, where we tend to describe things and concepts statically, by capturing their relevant attributes. Many things we find in the real world are not active themselves, but are manipulated by others. Think of bank accounts in a finance application, of hotel rooms in a booking system, or of messages in an email client. They all have perfectly clear relevance for the application, yet they are passive. As a result, they 1.1 do not fit in with the view of objects as small and active entities.
Think about the software mechanisms throughout.
However, we can redeem the use of domain objects by simply asking: If the 11.1 objects do exist in the system, which part can they play in the mechanisms of the software machinery?
In the case of the PlotRegion, a simple responsibility would be to keep interested objects informed about changes. In Fig. 11.8, both the central function graph and the plot region selector at the bottom must synchronize on the current plot region. Model-view separation, or more specifically the 9.2.1 MVC pattern, tells us that in such cases it is best not to assume one flow 9.2 of information, but rather to use a general OBSERVER pattern. For now, 2.1 the user can only select the region in the special widget, but perhaps a later extension will allow the user to zoom in and out of the function graph by mouse gestures on the main display. The implementation is, of course, straightforward:
fungraph.PlotRegion
public class PlotRegion {
. . .
private ListenerList listeners = new ListenerList();
. . .
public void setX0(double x0) {
this.x0 = x0;
fireChange();
}
. . . fire, addListener, removeListener according to pattern
}
As a detail, we decide to use the simpler pull variant of the pattern, 2.1.3 because a change in the region will require a full repaint of the function graph anyway, since incremental updates would be too complex. 9.4.3
fungraph.PlotRegionListener
public interface PlotRegionListener extends EventListener {
void plotRegionChanged(PlotRegion r);
}
Software is rarely ever a model of reality.
The example shows clearly that the software machinery is quite distinct 263 from any mechanisms found in the real world. Indeed, the reasons for introducing OBSERVER are strictly intrinsic to the software world and derive from the established structuring principles of that world. It would therefore be naive to expect that a close enough inspection of the application domain would yield a useful software structure.
The only exceptions to the rule are simulations: If the application-domain objects do have a behavior that needs to be recreated faithfully in software, chances are that one can obtain a one-to-one match.
Application objects are good choices for linking to use cases.
Even if pure application objects are rare, they do have their merits when they fit naturally into the software machinery. For one thing, they enhance 24 the traceability of the use cases and requirements to the actual software. Once one has understood the use cases, one can also understand the software. If customers are technically minded, for instance in subcontracting parts of larger systems, they might care to look at the code themselves.
11.2.3Furthermore, application objects may accommodate changes gracefully. If an application object reflects and encapsulates responsibilities derived from the application domain, then minor changes in the requirements are likely to be confined to the application object.
11.3.3.2 Boundary Objects
1.8.7Another easily recognized source of objects is the system’s boundary, which 1.8.1 contains objects that are interfacers in the terminology of role stereotypes. Boundary objects hold a number of attractions:
• They often link to concrete external entities such as the application’s file format, some XML transfer data, and so on. These entities may be available for inspection beforehand, so they can guide us in designing the software to process them.
• They are motivating because their reaction is visible externally and we may even demonstrate them to future users.
• 11.1The design process can start from the use cases. Since we encounter the boundary objects first, we have a clear picture of their reactions.
• They come with several predefined responsibilities. For instance, when 4.6 they accept data from the user or other systems, they have to shield 1.5.2 the system from possible failures, inconsistencies, and attacks.
• Their interface is often determined by libraries and frameworks. For instance, when using a SAX parser for XML files, we have to provide a ContentHandler to process the result. When writing a graphical user interface, we use SWT’s widgets. When writing a web application, we 201 may use the servlet API.
Once the boundary objects are identified, we can start assigning responsibilities to them.
Apply model-view separation to boundary objects.
An essential guideline for designing boundary objects is to use model-view separation, even if the boundary is not a graphical user interface. In particular, the boundary objects should be as small as possible. They should not contain any business logic, but only invoke the business logic implemented 9.2.2 in a model subsystem. The benefits of model-view separation then 201 carry over to different kinds of interfaces. Think of web services. Over time, many concrete protocols have evolved. If you keep the business logic self-contained, it becomes simpler to create yet another access path through a different protocol.
Make compound widgets take on self-contained responsibilities.
In the present example, the only boundary is the user interface. Since this is a common case, we explore it a bit further. As a first point, it is useful to create compound widgets that take on self-contained responsibilities. 7.5 In the example, the user has to enter the function to be plotted in the lower-left region of the screen (Fig. 11.8). We invent an object Formula Field for the purpose.
dataentry.FormulaField
public class FormulaField extends Composite {
private Text entry;
. . .
}
Once that object is in place, it can take care of more responsibilities. The formula string entered by the user needs to be parsed before the application 2 2.3.3 can do anything useful with it. And if something needs doing, somebody has 11.1 to do it—so why not the FormulaField? This choice is, in fact, supported by a second argument. Parsing includes validation, since a formula with a syntax error cannot be handled by the plotter at all. The general principle 1.8.7 1.5.2 is that such validations take place in the system boundary. By having the FormulaField parse immediately and report any errors back to the user, the remainder of the system is shielded from invalid inputs altogether and can deal with clean abstract syntax trees. 2.3.3
The FormulaField therefore includes a widget for error reporting and a field for the parsed result. (We will examine the aspect of reusing the previous parser implementation later on.) Whenever the user presses “enter” 12.4.3 in the text field, the attached listener calls the methodreparseFormula(). 7.1 If there is no parse error, the new formula is stored and reported to any interested listeners (lines 10–12). Otherwise, the error message is displayed and a null object indicating an invalid formula is reported (lines 14–16). 1.8.9 For model-view separation, note how the widget’s listener orchestrates the application of the relatively complex, model-level parser.
dataentry.FormulaField
1 private Label error;
2 private Expr formula;
3 . . .
4 public Expr getFormula() {
5 return formula;
6 }
7 private void reparseFormula() {
8 Parser<Expr> p = new Parser<Expr>(new SimpleNodeFactory());
9 try {
10 formula = p.parse(entry.getText());
11 error.setText("");
12 fireChange(formula);
13 } catch (ParseError exc) {
14 error.setText(exc.getMessage());
15 formula = null;
16 fireChange(null);
17 }
18 }
2.1.4You may rightly ask whether it is really sensible to implement OBSERVER here. In the actual application, there is only a single listener, and that object could have been made a direct collaborator of the FormulaField. We have used the more general structure for three reasons. First, it enables us to explain the FormulaField in a self-contained 11.2.1 fashion, without forward references. This simple story in itself is an indication that our object has a clear purpose. Second, the FormulaField yields an example of 12.1 12.4 the rather elusive concepts of loose coupling and reusability. Third, during the actual development of the example code, we wished to finish the “obvious” part beforehand, so 12.2 that we actually did not know the later collaborator. Using Observer was one way of postponing this decision.
Introduce compound widgets for elements that are likely to change.
Another example of an obvious compound widget is the task of selecting the plot region: It has to be done somewhere, but there is as yet no object to do it. The naive solution is to just place a few sliders into the main window, according to the screenshot in Fig. 11.8, and somehow wire them up with the plot region.
The designed solution is to make a new object responsible for enabling the user to manipulate the plot region. The RegionSelector offers four sliders, two for the position of the graph and two for the scaling. The region is then updated whenever the sliders change. The nice thing is that now all the code concerned with the current region is confined in a single object, rather than being spread throughout the main window.
dataentry.RegionSelector
public class RegionSelector extends Composite {
private Scale xPos, yPos, xScale, yScale;
private PlotRegion region;
. . .
}
A second argument for introducing the extra object is that the selection mechanism is likely to change, because the current one has admittedly a 11.2.3 rather poor usability. By making one object, and only one object, responsible for the user interface of the selection, we can substitute the interface once we get a better idea of what users really like to see here.
Merely pushing the four sliders into a surrounding widget is insufficient, however. The crucial point that insulates the remainder of the system from possible changes here is the lean and clear API that does not depend on 11.3.3.1 the widget’s internal details: The PlotRegion object was introduced and defined long before we even thought about the necessity of manipulating the region!
The idea of simply setting the PlotRegion to communicate the changes made by the user is a typical instance of a mechanism built into the software machinery. The 11.1 overall goal is, of course, that the graph is repainted if the user changes the region. This is accomplished indirectly by making the FunGraph widget, to be described later, observe 11.3.3.3 the PlotRegion object. The mechanism is, in fact, not very innovative, but is modeled after that of the MODEL-VIEW-CONTROLLER pattern. 9.2.1
11.3.3.3 Objects by Technical Necessity
Another set of objects that enter the design immediately are thrust upon you by the technical API of the frameworks and libraries that you are using. For instance, all elements of the user interface must be widgets, derived from the base class specified by the framework; when working with threads, you have to provide a Runnable that gets executed concurrently. 8 In all such cases, the object must exist for technical reasons, but design is still necessary to decide which responsibilities the object will take on beyond the minimal API specified by the framework.
In the running example, the plotter must use custom painting to actually 7.8 display the graph of the function. It registers a PaintListener on itself, 2.1.3 which calls paintComponent().
fungraph.FunGraph
public class FunGraph extends Canvas {
. . .
protected void paintComponent(PaintEvent e) {
. . .
}
. . .
}
When frameworks enforce specific objects, think about suitable responsibilities.
However, the implementation of paintComponent() depends on design decisions: Does the FunGraph itself draw the axis? Does it encapsulate the 11.3.3.7 1.8.6 potential complexity of tracing out the graph in a separate object? The answers are seen in the code: The painting of the axis is delegated, but the 11.3.3.7 actual graph is drawn directly.
fungraph.FunGraph
protected void paintComponent(PaintEvent e) {
. . .
if (axisPainter != null)
axisPainter.paintAxis(this, g, r, region);
. . .
paintFunction(g, r);
. . .
}
Painting the graph then involves tracing out functions → ∪ {⊥}, 11.3.3.1 given by the interface PlottableFunction introduced earlier. We use a naive approach of walking from pixel to pixel on the x-axis, evaluating the function at each point, and connecting the points we find by (vertical) 11.3.3.4 lines. The object Scaler, to be discussed in a minute, is a helper to convert between screen coordinates and real coordinates.
fungraph.FunGraph
private PlottableFunction fun;
. . .
private void paintFunction(GC g, Rectangle r) {
Scaler scaler = new Scaler(region, r);
int prevY;
. . .
for (int x = r.x; x != r.x + r.width; x++) {
. . .
double xp = scaler.toPlotX(x);
double yp = fun.funValue(xp);
int y = scaler.toScreenY(yp);
. . .
g.drawLine(x - 1, prevY, x, y);
prevY = y;
. . .
}
}
3.2.2In this context, PlottableFunction is a client-specific interface. Anything that can evaluate and describe itself can be plotted by FunGraph.
11.3.3.4 Delegating Subtasks
So far, we have dealt with objects that were indicated by external circumstances: concepts from the application domain, parts of the user interface, and the API of employed frameworks. Now, we turn to the first objects that emerge from an analysis of the concrete problem at hand: helpers. Such 1.8.2 1.8.5 objects are usually service providers; in the best case they are reusable.
If some task admits a self-contained description, introduce an object.
Here, we see a prototypical application of the general principle: If something needs doing, somebody has to do it. Moreover, if you are able to describe 11.2.1 a task in a single sentence, then this is a strong indication that a separate object, with a single responsibility, should take on the task.
In the running example, we have to convert between pixel-based coordinates on the screen and double coordinates for evaluating the given function. Of course, the FunGraph could do this itself. But then, the task is a snug little piece of functionality that might as well be given to a separate object. Here is our code (leaving out the analogous case for the y-coordinates):
fungraph.Scaler
class Scaler {
private PlotRegion plot;
private Rectangle screen;
public Scaler(PlotRegion plot, Rectangle screen) {
this.plot = plot;
this.screen = screen;
}
public double toPlotX(int x) {
return (double) (x - screen.x) / screen.width *
plot.getXRegion() + plot.getX0();
}
. . .
public int toScreenX(double x) {
return (int) ((x - plot.getX0()) / plot.getXRegion() *
screen.width + screen.x);
}
. . .
}
Service providers are often passive.
The Scaler uses a PlotRegion object that is to be mapped to a rectangular 11.3.3.1 screen area. Since a PlotRegion may change, the question is whether the Scaler should observe the changes. If this were so, then the Scaler itself would have to become observable to send messages when the result of scaling has changed. This would, in fact, make the Scaler an active, and therefore “better,” object.
Although this idea is viable, it does not fit the Scaler’s purpose: The Scaler performs a computation, and that computation is stateless. The OBSERVER pattern, in contrast, is all about notifying interested objects 2.1 about state changes.
Service providers, which act on behalf of a caller, are often passive. The collaboration can then be defined very precisely based on classical contracts. If the service providers do contain state (apart from caches, which 4.1 1.3.6 are an internal technical detail not visible to clients), they would offer the OBSERVER pattern, which still keeps them independent of any concrete 12.1 collaborators.
Passive service providers are found in great numbers in the Java library. InputStreamReaders, Connections to relational databases, SimpleDate Formats, a Pattern representing a regular expression—they all wait for their callers to ask them to perform their tasks.
Active collaborators fit well with responsibility-driven design.
It would be we wrong, however, to assume that most objects taking on 11.3.3.6 a well-defined subtask are passive. For instance, the ApplicationWindow 11.3.3.2 tying together the overall interface from Fig. 11.8 delegates the entry of the formula to a FormulaField and the plot region to a RegionSelector, as we have seen. These objects are very much active, as they react to user 1.8.7 input and pass on any information received in this way, after validating and preprocessing it to fit the application’s internal requirements.
Factoring out tasks opens the potential for reuse.
In all cases, the fact that a task is taken on by a self-contained object opens 12.4 up the potential for reusing the implemented solution to the task. Had we integrated the FormulaField and PlotRegion into the Application Window and FunGraph objects, respectively, they could not have been extracted. 1.8.5 From a technical perspective, having a functionality available in a 12.4.1 12.4.3 self-contained object is a prerequisite for reusing it. However, this alone is insufficient—objects are usually not reusable per se.
Factoring out tasks provides for potential changeability.
11.2.3Another benefit from moving subtasks into separate objects is that the current implementation can potentially be changed. First, if the new collaborators 1.1 11.5.1 take encapsulation seriously, then their internals can be exchanged at any time. For instance, the usability of theRegionSelector can be enhanced in any desirable way as long as the outcome of the selection is stored in the target PlotRegion.
One step beyond, if it turns out that entirely different implementations should coexist simultaneously so that the user can choose among them at 11.3.3.7 1.3.4 3.2.1 runtime, one can introduce a new interface and let different objects implement that interface. As with potential reuse, the existence of the helper 12.3 object is insufficient: Other objects must be able to reimplement the interface in a sensible manner, and the interface must be designed to be reimplemented.
11.3.3.5 Linking to Libraries
A lot of things that need doing in the software world will actually have been done already: by a teammate, by a different team in your company, by some contributor from the open-source community, or by a special-purpose company selling special-purpose components. Such ready-made solutions are then offered as libraries or frameworks. In the following presentation, we 7.3.2 will talk about libraries, but note that the arguments apply to frameworks as well.
Use libraries to boost your progress.
Responsibility-driven design is based on the principle that anything that needs doing must be done by some object. But this does not necessarily mean that one has to write the object from scratch. In many cases, it is better to look for a library containing a suitable object: You get the functionality almost for free. All you have to do is read a bit of documentation, look at a few tutorials, and there you are. Furthermore, a widely used library is also well tested. Any implementation you come up with will usually be more buggy in the beginning. And finally, the design of the library itself captures knowledge about its application domain. If it is a successful library, then its developer will have put a lot of thought into a suitable and effective API, efficient data structures, problematic corner cases, and so on. Before you could develop anything approaching its utility, you would have to learn as much about the application domain. We hope that these arguments will help you overcome the not-invented-here syndrome.
Place the library objects into your design explicitly.
But how does using a library relate to design? The library is, after all, finished. However, using a library should be an explicit decision of the development team. The team recognizes that a library offers an object that does something obviously useful. They would then write a CRC card for a library object and place it in into their current design. This step fixes one place in the design, and the remainder of the objects must be grouped 11.3.3.3 around this fixed point. In fact, since applications usually use several different libraries, the situation can become more as illustrated in Fig. 11.9: The library objects are beacons that are linked by application objects to create a new overall functionality.
Figure 11.9 Library Objects in the Design
In the example, we recognize that we need to parse the formula that the user has entered. This responsibility was given to FormulaField widget 11.3.3.2 earlier on. However, this does not mean that the developer of the widget 2.3.3 has to write a parser. In fact, we already have a suitable parser from earlier examples. The parser also comes with syntax trees Expr for simple arithmetic expressions, so that the FormulaField can deliver Expr objects.
Now, we come to the point of linking the library into the existing 11.3.3.3 11.3.3.1 design. Our FunGraph takes PlottableFunctions as input, so we will 2.4.1 have to create an ADAPTER to make the objects collaborate. It takes an Expr and implements the interface’sfunValue() method by evaluating the expression.
computation.ExprAdapter
public class ExprAdapter implements PlottableFunction {
private final Expr exp;
public ExprAdapter(Expr exp) {
this.exp = exp;
}
public double funValue(double x) throws Undefined {
Valuation v = new Valuation();
v.set(new VarName("x"), x);
try {
double res = exp.eval(v);
if (Double.isInfinite(res) || Double.isNaN(res))
throw new Undefined("not a number");
return res;
} catch (EvalError exc) {
throw new Undefined(exc.getMessage());
}
}
. . .
}
Avoid letting the library dominate your design.
172(Ch.8) When using a library, there is always the danger that the fixed objects it introduces into the design will dominate the subsequent design decisions. For instance, suppose you write an editor for manipulating XML documents 215 graphically. You use a particular DOM implementation for I/O. If you are not very careful, you will end up with a design in which many application objects access the raw DOM objects representing the single XML nodes, simply because they are there and come in handy. This has several disadvantages:
• You cannot replace the library if it turns out to be buggy, misses important features, or is simply discontinued.
• The DOM objects cannot take on new responsibilities from your application, so that they cannot actively contribute to solving use cases. This restricts your opportunities of useful design.
• 92(“Bad Smells”)Beyond that, your own objects can also become ill designed. They work on someone else’s data in a procedural fashion because they cannot delegate the processing to the library objects.
In the example, we have done the right thing, but for quite a different 11.3.3.1 reason: We have introduced a PlottableFunction because we were not sure what a suitable implementation would be. Now it turns out that this extra work pays off: If we find a better parser or library of arithmetic expressions, we simply have to adapt the FormulaField as the producer, the Expr Adapter as the consumer, and the code that hands the Expr objects between 11.3.3.6 them. But the central and most complex object, FunGraph, remains untouched.
The question arises of whether the FormulaField should provide PlottableFunctions directly, by wrapping them into ExprAdapters immediately after parsing. Then all changes would be confined to FormulaField and its helper ExprAdapter. Doing so would, however, tie the FormulaField to the context of plotting functions. It would not be usable if we wished to display, for instance, a tree view of the formula structure. In the end, there is a decision to be made; the important point is to recognize the decision and make 11.1 it consciously.
The problematic dependency on libraries illustrated here will later lead 11.5.6 170 to the more general Dependency Inversion Principle (DIP).
Avoid cluttering your design with adapters.
Of course, there is a downside to the strategy of keeping your software independent from specific libraries: The ADAPTERs you introduce can easily 2.4.1 clutter your design. You should always be aware that adapters do not contribute any functionality of their own, but merely stitch together different parts of the software. Just as any class must justify its existence by some concrete contribution, so must adapters.
In many cases, the library you choose will be essentially the only one or the best one for a given application domain. It is then not justified to introduce the extra complexity of adapters—you will not switch the library anyway. Just having an adapter because it leads to “decoupling” is usually 12.2 a bad idea; it is just another example of “speculative generality.” 92
If you are using the best or the standard library, the opposite strategy will be more useful: Since the library’s objects reflect a thorough design of its application domain, they can act as guides or anchors in your own design. By placing them into the network of objects early on (Fig. 11.9 on page 607), you are likely to fix important decisions in the right way.
Avoid dragging unnecessarily many libraries into a project.
A second snag in using libraries occurs in larger projects. Developers usually have their favorite libraries for particular application areas. For processing XML, for instance, there are many good libraries available. Also, there are always “cute” solutions to common problems that someone or other has used before. For example, there are compile-time Java extensions for generating getters and setters automatically for fields with special annotations.
With the number of available libraries constantly growing, there is the danger of stalling progress by relying on too many libraries. In such a case, all developers on the team will have to learn all APIs. You will have to keep track of more dependencies and check out updates. And if the developer who introduced his or her pet library into the project leaves, serious trouble can arise.
It is better to aim for consistency and minimality: Using the same solution for the same problem throughout a project’s code base is always a good idea. Apply this principle to libraries as well. Also, for the sake of smoothing the learning curve, it may be better to spell out simple things like generating getters and setters instead of using yet another tool. If another library or tool seems necessary, be conservative, prefer standard solutions to “super-cool” ones, and find a consensus within the team.
11.3.3.6 Tying Things Up
So far, we have introduced objects that distribute among them the work arising in the given use cases. Once the distribution is complete, it is usually necessary to tie together the different parts into a complete whole. Such 1.8.1 objects often fall under the role stereotype of structurers.
Create objects that are responsible only for organizing the work of others.
In the example, the ApplicationWindow creates the overall appearance of Fig. 11.8 by creating and linking the introduced compound widgets. Note how the RegionSelector and FunGraph share the PlotRegion to 9.2.1 propagate the user’s selection, following the MODEL-VIEW-CONTROLLER pattern.
main.ApplicationWindow
public class ApplicationWindow {
. . .
protected void createContents() {
shell = new Shell();
. . .
PlotRegion r = new PlotRegion(-10, 10, -10, 10);
. . .
FunGraph funGraph = new FunGraph(shell);
funGraph.setPlotRegion(r);
. . .
RegionSelector sel = new RegionSelector(entry);
sel.setPlotRegion(r);
. . .
}
}
2.2.1The object ApplicationWindow is the owner of the different parts and links them as it sees fit. Quite a different kind of “tying together” of parts is seen when an object sits in the middle of several objects and links them by actively forwarding messages or triggering behavior as a reaction to messages received. Such an object is a MEDIATOR: It encapsulates the logic 7.7 for connecting other objects.
In the example, we introduce a Mediator object for demonstration purposes. It receives notifications when a new expression has been entered and when a new type of axis has been chosen, in the lower-right corner of Fig. 11.8. (We treat the handling of different axis types later in this section.) 11.3.3.7 In both cases, the mediator passes the new choices to the FunGraph, which will update the display.
main.ApplicationWindow.createContents
Mediator mediator = new Mediator();
. . .
mediator.setFunGraph(funGraph);
. . .
form.addExprListener(mediator);
. . .
ComboViewer chooseAxis = new ComboViewer(entry);
. . .
chooseAxis.addSelectionChangedListener(mediator);
. . .
The mediator is not typical, in that its reactions are somewhat simplistic 100 and information flows in only one direction, toward the FunGraph. Nevertheless, someone needs to organize this flow, so we have introduced the new Mediator object. Also, the forwarding is not completely trivial, but requires some adaptations: A newly selected axis must be extracted 11.3.3.7 from the selection event, and the raw Expr parsed by the FormulaField must be wrapped to obtain a PlottableFunction. 2.4
main.Mediator
public class Mediator implements ExprListener,
ISelectionChangedListener {
private FunGraph funGraph;
public void setFunGraph(FunGraph funGraph) {
this.funGraph = funGraph;
}
public void selectionChanged(SelectionChangedEvent e) {
AxisPainter c = (AxisPainter)
((IStructuredSelection) e.getSelection())
.getFirstElement();
funGraph.setAxisPainter(c);
}
public void exprChanged(
final ExprEvent e) {final Expr exp = e.getExpr();
if (exp != null)
funGraph.setFunction(new ExprAdapter(exp));
else
funGraph.setFunction(null);
}
}
You will have noticed a slight asymmetry in the design: The FunGraph observes the PlotRegion directly, but depends on the Mediator to supply the function and type of axis. Why does it not observe the FormulaField and the ComboViewer? Or conversely, why does the Mediator not also forward the PlotRegion to the FunGraph? The argument 12.1 against the first modification is that the FunGraph becomes more closely coupled to its environment and is less reusable: While it does need a PlottableFunction and anAxisPainter, it does not care where they come from. It accepts them passively, as 1.3.4 parameters to its behavior. The second modification, in contrast, is viable. It would treat the PlotRegion as yet another parameter to the FunGraph. However, there is a different 1.8.4mismatch: The function and the axis are designed as immutable value objects, while the PlotRegion 11.3.3.1 is an active information holder. Passing the region around as a mere value then introduces an inconsistency there.
11.3.3.7 Roles and Changeability
The objects treated previously have had concrete tasks that they fulfilled themselves or delegated to others. We will now venture a bit further afield 3.2.1 and try our hand at (behavioral) abstraction: What if suddenly not one, but several possible objects can fill a place in the overall network and take on the associated responsibilities? In other words, what if we start designing 11.1 211 roles, rather than single objects (see Fig. 11.3 on page 576)? Then we start collecting sets of related responsibilities without deciding immediately which object can fulfill them.
Use roles to keep the concrete implementation of a task exchangeable.
The function plotter contains an instance of such a challenge. The user can switch between different kinds of axes at runtime (see the lower-right corner of Fig. 11.8). Each axis looks slightly different on the screen, but the essential responsibility is clear: to overlay the display area with a coordinate system. Also, we make an axis responsible for describing itself. At the language level, we render a role as an interface, shown in the next code snippet. The getDescription() method is clear, but what are suitable 7.8 parameters to paintAxis()? For technical reasons, we need the graphics context GC, and we also pass the screen area and the plot region, because the axis painter will need them for scaling its drawings. The first parameter fun Graph is introduced because in callbacks it is usually sensible to pass the 12.3.2 context in which they take place.
fungraph.AxisPainter
public interface AxisPainter {
String getDescription();
void paintAxis(FunGraph funGraph, GC g, Rectangle screenRect,
PlotRegion region);
}
We render roles as interfaces because then an object implementing the interface implicitly declares that it will fulfill the associated responsibilities faithfully. The very same promise is expressed by the Liskov Substitution Principle as well as by contract 3.1.1 6.4 inheritance: When an object overrides a method, it must honor the contract associated with that method; that is, it inherits the contract with the method.
The STRATEGY pattern anticipates the design-level concept of roles at the language 1.3.4 level: Because the system contains alternative implementations of some task, we abstract the commonality into a common superclass. In designing roles, we start from the top and first ask what needs doing, before we start wondering who will eventually do it.
Use roles to defer decisions about the concrete implementation.
Roles enable concrete objects to be exchanged later on. This is useful not only if there are different implementations, but also if the only implementation should be exchanged later on. In the example, we were unsure how best 11.3.3.3 to represent a formula. We therefore introduced an interface that captures just the responsibility that the object can somehow evaluate itself.
computation.PlottableFunction
public interface PlottableFunction {
String getDescription();
double funValue(double x) throws Undefined;
}
Roles very often specify only aspects of the object’s overall behavior.
In both cases shown previously, the concrete implementation objects have the only purpose of fulfilling the responsibilities associated with a role. In the majority of cases, however, the concrete object has far more comprehensive responsibilities—those from the role capture only one aspect of the overall behavior. An observer, for instance, is capable of receiving state 2.1 change notifications, but that is not its purpose; in fact, it needs the notifications only as auxiliary information precisely for fulfilling a different purpose.
In fact, it can be useful to start designing roles and then to create objects 211 filling these roles with concrete behavior in a second step. This approach will yield fewer dependencies between the objects so that the overall design remains more flexible and will accommodate changes more easily. We have seen this idea already in the context of client-specific interfaces. A different 3.2.2 perspective is offered by the Dependency Inversion Principle (DIP). 11.5.6
11.3.3.8 Neighborhoods: Subnetworks of Objects
263One further element of responsibility-driven design remains to be discussed: neighborhoods. Very often, single objects are too small as units of design, because many objects have to collaborate to achieve a task of any size. When trying to flesh these objects out from the start, we can easily lose track of the application’s overall structure.
Neighborhoods are groups of objects working on a common task.
Neighborhoods are groups of objects that collaborate closely to achieve a common overall goal. Fig. 11.10 gives the underlying structure. In the central, dashed area, a close-knit network of objects works on a task, while the outer objects consume their service through relatively few well-defined channels.
Figure 11.10 Neighborhoods
Objects collaborate more closely within a neighborhood.
The function plotter example is almost too small to demonstrate neighborhoods properly. However, the group of objects implementing the central plot area in Fig. 11.8 (on page 594) are certainly connected more closely 11.3.3.3 among themselves than with the rest of the system: TheFunGraph uses 11.3.3.7 different AxisPainters for overlaying a grid, a Scaler for mapping coordinates from the screen, and a PlotRegion to maintain the coordinates to be displayed.
The outside world has to know next to nothing about these internal collaborations. The Scaler is package-visible and hidden altogether; the Plot Region and the AxisPainter are merely plugged into the FunGraph object. Furthermore, we can hide the exact nature ofAxisPainters by making their concrete classes package-visible and exposing just the interface. Then, the FunGraph can be asked to provide all possible choices, so that the main ApplicationWindow merely has to plug them into the ComboBox Viewer shown in Fig. 11.8.
main.ApplicationWindow.createContents
ComboViewer chooseAxis = new ComboViewer(entry);
AxisPainter[] axisPainters = funGraph.getAvailableAxisPainters();
chooseAxis.setInput(axisPainters);
Neighborhoods have a common overall purpose.
Neighborhoods lift the idea of responsibilities to groups of objects. Just as the Single Responsibility Principle demands that each individual object has 11.2 a clearly defined purpose, so the objects in a neighborhood taken together have a common, more comprehensive responsibility. In the end, the idea of what a “single” responsibility really is depends on the degree of abstraction 11.2.2 chosen in expressing the responsibility.
Neighborhoods may simplify outside access through designated objects.
Neighborhoods also shield the outside world from the internal complexities of the contained subnetwork of objects. If the objects have few collaborations with the outside world, then the outside world has to understand only these few collaborations and can leave the remaining ones alone.
One step further, a neighborhood may choose to designate special objects through which all communication with the outside world is channeled. The FACADE pattern expresses just this idea: to shield clients of a subsystem 1.7.2 from its internal complexities.
Use modularization to enforce boundaries of neighborhoods.
Encapsulation at the level of objects means hiding the object’s internal data structures. Encapsulation at the level of neighborhoods means hiding the internal object structure. The benefits of encapsulation then transfer to the larger units of design: You can change the internals if you see fit, and it becomes simpler to understand and maintain the overall system.
Encapsulation for object structures is available at many levels. Nested 1.8.8 classes are usually private anyway, and package-visible (default-visible) 1.7 classes and methods can be accessed only from within the same package. OSGi bundles in Eclipse help you further by exposing packages to other A.1 bundles selectively. Also, you may consider publishing only interfaces in 3.2.7 these packages and keeping the implementation hidden away in internal packages. The Eclipse platform uses this device throughout to keep the code base maintainable.
11.3.4 Summing Up
The presentation of the function plotter’s design has focused on the resulting code, because it is this code that needs to be developed and eventually maintained. Now it is time to look back and to evaluate our design: Does it lead to an overall sensible structure?
Fig. 11.11 shows the principal objects in the design. The dashed objects are roles. The arrows with filled heads indicate access or usage. The arrows with open heads, as usual, denote subtyping. The dashed arrows mean that an access takes place, but only through an OBSERVER relationship or event notifications. Both dashed objects and dashed arrows therefore indicate a 12.1 looser kind of relationship that does not entail an immediate dependency.
Figure 11.11 Overall Structure of the Function Plotter
Let us then check our design. The FunGraph implements the main functionality. It relies on several direct helpers: A PlottableFunction to obtain the values to be drawn and an AxisPainter to draw the coordinate grid according to the user’s choice. Both are roles—that is, interfaces at the code level—to keep the core functionality flexible. The Plot Region, in contrast, is an integral asset for drawing functions, so it is a concrete class. However, the actual drawing is insulated from the challenges of coordinate transformations by accessing the PlotRegionthrough a Scaler. There are two choices of coordinate grids, both of which also access the Plot Region through a Scaler.
The remainder of the user interface components are grouped around the FunGraph. A RegionSelector contains the widgets to manipulate a Plot Region. Any changes are sent directly to FunGraph as notifications. This arrangement follows model-view separation, with thePlotRegion as the model. However, the RegionSelector is not a full view, because it does not listen to changes of the PlotRegion. This could, however, be accomplished easily if necessary.
The ComboBox for choosing the coordinate grid and the FormulaField are handled differently. Both report their values to a Mediator, which decides 1.3.4 what to do with them. In this case, it parameterizes the FunGraph.
The parsing of formulas, because of its complexity and a preexisting library, is isolated in one “corner” of the design: Only the FormulaField knows about parsing, and only the Mediator knows that behind a PlottableFunction, there is actually just a simple Expr object.
This summary is actually more than a convenience for the reader: Whenever you finish a design, it is useful to try to tell the overall story, to check whether the reasoning makes sense when told in a few words. If you are able to accomplish this, then the design is simple enough and understandable enough to be actually implemented.
11.4 Responsibilities and Hierarchy
Hierarchy and abstraction are at the heart of object-oriented programming. We have already examined at the language level the fundamental principles 3.1.1 governing the use of inheritance and interfaces. The Liskov Substitution Principle requires that an object of a subtype—a derived class or a class implementing a given interface—can be used in all places where the super-type 3.2.1 is expected. Behavioral abstraction complements this with the idea that superclasses and interfaces usually specify general reactions whose details 6.4 can be implemented in different ways. Finally, the idea of contract inheritance makes these concepts precise by relating them to the pre- and post-conditions of individual methods.
It now remains to ask how hierarchy relates to responsibilities. The connection, of course, rests on the fact that responsibilities always specify aspects of an object’s behavior and that hierarchy is one way of structuring this behavior. In expressing the earlier principles in terms of responsibilities, we will find that the new terminology also offers a new perspective on the previous ideas.
Before we start, we emphasize that inheritance and subtyping are not central to the idea of responsibilities. In fact, responsibility-driven design asks only what objects do and deliberately ignores how they have been 1.4.1 created: It is irrelevant whether a method’s implementation is inherited or whether we need to introduce and implement certain interfaces to satisfy the compiler that a given method call is legal. Responsibilities help us free our minds from these implementation details to focus on the design itself.
All classes and interfaces have responsibilities.
All classes have responsibilities, including those used as base classes. Likewise, all interfaces can be assigned responsibilities, because in the end they must be implemented by objects—an interface is merely a restricted way of accessing an object. As pointed out earlier, responsibilities relate to the concrete objects, independent of the technical questions of inheritance and subtyping.
Classes take on the responsibilities of their super-types.
The Liskov Substitution Principle demands that clients can work with subclasses where they expect one of their super-types. This means that a class must take on all responsibilities of its super-types: When a client looks through the responsibilities of a super-type, it expects that the concrete6.4 object will fulfill them. This is analogous to contract inheritance, so that we could say that responsibilities are inherited, too.
For instance, every SWT Control must notify interested listeners about 7.8 mouse movements, so a Canvas, which is used for custom widgets, must report mouse movements as well.
Roles capture behavioral abstraction.
11.1We said earlier that roles are usually rendered as interfaces at the implementation level. Interfaces serve many different purposes, among them 3.2.1 1.4.1 behavioral abstraction: The precise reaction of an object to an incoming message is left unspecified, so that each concrete class is free to react suitably.
Roles, as collections of responsibilities, can also exhibit this kind of abstraction. All that is necessary is to phrase the responsibilities in general, abstract terms. For instance, an AxisPainter in the function plotter example 11.3.3.7 is responsible for painting a grid over the function graph, but that job description is so abstract that it allows for different concrete interpretations.
Even without deliberate generalization in the phrasing of responsibilities, roles should always be understood as behavioral abstraction. The intention 11.1 of roles is that many different objects will fill them, so that it is usually not safe to presume any concrete reaction.
Inheritance allows for partially implemented responsibilities.
We saw in Part I that it is quite common for a class to leave some of its methods unimplemented. A class might leave the definition to the subclasses 1.4.10 altogether, or it might leave out specific steps in a mechanism through TEMPLATE METHOD. In both cases, the superclass fixes responsibilities, 1.4.9 but delegates their concrete implementation to the subclass.
Inheritance can structure responsibilities.
Of course, a subclass can also take on new responsibilities quite apart from those of its superclass: An SWT Text input field enables the user to enter text; a Button presents a clickable surface. Both show suitable indications of mouse movements and keyboard focus. None of this, however, belongs to the responsibilities of their common superclass Control.
With each level of the inheritance hierarchy, new responsibilities can be stacked upon the old ones from the superclass. Inheritance therefore helps to structure a class’s responsibilities further, with the intention of sharing the responsibilities that are introduced higher up among many different classes.
Protected methods introduce hidden responsibilities.
Protected methods enable a special form of collaboration between a subclass 3.1.2 and its superclass. On the one hand, the superclass can offer services 1.4.8.2 that only its subclasses can use. On the other hand, through method overriding 1.4.11 and abstract methods, the superclass can request services from its 1.4.9 subclasses. Taken together, these mechanisms enable classes along the inheritance chain to have “private” responsibilities that are not visible to other objects.
Inheritance can be used for splitting responsibilities.
Taken together, the three previous points show the power of inheritance: It enables us to split the implementation of responsibilities of a single object into separate pieces of source code. At each step, the responsibilities are 11.2 self-contained and, ideally, small. As a result, the overall code base becomes more understandable, maintainable, and possibly reusable.
11.1A larger example can be seen in the context of the class NewWizard. It uses a NewWizardRegistry to gather supported file types from the different plugins. That class is built in two inheritance steps, because different kinds of wizards are registered within the system. TheAbstractWizard Registry simply keeps registered wizards in a list. Its subclass Abstract ExtensionWizardRegistry adds the responsibility of gathering these, but 12.3.3 delegates the choice of the the type of wizard and their source extension point to its subclasses. TheNewWizardRegistry then takes on this remaining responsibility.
Be precise despite the abstraction.
Inheritance and hierarchy usually involve abstraction: The super-types specify responsibilities that comprise more cases, because they are implemented differently in the different subtypes. It is important not to confuse “abstract” with “vague” or “imprecise”: If an abstract responsibility is imprecise, then it is unclear whether a subtype validates the Liskov Substitution Principle and whether clients can rely on its behavior. The formulation of a responsibility 6.4 must always be precise enough to allow precise reasoning about whether a subtype fulfills it properly.
11.5 Fundamental Goals and Strategies
It is really hard to say what makes a good designer and good designs. Of course, designs should be simple and easy to implement; they should follow established conventions and patterns; they should accommodate changes in requirements gracefully, simplify ports to different platforms, and so on. At a personal level, the designers should be able to communicate with different stakeholders; they should have a clear grasp of the strategic goals and overall architecture to make informed decisions; and so on. But these criteria are all rather abstract, and their application may depend in many cases on personal preferences and biases, as well as on one’s own previous design experience.
Conversely, it is quite straightforward to say what makes a bad designer. 92(“Bad Smells”) Bad designers ignore the fundamental rules of their trade: They let objects access each other’s data structures; they have no clear idea of what each object is supposed to do; their methods can be understood only by reading through the code; and they commit a multitude of other sins. In short, they create designs that result in completely unreadable, throwaway implementations.
This section gathers several fundamental requirements related to design. They have appeared as specific aspects and arguments in previous discussions, but now we switch perspective and put them at center stage. We also add more conceptual background, which would have been misplaced in the concrete previous settings.
11.5.1 Information Hiding and Encapsulation
The most fundamental principle of any software design—not just object-oriented design—is certainly information hiding. The size and complexity of 205 modern software simply demands that we split up the implementation into chunks, or modules, which can be worked on independently by different team members or different teams. This is possible only if each team can focus on its own contribution and does not have to know the technical details of the other teams’ modules. The information about what goes on in each module is hidden from the world at large.
Information hiding goes beyond encapsulation.
Information hiding is not simply another name for encapsulation. Encapsulation 216(§7.4,§7.6) 1.1 is a language-level feature that controls access to part of a module’s definition. For instance, the Java compiler will throw an error when you try to access an object’s private fields from outside its own class. That is certainly very useful, in particular if some other team includes a tech guru who likes to tinker with other people’s data structures.
Information hiding means much more than just making an object’s internals inaccessible. It means restricting the knowledge that other teams may have—or need to have—about your objects. So the information hidden is not the object’s data, but your own team’s information about why and how the object works.
Few software engineers, and indeed only a small part of the literature, will be so severe with their distinction between information hiding and encapsulation. Often the terms are used interchangeably, or encapsulation is used to encompass information hiding, because encapsulation, as we will see, is not useful on its own. So when someone says to you, “I have encapsulated this data structure,” that statement usually implies “You’re not supposed to know I used it at all” and “You can’t hold me responsible if I trash the code tomorrow and do something entirely different.” We make the distinction here mostly to highlight the extra effort a professional developer has to spend beyond making fields private. We will use “information hiding” when discussing design questions and “encapsulation” when discussing the implementation’s object structures.
Suppose, for instance, that your team is responsible for your software’s AuthenticationService, as shown next. The method authenticate() enables clients to check whether the given user can log on with the given password. Sometimes, other modules may need a complete list of users—for instance, to display them to administrators. It is OK to have a method get AllUsers() for this purpose:
public class AuthenticationService {
private List<User> users = new ArrayList();
public boolean authenticate(String user, String password) {
. . .
}
public User[] getAllUsers() {
return users.toArray(new User[users.size()]);
}
. . .
}