How to Use Objects: Code and Concepts (2016)
Part IV: Responsibility-Driven Design
Chapter 12. Design Strategies
Design is a challenging activity: We invent objects, distribute responsibilities, and define collaborations to solve a given real-world problem in software. In the process, we must make a vast number of decisions, each of which influences the outcome of the project: how well we fulfill the users’ expectations; how quickly we can deliver the software; how much effort we have to spend; how easily the software can be adapted to new requirements; how long the software will remain usable; and maybe even whether the project is a success or a failure.
In the end, there is almost never a single best answer to the design questions that arise in this process. We have to make do with heuristics and often have to be content with steering in the right direction. We are always striving for the “best” design, but at the same time for the “simplest thing that could possibly work.” We need to get the software finished quickly now, but we must also think ahead about later modifications. We might even be building only the first product from a larger product family.
To meet these challenges, we need a language for talking about designs as well as criteria for judging them to be “good” or “bad.” And we need strategies that suggest routes that are likely to lead to good designs.
All of these strategies and judgements ultimately have to be measured against the resulting code: It is the code that must be developed, modified, maintained, and reused. It is the coding that consumes a project’s resources and makes the software affordable or expensive, which makes it a throwaway bulk of nonsense or a neat, well-structured, maintainable piece of work. One important difference between expert designers and novice designers is that the experts have seen and solved so many problems in concrete code that they can predict the outcomes of their design decisions with some accuracy.
This chapter approaches the question of design strategies by focusing on the goals to be met: How can we achieve flexible solutions that allow modifications throughout the software’s lifetime? How can we ensure that new functionality can be integrated with as little effort as possible? How can we build reusable components to reduce the overall cost of software development within our team or company? Each of these areas will be treated from the conceptual structure to the resulting code, using examples from the Eclipse platform. Indeed, analyzing and imitating the work of expert designers is the best way to become an expert oneself.
As we progress through the material, you will notice that we have actually discussed many of the conceptual considerations before. They have served to explain and to justify the language usage in Part I, they have guided the quest for precise contracts in Part II, and they have formed the basis of event-driven programming and model-view separation in Part III. Now, we set these individual points within the larger context of design.
12.1 Coupling and Cohesion
How do we recognize “good” design and why is a certain design “good” or “bad” in the first place? In the end, good design leads to code with desirable properties—code that is, for instance, changeable, maintainable, understandable, portable, reusable, and testable. But this is not very helpful: Once the implementation is ready, there is no use in talking about the design—if the design was “good,” we are done; if it was “bad,” there is nothing we can do about it. We need criteria for evaluating the design beforehand so that we can predict the properties of the resulting code.
Perhaps the most influential criteria for evaluating designs are coupling 236 and cohesion. They were introduced early on in the study of “good” software structures and have proved helpful ever since. Briefly speaking, they address the central aspect of how “connected” different pieces code in a system are, where “connected” means that the pieces must be understood and maintained together. Coupling then captures the connectedness between modules, while cohesion captures connectedness within a module.
While this may sound rather straightforward, it turns out to be a challenge to define coupling and cohesion precisely and to apply them to concrete 223,116 designs. Especially for coupling, the experts of the field will come up with so many facets that it seems implausible that one could ever find a one-sentence summary. We will first examine the agreed characterization of coupling as a factor that limits changeability: Software that is not changeable cannot be maintained, ported, and reused. Next, we take a conceptual step beyond and ask what brings about the coupling, besides the obvious technical dependencies. Then, we turn to the question of cohesion, before discussing the Law of Demeter, a helpful heuristic that limits coupling.
12.1.1 Coupling and Change
Changeability is a central property of software: We may get the requirements wrong, the requirements may change, or the context in which the software runs may change. In any case, our users expect us to come up with a cost-effective modification to our software so that it fits the new expectations.
Coupling describes the necessity of induced changes.
Coupling can be defined most easily by the necessity of propagating changes [Fig. 12.1(a)]: Component A depends on B—for instance, because A calls one of B’s methods. It is also coupled to B if a change in B requires us to change A as well, as indicated by the shaded areas.
Figure 12.1 Coupling as Propagated Change
Coupling is problematic precisely because we will always make changes to our software. During development, we might remedy problems with earlier design or implementation decisions. After deployment, we must integrate new functionality or adapt existing points. Coupling then leads to an increase in both work and development costs.
The most basic form of coupling is easily recognized at the technical level. For instance, A is coupled to B in the following situations:
• A references a type name defined in B.
• A calls a method defined in B.
• A accesses a field or data structure from B.
• A instantiates a class defined in B.
• A extends a class or implements an interface from B.
• A overrides a method introduced in B. 12.2.1.2
Whenever B changes the aspect that A references, then A has to change as well. For example, if a type name changes, all references must change; if a method gets a different name or different parameters, all calls must be adapted; and so on.
Coupling can occur through a shared third.
Coupling can also occur without a direct dependency between the modules in question. In Fig. 12.1(b), both A and B use a module C—for instance, as a service provider. Now some change in B might result in C being no longer good enough, so that it must be changed. In turn, this change may ripple through to A.
227 A special form of indirect coupling is stamp coupling. Here, both A and B use a common data structure C for communication. However, the data structure is not made for the purpose, as would be usual in passing parameters to methods. The data structure instead contains a number of aspects that are irrelevant to A and B. As a result, both modules become coupled to this fixed entity, and as a result to each other.
Dependencies do not always lead to strong coupling.
Of course, dependencies do not always lead to undesirable coupling 11.5.1 [Fig. 12.1(c)]. If information hiding is taken seriously in the design of B, then some changes can be made to B without touching A. For instance, B’s internal data structures, held in its fields and never exposed through public (or protected) methods, can usually be exchanged without A noticing.
Loose coupling means insulation from change.
In principle, then, any dependency might induce coupling, but some dependencies are more acceptable than others. The term loose coupling is used to describe situations such as in Fig. 12.1(c), where the dependency is designed such that we can get away with many changes to B without changing the remainder of the system. Decoupling then means breaking an existing coupling or preventing it from occurring.
11.5.1 However, loose coupling is not the same as information hiding, which also aims at enabling change. Information hiding describes the bare necessity of hiding internals, usually at the level of individual objects. Loose coupling concerns the design of the interface of objects and subsystems so that as many changes as possible can be made without changing the interface, 4.1 and without adapting the specified method contracts. To be effective, loose coupling must usually be considered at the architectural level, when planning the overall system and its components.
12.2.1.4 11.5.6 One strategy for achieving loose coupling has been seen in the Dependency Inversion Principle (see Fig. 11.12 on page 635). Rather than making one subsystem A depend on the concrete implementation classes of another 3.2.7 subsystem B, we introduce an interface between them that captures the expectations of A. As a result, B is free to change as much as possible; all that is required is that it still fulfills A’s expectations.
Observer relationships are a standard means of decoupling.
12.2.1.8One special case of achieving loose coupling is to channel collaboration 2.1 through the OBSERVER pattern. The subject remains entirely independent 2.1.2 of its observers, as long as the observer interface is, indeed, defined from the possible state changes to the subject.
12.2.1.9 93 A slightly more general view is to let an object define events that it passes to any interested parties. It does not care about these other objects at all; it merely tells them about important occurrences within its own life cycle.
Many architectural patterns are geared toward achieving loose coupling.
Since loose coupling is such an important goal in system design, it is hardly surprising that many architectural patterns achieve it in particular 59,101 collaborations.
For instance, the MODEL-VIEW-CONTROLLER pattern insulates the 9.1 model from changes in the view. But the pattern does more: It also decouples the different views from each other, because each one queries, observes, and modifies the model independently. As a result, any individual view, as well as the overall application window, can change without breaking existing views. Note also that the essential decoupling step rests on the observer 9.2.3 relation between the model and the view, as suggested earlier.
The PRESENTATION-ABSTRACTION-CONTROL (PAC) pattern decouples 59 different elements of the user interface further by making them communicate through messages. In the LAYERS pattern, each layer insulates the 12.2.2 higher ones from any changes in the lower ones; at the same time, each layer remains independent of its higher layers. The PIPES-AND-FILTERS 12.3.4 pattern keeps individual data processing steps independent of both the other processing steps and the overall system structure.
Decoupling involves an effort.
Fig. 12.1(c) on page 639 is usually a too naive view: It assumes that B is already defined such that A remains loosely coupled. Very often, this is not the case. Decoupling must be achieved explicitly—for instance, through FACADE objects or through ADAPTERs that translate requests. In this context, 12.2.1.6 1.7.2 2.4.1 a mapper is an adapter that translates requests in both directions. In 93 general, such constructs are instances of shields; in other words, they are 225 software artifacts that are introduced with the sole purpose of decoupling different objects or subsystems.
In reality, decoupling by shields requires some infrastructure (Fig. 12.2). For A to access B in a loosely coupled manner, we have to define an interface I behind which we hide the possible changes. Then, we introduce an adapter (or mapper) C to actually implement the interface based on an existing B. The adapter also acts as the hinge that keeps A and B connected despite possible changes to B; these lead to changes in C, and only in C.
Figure 12.2 Shields for Decoupling
Beyond this, the subsystems A and B themselves will probably have to be more general, and therefore more complex, to communicate effectively through the interface I. As an example, decoupling the model from the view in the MODEL-VIEW-CONTROLLER pattern can involve substantial work to 9.4.3 make repainting efficient. The work results from the necessity to translate model changes to screen changes, and vice versa, which could have been omitted if the model itself could contain the screen area associated with each model element.
Use decoupling to keep subsystems stand-alone.
You may ask, of course: If we know that B in Fig. 12.2 will have to implement I, why does it not do so directly? For one thing, it might be the case that B was created long before A and its special requirements were simply 11.3.3.5 unknown at that time. For instance, B might be a library or subsystem taken over from a different project.
But even if B and A are designed at the same time, it is often sensible to keep B independent of A, because it enables B to remain self-contained and stand-alone. One good reason is that the team working on B remains free to choose the most sensible API from B’s point of view. B can have 11.2 a well-defined purpose and can be implemented and tested effectively. The 11.5.2 underlying strategy here is simply Separation of Concerns. Or perhaps the interface I is not stable, because the demands of A are not known in all details. Getting B right and tested will then at least provide a reliable collaborator as early as possible.
Of course, you may also be planning to reuse B in a different project 12.2.1.5 3.2.7 later on. In such cases, it is often useful to decouple subsystems A and B to keep them independent and stand-alone. The additional effort involved in creating the structure will pay off with the first restructuring of the system that becomes necessary.
Decoupling can be dangerous.
12.3 12.4 Loose coupling, like extensibility and reusability, often serves as an excuse 92 for speculative generality. For example, creating the situation in Fig. 12.2 involves defining a nice, minimal interface I as well as the natural API for B, both of which can be intellectually rewarding challenges. The temptation of decoupling components for no other reason than decoupling them can be great at times. However, if no changes are likely to occur, then the effort of decoupling is spent in vain.
Beyond that, decoupling involves the danger of getting the interface I in Fig. 12.2 wrong. Within a system, you might simply forget to pass some parameter to a method; that is easily changed by Eclipse’s refactoring tool Change Method Signature. Between systems, I would be a protocol, and changing it would require modifying the communication components on both ends. In any case, I must fulfill three roles at once: It must capture A’s requirements; it must be implementable by B; and it must anticipate the likely changes to B from which A is supposed to be insulated. If you get the interface I wrong, the effort devoted to decoupling is lost. Also, changing I and C probably involves more effort than changing A would have required in a straightforward, strongly coupled connection from A to B.
12.1.2 Coupling and Shared Assumptions
The accepted characterization of coupling through changes from the previous 12.1.1 section leads to a practical problem: How do you judge which changes are likely to occur, and how do you evaluate whether the changes will ripple through the system or will be “stopped” by some suitably defined API between modules? It takes a lot of experience to get anywhere near the right answers to those questions.
We have found in teaching that a different characterization can be helpful to grasp the overall concept of coupling. As we shall see, it links back to the original notion in the end.
Coupling arises from shared assumptions.
To illustrate, let us look at a typical example. The Java Model of the Eclipse Java tooling maintains a tree structure of the sources and binary libraries that the user is currently working with. The Java Model provides the data 9.1 behind the Package Explorer, Outline, and Navigator views and it is used for many source manipulations. The implementation hierarchy below Java Element 3.1.5 contains all sorts of details about caching and lazy analysis of sources and libraries. In an application of the BRIDGE pattern, a corresponding 12.2.1.5 interface hierarchy belowIJavaElement shields other modules 3.2.3 from these details and exposes only those aspects that derive directly from the Java language, such as packages, compilation units, fields, and methods. Certainly, this is a prototypical example of decoupling.
Now let us look a bit closer. For instance, an IMethod represents some method found in the sources and libraries. It has the obvious properties: a name, some parameters, and a return type. It also links back to the containing type declaration (i.e., the parent node in the tree), as is common with compound objects. But then there is one oddity: The return type is a 2.2 String. What is that supposed to mean? Why don’t we get the type’s tree 209,2 structure?
org.eclipse.jdt.core.IMethod
public interface IMethod extends IMember, IAnnotatable {
IType getDeclaringType();
String getElementName();
ILocalVariable[] getParameters() throws JavaModelException;
String getReturnType() throws JavaModelException;
...
}
Let us check whether the API is at least consistent. Sure enough, the ILocal Variables of the parameters also return their types as Strings!
org.eclipse.jdt.core.ILocalVariable
public interface ILocalVariable
extends IJavaElement, ISourceReference, IAnnotatable {
String getElementName();
String getTypeSignature();
...
}
When we look at calls to these odd methods, we see that clients usually analyze the results immediately using some helper class, usually Signature or BinaryTypeConverter.
111 The explanation is simple: The Java Language Specification defines a neat, compact format for encoding Java types as strings. This format is also used in class files so it is easily extracted from the probably large libraries maintained in the Java Model. Because the Java Model must represent so 1.1 many types, Amdahl’s law suggests optimizing the storage requirements.
But let us see what effect this decision has on the classes involved (Fig. 12.3): All of them work on the common assumption that types are represented as binary signatures. The assumption ties them together.
Figure 12.3 Coupling as Shared Assumptions
We might think about replacing the word “assumption” with “knowledge.” This strengthens the intuition that modules are coupled if their developers have to share some piece of knowledge or information for everything to work out. We have decided on “assumption” because “knowledge” implies some certainty, while “assumption” conveys 28 the idea that software is very much a fluid construct, in which we make a decision today only to find we have to revise it tomorrow.
Shared assumptions imply coupling-as-changes.
Coupling-as-assumptions really means taking a step back from coupling-as-changes: If any of the classes in Fig. 12.3 were to deviate from the common assumption that types are handled as in binary signatures, the other modules would have to follow suit to keep the API consistent.
Watch out for any assumptions shared between modules.
The benefit of focusing on shared assumptions is that developers are very familiar with assumptions about their software. They talk about the representation of data passed between modules, stored in files, and sent over the network. They think about invariants within objects, across shared objects, 4.1 6.2.3 and within the inheritance hierarchy. They think about their objects’ 6.4.2 10.1 states, and about the allowed sequences of method calls. All of these details can lead to rippling changes, as you will have experienced in many painful debugging sessions.
In the end, coupling can be recognized, and possibly also tamed, even by modestly experienced developers. All that is required is that the developers become aware of their own assumptions. This does take a lot of discipline, 11.1 but much less experience than anticipating the necessity and likelihood of changes. It is similar to learning contracts, which involves describing 4.1 the program state at specific points in detail, rather than executing code mentally.
Shared assumptions shape the contracts between modules.
Another nice result of tracking assumptions is that the assumptions usually translate to code artifacts sooner or later. That is what traceability is 11.5.4 all about: You express in code the decisions you make during the design phase. In particular, explicit assumptions help you define APIs that the client modules can understand and employ easily. If the assumptions are reasonable and you do not blunder in expressing them in concrete method headers, the method headers will be reasonable as well.
Shared assumptions help to distinguish between good and bad coupling.
Coupling is often seen as a measure of how “bad” a design is. Yet coupling 223 is also necessary: Without it, there can be no collaboration. But why are some forms of coupling “tight” and therefore bad, whereas others are “loose” or “normal” and therefore acceptable?
Looking at the nature of shared assumptions can help to make that distinction. If the assumptions shared between two modules are obvious 236(p. 119) and unavoidable to reach the goal of collaborating, the resulting coupling is acceptable. If they are obscure and motivated by technical detail, the coupling should be avoided.
This approach also links back to the Precondition Availability Principle 4.2.2 from design-by-contract: The contract must always be specified in terms that the clients can understand. It is precisely the assumptions shared by client and service provider that enable the client to make sense of the contract.
Decoupling means hiding assumptions.
Finally, thinking and talking about assumptions explicitly will also help to understand decoupling. Once you recognize an assumption that couples modules, you can start thinking about an API that is independent of that assumption and that would remain stable if you were to break the assumption in the future.
In the previous example of the Java Model API (Fig. 12.3 on page 644), the assumption was that types are represented and passed around as primitive Strings. We could apply a standard technique and wrap that data in objects of a new class JavaType. The API would become cleaner, as would the clients: JavaType could offer methods for analyzing and manipulating types, delegating the actual work to the existing helpers like Signature.
Coupling-as-assumptions can be traced through the literature.
Once you start to pay attention, you will find the idea of assumptions mentioned in connection with coupling in many places in the literature. However, this mostly happens by way of asides and informal introduction.
236(p.119) Here are a few examples. In their seminal work, Stevens et al. see a large amount of shared information as a threat to good interface design: “The complexity of an interface is a matter of how much information is needed to state or to understand the connection. Thus, obvious relationships result 223 in lower coupling than obscure or inferred ones.” Assumptions that lead to coupling can also be made about the software’s application context: “For example, a medical application may be coupled to the assumption that a typical human has two arms because it asks the doc[tor] to inspect both arms 54,68,51 of [a] patient.” A related classical insight, known as Conway’s law, states that organizations tend to structure their software along their own structures: Departments and teams specializing in particular fields are assigned the modules matching their expertise. Decoupling happens by minimizing 26 the need for information interchange between teams. A recent proposal to quantify coupling measures “semantic similarity” through terminology contained in source names. It interprets this as an indication of how much domain knowledge is shared between different modules.
In short, the experts in the field are well aware of the roles that shared, and often implicit, assumptions play in the creation of coupling between modules. To become an expert, start paying attention to your own assumptions.
12.1.3 Cohesion
Good modularization is characterized on the one hand by loose coupling and 236 on the other hand by high cohesion. While coupling describes relationships between modules, cohesion is a property of the individual modules.
Cohesion expresses that a module’s aspects are logically connected.
When trying to understand or maintain a module from any sizable piece of software, we usually ask first: What does the module contribute? What 11.1 are its responsibilities? The Single Responsibility Principle states that the 11.2 design is good if there is a short answer.
The idea of cohesion takes the idea from classes to modules, which do not usually have a single responsibility, but a number of related ones. Cohesion then states that the different responsibilities are closely related to one another. In the end, one might even be able to come up with a more abstract description of the module that describes a “single” task. However, the abstraction level would be too high to be useful, so it is better to keep several smaller, highly cohesive responsibilities.
For instance, Eclipse’s Java Model maintains the structure of the sources and libraries visible in the current workspace. Its responsibilities include representing the structure, parsing sources down to method declarations, indexing and searching for elements, tracking dependencies, generating code and modifying sources, reacting to changes in files and in-memory working copies, and several more. All of these are closely related, so it is sensible to implement them in one module.
There are other perceptions of high cohesion within a module. The module is stand-alone and self-contained. It is meaningful in itself. It captures a well-defined concept. These intuitions can help you recognize cohesion, or to diagnose problems in modules that lack cohesion.
Cohesion causes correlated changes.
We have characterized coupling by the effects of changes on other modules. Cohesion is often similar: Since the different responsibilities of a module are strongly related to each other, it is likely that changes in one place of the implementation will require other places to be touched as well. Although this sounds rather undesirable, the problem is outweighed by the possibility of sharing the implementation of the different aspects—for instance, through accessing common data structures and helper methods.
In fact, one viable heuristic for modularization is to place things that are likely to change together in a single module. For instance, a device driver module contains all the code that is likely to change when the hardware is switched. In contrast, any higher-level functionality that builds on the primitive data obtained through the device driver is better handled somewhere else, because then it can remain untouched even if new hardware is used.
Cohesion is not related inversely to coupling.
Even though coupling and cohesion are usually mentioned together as principles for modularization, and both can be connected to the necessity of changes to the software, they are complementary and orthogonal rather than linked. Low coupling does not make for high cohesion, and vice versa: It is obviously easy to scatter logically related functionality throughout the system and tie it together through loosely coupled collaboration. In the other direction, having a module with high cohesion does not say anything about its integration into the system. In the end, both properties must be achieved independently.
See cohesion and Separation of Concerns as opposite forces.
Even if loose coupling is not the inverse of high cohesion, it is useful to look further in this direction. Very often in design, we have to decide whether two pieces of functionality should reside in the same module (or class, component, subsystem, ...), or whether they are better split across different modules.
Cohesion can be seen as a force drawing the pieces of functionality closer together, into one module, if they are closely related [Fig. 12.4(a)]. 11.5.2 In contrast, Separation of Concerns suggests that two distinct features, even if they are related and their implementations must collaborate, are better placed in distinct modules [Fig. 12.4(b)]. The opposite force to high cohesion then is not loose coupling, but Separation of Concerns. If we have decided on a split, loose coupling tells us how the modules should interact to keep them changeable.
Figure 12.4 Cohesion and Separation of Concerns as Forces
An example is found in the MODEL-VIEW-CONTROLLER pattern. The 9.2.1 View and the Controller have quite distinct responsibilities: One renders data on the screen, and the other decides about the appropriate reaction to user input. Separation of Concerns suggests making them into separate classes, as done in the classical pattern. At the same time, both are linked tightly to the concrete display, because the Controller must usually interpret mouse gestures relative to the display and must often trigger feedback. This cohesion of being logically concerned with the same display suggests placing them in a single class—an arrangement that is found frequently, namely in the form of the DOCUMENT-VIEW pattern. 9.2.8
Tight coupling may indicate the potential for high cohesion.
We can gain a second perspective on the question of splitting tasks between separate modules by considering the resulting coupling. If distributing tasks would result in tight coupling, then it might be better to place the tasks into the same module. It is then just possible that the tasks have so much in common that they exhibit some cohesion as well.
For instance, the NewClassWizardPage from the introductory example 11.1 in Chapter 11 provides a form to let the user enter the parameters of a new class to be created. It also creates the source of the new class. If we decide to split these tasks, as suggested by model-view separation, then the two are tightly coupled: Any change in the available options would require a change to the code generator, and in many cases vice versa. It is better to say that their common logical core, which lets the user create a new class, leads to sufficiently high cohesion to implement both tasks in a single class.
12.1.4 The Law of Demeter
Pinning down the notion of “tight coupling” precisely can be rather challenging. Since designers may have different ideas about expected changes, they may arrive at different evaluations of whether some link between objects is too “tight.” An influential heuristic is the Law of Demeter, named 158 172 after the Demeter development environment in which it was first introduced and supported. Its motivation is depicted in Fig. 12.5(a). Suppose object A needs some functionality available in object D. It invokes getters to follow 1.3.3 the dashed path through the application’s object structure and then uses D’s methods. The problem is that now A depends on the overall network and the structure of all traversed objects, so that these structures cannot be changed without breaking A. Such accessor sequences are also called 172 “train wrecks,” because in the code they resemble long strings of carriages. More strictly speaking, the law also excludes passing on references to internal parts [Fig. 12.5(b); repeated here from Fig. 1.1]. It then does nothing else 1.1 but reinforce the idea of encapsulation in compound objects. 2.2.3
Figure 12.5 Motivation for the Law of Demeter
Objects may work only with collaborators that have been made available to them explicitly.
The Law of Demeter is formulated to rule out such undesirable code, by defining a restricted set of objects with methods that a given method may 158 safely invoke. The original presentation calls these “preferred suppliers” (i.e., service providers). More precisely, the Law of Demeter states that a method may access only the following objects:
1. this
2. 2.2 The objects stored in fields of this (i.e., its immediate collaborators and parts)
3. The arguments passed to the method
4. The objects created in the method
5. 1.3.8 Objects available globally—for example, as SINGLETONs
The overall intention is that an object may work with those collaborators that have explicitly been made available to it.
Note that objects returned from invoked methods are not included in the list of preferred suppliers. These objects are “fetched by force,” not made available. As a result, the problematic getter sequences are ruled out.
The Law of Demeter leads to encapsulation of object structures.
2.2.3One way of looking at the Law of Demeter is to say that it provides an extended form of encapsulation. In Fig. 12.5(a), we wish to hide the existence 11.5.1 of C from A. We hide information with the result that we can later change the object structure.
Information hiding within the individual objects is, of course, an orthogonal issue. In principle, compound objects encapsulate their internal 2.2.3 helpers. The Law of Demeter goes beyond this in also applying to shared objects, such C in Fig. 12.5(a), which is shared between B and E.
The Law of Demeter encourages use of proper objects, rather than data structures.
The Law of Demeter forces developers to think of objects as service providers, 1.8.2 rather than mere data structures that keep references to others. In 1.8.3 Fig. 12.5(a), developers must design B in such a way that it is a useful collaborator to A, without exposing its own collaborators. B will therefore 1.1 have clearly outlined responsibilities, ideally fulfilling the Single Responsibility 11.2 Principle. It will most probably also be active and will aggregate services from its collaborators, rather than just delegating individual method calls.
Read the Law of Demeter as a guideline, not as a strict law.
The Law of Demeter is actually quite controversial, even if it is often cited. 224 Already the original authors have recognized that following the law strictly 158 has several undesirable consequences. Most important, since A is not allowed to reach beyond B in Fig. 12.5(a), B must provide all the functionality that A requires from C and D. Likewise, C must provide the functionality that B requires from D. In consequence, B and C have much larger interfaces than would be expected from their own purposes, and the objects no 11.2 1.1 longer focus on their own tasks.
The purpose of B might also be the selection of a suitable collaborator for A. In terms of role stereotypes, it might be a controller or a structurer. 1.8.1 It might also be a factory for objects that hides the creation itself, but 1.4.12 does not make any decisions. In all such cases it is, of course, mandatory to break the Law of Demeter.
To make this point clearer, let us look at an example where breaking the law actually achieves loose coupling. Eclipse contains many different kinds of editors, many of which share common abstract 'font-size:10.0pt; font-family:"Times New Roman",serif;color:black'>12.2.1.9 deal with error markers, and so on. Since not all editors provide all aspects and their implementations will differ, Eclipse uses the EXTENSION 3.2.2 OBJECTS pattern: Interested objects access an aspect of behavior through a generic getAdapter() method, specifying an interface for the kind of object requested. This induces loose coupling, because each client gets exactly the minimal interface that it requires. Sometimes the returned objects 12.1.1 will already be part of the editor’s infrastructure, and sometimes they are small adapters for accessing the editor’s internal structures (see Fig. 12.2 on page 641).
12.2 Designing for Flexibility
The first goal of any software design must be to get the system to work, spending as little effort on the implementation as possible. Next in line, the second goal must be to keep the implementation changeable and flexible. During development, it is common to find that the design or implementation is not working out exactly as planned or that requirements were misunderstood in the first place. After deployment, new requirements also keep arising all the time, and bugs and misunderstandings must be fixed by adapting the implementation. The overall cost of a system is, in fact, usually dominated by this kind of maintenance work. By keeping an eye on flexibility from the beginning, we can significantly reduce these future costs.
12.1 Coupling and cohesion give us a good handle on the design decisions to be made. Below these rather strategic—and perhaps a little abstract—concepts, 11.5.1 tactical considerations about proper information hiding, separation 11.5.2 11.2.3 of concerns, and the Single Responsibility Principle go a long way in achieving the goal. That is, if we get the individual objects “right,” the chances that the overall system remains flexible increase dramatically. This section is about the strategic considerations that go beyond the tactical ones.
The overall goal for flexibility: localize decisions.
Information hiding is a very strict goal: An object must not reveal its internal implementation, not even accidentally such as through the return types of getters. However, an object’s public interface always reveals the object’s purpose: Which services will the object provide? Is it “intelligent,” containing subtle or powerful algorithms, or is it rather “dumb,” mainly keeping data with a few operations to match?
When designing for flexibility, we cannot hide away all our decisions, because some go into the interfaces of objects. Also, we must treat the published interfaces as something fixed, something that we cannot change in a hurry, because other objects rely on them. But we can aim at minimizing the influence that changes in our decisions will have on these interfaces. Likewise, we can confine or localize our decisions in larger software units, such as by hiding objects behind interface definitions or publishing only facade objects.
12.2.1 Techniques for Decoupling
71Professional design always involves the solution space: We have to be aware of the technical tools that enable us to express our design in concrete code. We therefore start out by gleaning a few tricks from the Eclipse platform. Many of the points overlap with the usage of the relevant constructs from Part I, but now we ask the reverse question: The focus is not on the possible applications of a construct, but rather on the constructs that fit a specific application—namely, that of achieving decoupling.
12.2.1.1 Using Objects Instead of Primitives
The most basic technique for keeping the implementation flexible is to avoid exposing primitive data types such as int or String and to define application-specific objects instead. This tactic is hardly worth mentioning, except that one is often faced with an awkward choice: to pass the primitive data along now and immediately, or to stop coding and start thinking about the meaning of the data and about an appropriate design for an object encapsulating the data. By placing data inside objects you can localize the decision about its representation, possible indexes and caches, and other facets, but it does take some time. Besides the encapsulation, you also gain the invaluable chance to express the data’s purpose in the class name. 11.2.1
12.2.1.2 Factoring Out Decisions: Composition and Inheritance
The guideline in regard to flexibility is to localize decisions. To design such that decisions can be revised means to find interfaces behind which the decisions can be hidden. With that approach, even if the decisions change, the interfaces can stay in place. At this point we need to talk about classes, even if otherwise we focus on objects: Classes are the units of code that may depend on the decisions.
Factor out decisions into separate classes.
Suppose you are about to make a decision that you know may change in the future. In recognition of this fact, you create a separate class to which any code that depends on the decision will be confined. The class is a rigid container for the fluid consequences of the decision. Usually the new class must communicate with its surroundings. Now there are two ways to achieve this: composition and inheritance.
It is common consensus that inheritance introduces a strong coupling 44(Item 16) 232 between the base class and its derived classes. We have discussed at several 1.4.8.3 3.1.2 points that inheritance is, indeed, a powerful construct that needs some taming to be useful. The Fragile Base Class Problem highlights most 3.1.11 clearly the idea of coupling as the inability to change the base class without 12.1.1 breaking the derived classes. We have also seen that for this reason deep 3.1.5 inheritance hierarchies are rather rare and should be avoided. But when exactly should we use composition, and when should we use inheritance?
Prefer composition for largely independent functionality.
Let us look at the example of source code editors in Eclipse. The base class AbstractDecoratedTextEditor introduces many elements that you will be familiar with from your daily work in Eclipse: the main source editing widget, the area for line numbers, and that for errors, warnings, tasks, and other elements on the right-hand side. The latter two areas are linked only very loosely to the editing functionality. In fact, they mainly have to scale their displays to match the size of the overall source code and the position of the editor’s vertical scrollbar. It is therefore sensible to move 7.8 them into separate custom widgets that can hide away decisions about the presentation of line numbers, errors, and so on.
org.eclipse.ui.texteditor.AbstractDecoratedTextEditor
public abstract class AbstractDecoratedTextEditor
extends StatusTextEditor {
private LineNumberColumn fLineColumn;
protected OverviewRuler fOverviewRuler;
...
}
The actual declared type of fOverviewRuler is IOverviewRuler, but that interface has only a single implementing class.
Prefer inheritance to assemble fragments that are linked tightly anyway.
The source code editors of Eclipse also show how inheritance can be used to localize decisions about different aspects of an object. For instance, the standard JavaEditor is assembled through several inheritance steps:
WorkbenchPart Provides the basic assets of windows in Eclipse, such as the title, tooltip, icon, and site for the integration with the context.
EditorPart Adds the element of an editor input of type IEditorInput. Editor inputs abstract over the concrete location of the file being edited. They can be saved and restored to maintain the workbench state across sessions.
AbstractTextEditor Assembles the main ingredients for source code editing, 9.3.1 such as an ISourceViewer, font resources, and an IDocument Provider for accessing the document on the external storage.
StatusTextEditor Can replace the text editing widget with a page for showing error messages, such as the file being missing or exceptions being thrown during initialization.
AbstractDecoratedTextEditor Adds the right-hand-side overview rulers, line numbers, and support for displaying annotations such as errors, tasks, and changes on the edited source code.
JavaEditor Adds all the Java-specific elements, such as the outline page and a matcher for parentheses according to the Java syntax.
When going through these contributions, you will notice that the elements introduced in each step are tightly linked to those of the preceding steps. For instance, anything below the AbstractTextEditor will at some point or other access the ISourceViewer and theIDocumentProvider.
Also, it is quite common to create many cross-references between the different aspects. A prototypical case is seen in the JavaEditor. To maximize its support for the Java language, the editor requires a special source viewer. To introduce it, it overrides the factory methodcreateSourceViewer() 1.4.12 of AbstractTextViewer, as seen in the following code. Look closely at the required arguments to see how many of the elements introduced in the hierarchy are tied together to achieve the desired functionality. (The methodcreateJavaSourceViewer() merely creates a JavaSourceViewer.)
org.eclipse.jdt.internal.ui.javaeditor.JavaEditor
protected final ISourceViewer createSourceViewer(
Composite parent, IVerticalRuler verticalRuler,
int styles) {
...
ISourceViewer sourceViewer = createJavaSourceViewer(
editorComposite, verticalRuler,
getOverviewRuler(),
isOverviewRulerVisible(),
styles, store);
...
return sourceViewer;
}
Use inheritance to optimize the public interface of objects.
The relationship between a base class and its derived classes includes a 3.1.2 special, semi-private interface. It is given by protected services offered to 1.4.8.2 the derived classes and hooks, mostly for TEMPLATE METHODS, that must 1.4.9 be provided by the derived classes. In Part I, we emphasized the dangers of this special interface, because it offers privileged access to a class’s internals, which can quickly destroy the class invariants. 6.4.2
The perspective of localizing decisions now enables us to see the opportunity created by this intimacy: A class can make some aspects available to a few selected “friends” while keeping them hidden from the world at large. As a result, the public API can remain lean and focused (Fig. 12.6): It accepts only a few service calls that fit directly with the object’s purpose, 11.2.1 and the callbacks are mostly notifications using the OBSERVER pattern. In 2.1 12.2.1.8 contrast, the interface between base class and derived classes includes all kinds of links between the object’s aspects, as we saw earlier. It also comprises many small-scale downward hooks and callbacks. Perhaps methods are even overridden on an ad-hoc basis to enforce a specific behavior. Without the special interface, all of this would have to be public and could not remain localized.
Figure 12.6 Optimizing the Public Interface
239C++ friends declarations serve the same purpose of keeping the general API lean but providing a selected few more rights. Something similar can, of course, be accomplished in Java with package visibility, albeit at a much coarser granularity.
12.2.1.3 Roles: Grouping Objects by Behavior
11.1Roles in responsibility-driven design are sets of related responsibilities. They serve to characterize objects independent of their implementation class. At the language level, they map to Java interfaces. Roles usually abstract away many details of the object’s behavior. Often, they describe behavior 11.3.3.7 that can be implemented in many different ways in different contexts. We have already seen that roles can keep objects exchangeable. In the current context, they can help to localize decisions behind a common abstraction that will be appropriate even if we have to reconsider a decision.
Capture the minimal expected behavior in an interface.
You have to make a decision if you are faced with several alternative designs or implementations. By putting a role in front of the alternative you choose, you can keep the remainder of the system independent of your choice (Fig. 12.7).
Figure 12.7 Hiding Alternatives Behind Roles
The challenge is, of course, to design the role. There are several approaches. Starting from the alternatives, you can think about their commonalities 70—the assumptions that will hold true regardless of the decision. On the one hand, this approach is quite straightforward and leads to powerful and extensive roles. On the other hand, there is the danger that tomorrow you might encounter yet another alternative that does not fit the bill.
In the other direction, you can ask what you actually wish to achieve with your design. What is the (main) purpose of the object or subsystem 11.2.1 you are creating? What are the demands that the remainder of the system will have? This approach leads to roles that contain the minimal necessary responsibilities. It has the advantage that newly arising alternatives still match the demands, because they do fulfill the same overall purpose—otherwise, they would not be alternatives at all.
For an example, let us turn to Eclipse’s workspace, which comprises all the resources such as projects, folders, and files accessible in the IDE. Internally, the workspace implementation is very complex, since it must keep track of resource modifications, symbolic links, locking, and many more details. To keep the exact implementation changeable, the designers have introduced an interface IWorkspace, which provides an abstract view on the available operations. It is minimal in the sense that all methods can be explained by the abstraction of a “workspace.” Anything that is not in the abstraction is not in the interface.
We can only give the general flavor of the workspace abstraction here, using typical methods from the IWorkspace interface shown in the next code snippet. First, all resources are contained in a tree structure accessible 2.3.1 below the workspace root. One can also observe resource changes, such as to keep user interface elements up-to-date. The workspace is responsible 9.1 for running builders; usually this happens automatically, but builds can also be triggered. Any workspace modification should be wrapped in a IWorkspaceRunnable and should be specified further by the resources that it modifies (called the scheduling rules). The workspace will make sure that operations do not interfere with each other, so that the scheduling rules 8.1 act as soft locks. The workspace can also check pre-conditions of intended 4.1 operations—for instance, to validate a desired project location. Dialogs can use these methods to be robust and to ensure that subsequent operations 4.6 will succeed.
org.eclipse.core.resources.IWorkspace
public interface IWorkspace extends IAdaptable {
public IWorkspaceRoot getRoot();
public void addResourceChangeListener(
IResourceChangeListener listener);
public void build(int kind, IProgressMonitor monitor)
throws CoreException;
public void run(IWorkspaceRunnable action, ISchedulingRule rule,
int flags, IProgressMonitor monitor)
throws CoreException;
public IStatus validateProjectLocation(IProject project,
IPath location);
...
}
The question is, of course, how we will ever obtain an object filling the 1.3.8 abstract role in Fig. 12.7. In the case of the resource plugin, a SINGLETON is justified because each Eclipse instance works on a single workspace (as in 1.4.12 the following code). An alternative is to provide factory methods or abstract factories.
org.eclipse.core.resources.ResourcesPlugin
public static IWorkspace getWorkspace()
Enabling several or many alternatives leads to extensibility.
There are also many examples where all conceivable alternatives are actually 11.1 valid. For instance, we started our exploration of responsibility-driven design with the question of how the dialog available through the New/Other context menu was ever to encompass all types of files available in Eclipse. The answer is that it doesn’t. It merely defines a role “new wizard,” with interface INewWizard, whose behavior is to ask the user for the file-specific information and then to create the file. Such situations, where different useful alternatives for a role can be implemented side-by-side, lead 12.3 to extensibility of the system.
Provide a general default implementation of the interface.
In such cases, other team members or even your customers will supply the implementation or implementations of the role. You can make their task 3.1.4 considerably easier if you provide a general default implementation as a base class. Its mechanisms can already work toward the common purpose of all alternatives you can think of.
12.2.1.4 Dependency Inversion
11.5.6The Dependency Inversion Principle proposes to make central parts of an application depend on abstractions, rather than concrete implementations. 12.2.1.3 Technically, this means introducing roles to capture the expected behavior of other subsystems. From an architectural perspective, it requires that you identify the possible variations in this behavior.
Dependency inversion is also a powerful technique for decoupling and for achieving flexibility. After identifying the parts where the implementation has to remain flexible, the remainder of the application must depend on these parts only through abstractions. An essential point is that applying the principle does not miraculously make “everything flexible.” It is your own responsibility to choose carefully where the extra indirection through interfaces actually brings new benefits and where it simply obscures the sources.
Since the source code for this approach will look essentially the same as in previous sections, we will not provide an example here. Instead, we encourage you to go back to the snippets from the Eclipse platform and to view them as instances of dependency inversion—the change of perception is really quite startling and helpful.
12.2.1.5 The Bridge Pattern
On the one hand, inheritance induces strong coupling and comes with all 12.2.1.2 the associated problems for changeability. On the other hand, it is a very neat and powerful implementation mechanism. The BRIDGE pattern aims at 100 keeping the strong coupling hidden at least from the clients by introducing a self-contained abstraction. At the same time, it enables clients to use inheritance themselves.
Pattern: Bridge
To keep the implementation of abstractions hidden from clients and to enable clients to add new subclasses themselves, introduce parallel class hierarchies for the abstraction and its implementation.
1. Define the abstraction hierarchy that clients will use.
2. Implement the abstraction in a separate hierarchy.
3. Provide factories that allow clients to obtain concrete implementations.
BRIDGE shields the clients behind a fixed abstraction.
A typical application of the pattern is seen in the design of the Abstract Window Toolkit (AWT), which was Java’s first user interface framework and remains the technical basis of Swing. The overall goal is to enable AWT to choose a platform-specific implementation for each widget at runtime. These implementations are called peers, because they accompany the widgets created by the clients.
Let us trace the pattern’s approach with the example of a TextField. AWT first creates an abstraction hierarchy for the clients, where TextField 3.2.3 inherits from TextComponent, and TextComponent in turn inherits from Component, the base of all widgets in AWT. The actual handling of text takes place in the TextComponent. Note how the widget accesses its peer for the real operation: The widget, as perceived by the client, is only a handle on the real implementation.
java.awt.TextComponent
public class TextComponent extends Component implements Accessible {
public synchronized String getText() {
TextComponentPeer peer = (TextComponentPeer) this.peer;
text = peer.getText();
return text;
}
public synchronized void setText(String t) {
...
}
...
}
The abstraction hierarchy is mirrored on the peers. TextFieldPeer derives from TextComponentPeer, which derives from ComponentPeer.
java.awt.peer.TextComponentPeer
public interface TextComponentPeer extends ComponentPeer {
String getText();
void setText(String l);
...
}
The implementation hierarchy remains hidden and changeable.
The real implementation of the AWT widgets is hidden in the bowels of the JRE library. Probably the methods of these objects are native anyway. For instance, the peer for a text field on Linux is called XTextFieldPeer. The clients are not concerned with that hierarchy at all, so it can be modified as necessary.
Link abstraction and implementation by a factory.
Since clients cannot create objects for the offered abstraction directly, the 1.4.12 framework has to define an ABSTRACT FACTORY. In AWT, the factory is called Toolkit. Here is the case for the text field:
java.awt.Toolkit
public abstract class Toolkit {
protected abstract TextFieldPeer createTextField(
TextField target) throws HeadlessException;
...
}
To simplify the API for clients, it is useful to hide the creation of implementation objects altogether. Here is the case of AWT: Whenever a widget is added to the widget tree and must be displayed, it constructs its peer on the fly.
java.awt.TextField
public void addNotify() {
synchronized (getTreeLock()) {
if (peer == null)
peer = getToolkit().createTextField(this);
super.addNotify();
}
}
Clients can extend the abstraction hierarchy.
A central attraction of BRIDGE is that clients can introduce new cases into the abstraction hierarchy. In the case of AWT, they can implement their own types of widgets. In fact, the entire Swing framework relies on 7.5 7.8 this ability: Its root JComponent derives from AWT’sContainer. As a result, Swing can, among other things, rely on AWT’s low-level resource handling and painting facilities so that the native code written for the different window systems is reused.
BRIDGE is usually a strategic decision.
The preceding code demonstrates a slight liability of BRIDGE: It tends to require a lot of extra infrastructure and many class and interface definitions. You should therefore not introduce BRIDGE lightly, but only if you see the concrete possibility that the implementation hierarchy will have to change at some point.
AWT is a case in point. Another example can be seen in the Eclipse 235 Modeling Framework (EMF), which generates concrete classes from abstract UML-like models. In this process, it introduces a lot of boilerplate code and internal helper objects. All of those are not for consumption by the clients, but are necessary for setting up the complex and powerful runtime infrastructure that the EMF promises. Accordingly, the EMF generates a hierarchy of interfaces that reflects directly the properties, relationships, and subtyping given in the model. Clients access only these interfaces and will never depend on the intricate implementation details at all.
12.2.1.6 Boundary Objects and Facades
Objects are meant to form groups, or neighborhoods, of collaborators that 1.1 11.3.3.8 work toward a common goal. Unfortunately, objects in such groups also have a natural tendency to cling together, and to glue surrounding objects to the group. To collaborate, objects need references, so that class names of collaborators are hard-wired into the objects. In the end, one tends to end up with an unmanageable lump of objects for an application.
24 To avoid this, software of any size at all must be broken up into largely independent subsystems. Development can then be organized around this structure, and the design and implementation can proceed at least partly 12.4 in parallel; in the best case, one may even reuse some subsystems between products. Ideally, collaboration between subsystems is then channeled through a few well-chosen and well-defined boundary objects 1.8.1 (Fig. 12.8). Their role is that of interfacers that enable communication, but they also serve to protect and encapsulate the subsystem from the outside world, and shield it from changes elsewhere.
Figure 12.8 Boundary Objects Between Subsystems
We have already seen several instances of boundary objects. For example, 1.7.2 facades facilitate communication from clients to service providers 2.4.3 inside a different subsystem. Remote proxies channel method invocations 2.4.1 through byte streams. Adapters can bridge semantic gaps between object interfaces of different subsystems. All of these are, however, not very strict and rather incidental: They mainly perform those translations that are inevitable.
Boundary objects can define the clients’ expectations.
One very good reason for introducing boundary objects is that the remainder of the subsystem may not yet exist. Suppose, for instance, that in Fig. 12.8 subsystem A is under development and relies on B. Unfortunately, B has been subcontracted to a different company and will not be available for a few months. In the current context, all decisions regarding B will be made elsewhere and must be localized in B.
It is then useful to design boundary objects D through which all calls to B will be channeled. Be very careful and diligent about their API, but 5.3.2.1 only mock up their behavior for the time being. The unique chance here is that you can define the objects D according to your own expectations, creating a “wish list” for the later functionality. You can even hand the classes to the other company and ask for a proper implementation.
Boundary objects can insulate subsystems from changes elsewhere.
Very often, one of the subsystems in Fig. 12.8 is somewhat volatile. It may be a third-party library under active development, a prototype that will eventually evolve into a real implementation, or even a mock-up that so far answers only a few well-defined requests. For instance, if B is volatile but keeps the behavior of boundary object D consistent throughout the changes, then A can remain stable.
Boundary objects can confine unknown or complex APIs.
One particular instance occurs when you are quite unsure about some API you have to use. You know that you can accomplish some task, but you do not yet know the best, the most idiomatic, the most well-established way of doing it. In terms of the overall guideline of localization, you cannot decide what the best implementation really is. Just working on this basis means spreading your shallow knowledge throughout your production code: After you have learned more about the API, you will have to go back and perform massive shotgun surgery. 92
Suppose, for instance, that we want to write an editor for an XML document containing the description of vector graphics, similar to SVG. Model-view separation demands that the user interface observes the XML 9.1 document. But then neither the standard Document nor Elementprovides methods for adding and removing listeners! After some web searching, we find that EventTarget is the thing we need. But then the event model of the W3C DOM specification does look somewhat complex and also a bit intimidating.
So we start thinking about a subsystem “picture DOM” that encapsulates the handling of standard DOM objects as representations of pictures. One particular boundary object will hide the complexity of the native event model. What we really want is to be notified about the “obvious” tree manipulations through a standard OBSERVER pattern. So we start by defining 2.1 the listener interface, which also belongs to the boundary of the “picture 2.1.2 DOM.”
pictures.picturedom.DOMModificationListener
public interface DOMModificationListener {
void nodeInserted(Element elem, Element parent);
void nodeRemoved(Element elem, Element parent);
void attributeModified(Element elem, Attr attr);
}
Then we create the facade object itself according to our expectations.
pictures.picturedom.PictureDOMFacade
public class PictureDOMFacade implements EventListener {
...
private ListenerList listeners = new ListenerList();
private Document doc;
...
public void addDOMModificationListener(
DOMModificationListener l) {
...
}
public void removeDOMModificationListener(
DOMModificationListener l) {
...
}
...
}
It is only when implementing the facade that we have to understand the DOM event model. In fact, it is sufficient to understand it partially: If later on we learn more, we will then be in a position to adapt the implementation without touching the remaining system. Upon searching for references to the EventTarget interface in the Eclipse platform code, we find that we will have to register for all relevant events separately. The events are named and it seems that no constants are available. This does look messy! Fortunately, we are sure we can change the code if we happen to learn we overlooked something.
pictures.picturedom.PictureDOMFacade
public static final String DOM_NODE_INSERTED = "DOMNodeInserted";
public static final String DOM_NODE_REMOVED = "DOMNodeRemoved";
...
public static final String[] EVENTS = { DOM_NODE_INSERTED,
DOM_NODE_REMOVED, DOM_ATTR_MODIFIED };
private void hookDocument() {
if (doc == null || listeners.isEmpty())
return;
EventTarget target = (EventTarget) doc;
for (String evt : EVENTS)
target.addEventListener(evt, this, false);
}
The final step is to translate the native notifications into the desired ones. Here we go—once again the code is open for future improvements.
pictures.picturedom.PictureDOMFacade.handleEvent
public final void handleEvent(Event evt) {
if (evt instanceof MutationEvent) {
MutationEvent mut = (MutationEvent) evt;
if (DOM_ATTR_MODIFIED.equals(mut.getType())) {
fireAttributeModified((Element) mut.getTarget(),
(Attr) mut.getRelatedNode());
} else
...
}
}
Combine boundary objects with roles for further abstraction.
172(Ch.8) It is often useful to combine the technique of boundary objects with the 12.2.1.3 definition of roles for their minimal expected behavior. The Eclipse platform uses this throughout. For instance, JavaCore creates Java model elements that are specified through interfaces. Similarly, the ResourcesPlugin doles out handles to concrete resources, but the static types are always interfaces.
Boundary objects can isolate subsystems from failures elsewhere.
Quite apart from the structural benefits of explicit boundary objects, they also have advantages at runtime. As we have seen, effective development and 4.5 efficient execution always require a good deal of trust: The non-redundancy principle demands that methods never check their pre-conditions, and every caller relies on invoked methods to actually establish the post-conditions 4.1 they promise. In cross-module calls, this may not always be justified; perhaps the “other” developers do not share our understanding about the contracts.
For instance, the NotificationManager sits on the boundary of the Eclipse resource management. Objects from all over the application register to receive change events, and the manager sends these out in due turn. This sending step is a bit dangerous: If any of the listeners throws an exception, it might upset the consistency of the resource data structures. At this crucial point, the notification manager prevents this dangerous outcome from happening by wrapping the notification through SafeRunner.
org.eclipse.core.internal.events.NotificationManager
private void notify(
ResourceChangeListenerList.ListenerEntry[] resourceListeners,
final IResourceChangeEvent event, final boolean lockTree) {
for (int i = 0; i < resourceListeners.length; i++) {
SafeRunner.run(new ISafeRunnable() {
public void run() throws Exception {
listener.resourceChanged(event);
}
});
}
}
12.2.1.7 Factories
A particularly strong form of coupling arises from the creation of objects: Java gives us only one chance of setting an object’s type and general behavior—namely, by choosing its class at creation time. Enabling flexibility here means introducing factory methods or abstract factories. The1.4.12 term “virtual constructor” for a factory method summarizes the benefit very clearly: We are suddenly able to override the creation step itself. Since we 1.4.12 have already seen many examples, both in the small and in the large, we merely point out the power of the technique here.
Another connection is worth mentioning: Factories work best when combined with roles. The return type of a creation method must be general 12.2.1.3 enough to enable different implementations. Specifying an abstract base 3.2.10 class rather than an interface constrains the choice of implementations. Even so, it keeps the overall structure less abstract and more tangible and might improve understandability.
12.2.1.8 Observers
Most collaborations have very concrete participants: One object requires a 11.1 service of a second object or notifies that object about some event. Both objects are designed at the same time and in lockstep to ensure the collaboration is smooth and effective. While elaborating the different use cases 11.3.2 with CRC cards, we trace through these concrete collaborations. One step 4.1 6.1.2 6.1 further toward the implementation, we design the contracts of the method calls to achieve a sensible distribution of work between the caller and the callee.
Observers enable flexible collaborations and flexible behavior.
2.1The OBSERVER pattern offers the chance of more flexible collaboration 2.1.2 schemes. At its core, the subject specifies the observer interface by considering only its own possible modifications, but never the concrete expected observers. As a result, the subject knows next to nothing about its observers. In turn, new observers can be added in a very flexible manner and existing observers can be changed without breaking the subject.
2.1.3 At the same time, the commonly used push variant of the pattern provides the observers with so much detailed information that they are really well informed about the subject. It is almost as if the subject is constantly collaborating with its observers, since the most significant occurrences in an object’s life are usually connected to its state.
An observer can then implement quite complex functionality based on the information it receives. For instance, by observing Eclipse’s resources, one can update the user interface or compute and track meta-information about files. Just look through the callers of addResourceChange Listener() in IWorkspace to get an impression of the breadth of the possible applications.
12.2.1.9 Inversion of Control
The OBSERVER pattern introduces callbacks. As we saw in the previous section, these can be helpful for achieving flexibility. However, the callbacks are restricted to changes in an object’s technical state. One step beyond, one can ask which events within an object’s life cycle may be relevant to others, even if the object’s state does not change. A flexible system then emerges, because the events are detected and distributed regardless of their receivers.
An event-driven style keeps objects loosely coupled.
Suppose we apply this idea to the “big players” in a network (Fig. 12.9). These objects may use local helpers, indicated with thin dashed lines. But it is the big players that tie together the overall system, by communicating among themselves. Now, rather than sending messages tailored to the receivers, they send out events about themselves without caring about the receivers. The receivers react to the events and broadcast new events about themselves. As the thick dashed lines indicate, the “big” objects remain 12.1 largely independent and are coupled very loosely, because none of them makes too many assumptions about the event receivers.
Figure 12.9 Flexibility Through Inversion of Control
We may ask, for instance, why SWT is such a flexible and versatile 7.1 widget toolkit. It is certainly not its artistic quality and beauty—SWT does nothing about that anyway. SWT’s widgets are flexible because they offer so many different events capturing possible user interactions. As a result, almost any application functionality can be built on top of SWT, because that functionality can be triggered at the right moments.
The object-oriented perspective on method calls is message passing. We do not think 1.4.1 109 about technical aspects such as call stacks, dynamic dispatch, and parameter passing, nor do we expect a particular piece of code to be executed for a method call. Instead, an object sends out a message and trusts the receiver to take the appropriate action. The attraction of this fundamental notion becomes even clearer in the light of event-based communication. Messages can then be understood as “I think something like this should happen,” rather than “Please do this for me.” With this looser interpretation, the receiver is more flexible in its reactions. If applied throughout the design, the looser interpretation leads to more flexible systems.
Inversion of control keeps the system flexible.
Events also come with inversion of control: It is the sender of the event, not 7.3.2 its receiver, that determines when a reaction or functionality is required. In Fig. 12.9, the receivers are content knowing that an event has occurred, but they do not inquire about the exact internal circumstances in which this has happened. In the end, inversion of control, rather than the events themselves, makes for the increased flexibility, because many different behaviors can be attached to an event source. 12.3.2
Let us look at a concrete example, the Outline View of Eclipse. That view somehow manages to show the structure of any type of file, whether it contains Java sources, XML data, or a bundle manifest. How does this work? The construction assembles the view by inheritance. The classContent Outline 12.2.1.2 derives from PageBookView. That class tracks the currently active editor and caches a page for each of the views and editors. All pages are contained in a page book and the currently required one is brought on top. To keep informed about the active editor, the PageBookView registers a 7.1 listener when it shows itself on the screen:
org.eclipse.ui.part.PageBookView
public void createPartControl(Composite parent) {
book = new PageBook(parent, SWT.NONE);
...
getSite().getPage().addPartListener(partListener);
...
}
12.3.3.4The site of a view is the context in which it is shown and gives the view access to its surroundings. The page of a workbench window contains all views and editors.
Here is the first case of flexibility brought about by inversion of control: The overall workbench window does not care about exactly which views are shown. The notifications about the active view or editor work equally well for all of them.
2.1.3 The events about part changes arrive, through a private anonymous class, at the method partActivated() in PageBookView. At this point, the PageBookView decides that a new page must be created if it does not exist.
org.eclipse.ui.part.PageBookView
public void partActivated(IWorkbenchPart part) {
...
PageRec rec = getPageRec(part);
if (rec == null) {
rec = createPage(part);
}
...
}
Here, the second instance of inversion of control occurs: The PageBookView decides that a page must be created and its createPage() method calls the following doCreatePage(). In other words, the class asks its concrete subclass to actually provide the page:
org.eclipse.ui.part.PageBookView
protected abstract PageRec doCreatePage(IWorkbenchPart part);
At this point, we find inversion of control a third time: The ContentOutline view asks the currently active editor for an IContentOutlinePage (lines 2–3); then it asks the retrieved page to render itself within the page book (line 7)—the fourth specimen of inversion of control.
org.eclipse.ui.views.contentoutline.ContentOutline
1 protected PageRec doCreatePage(IWorkbenchPart part) {
2 Object obj = ViewsPlugin.getAdapter(part,
3 IContentOutlinePage. class, false);
4 if (obj instanceof IContentOutlinePage) {
5 IContentOutlinePage page = (IContentOutlinePage) obj;
6 ...
7 page.createControl(getPageBook());
8 return new PageRec(part, page);
9 }
10 return null;
11 }
Inversion of control localizes decisions.
But let us go back to the overall guideline: To keep a system flexible, try to localize decisions. The example of the outline view shows clearly how inversion of control achieves this. Each step makes one decision, but delegates others to the respective callee. The workbench window’s page delegates reactions to the user’s switching of the active editor; the PageBookView delegates the creation of a particular page; the ContentOutline asks the current editor to provide the actual page. The decision of what to do in each step rests with the object taking the step and does not depend on the other objects involved.
From a different point of view, the scenario and the techniques can be seen as a case of extensibility: The PageBook caters to many types of pages; the ContentOutline works 12.3 with different kinds of editors. Then, the overall strategy is covered by the INTERCEPTOR12.3.2 pattern. You may want to peek ahead now or to come back later to explore this link in more detail.
12.2.2 The Layers Pattern
Developers like to think in terms of high-level and low-level aspects of their software. For example, the business logic is high-level, the necessary networking is low-level. JFace is more high-level than SWT, because we connect 9.3 widgets directly to the application’s data structures instead of placing strings and images into the widgets. In addition, the user interface focuses 9.1 on a pleasant user experience and delegates the details of processing to the model.
It is often useful to apply Separation of Concerns along these lines—that 11.5.2 is, to treat high-level aspects in one module and low-level aspects in another. As the previously mentioned examples show, the perspective determines precisely what is high-level and what is low-level. The perspective is based on an implicit measure of “abstraction” (Fig. 12.10): 59
(a) The business logic assumes that it can send data over the network, but it is not concerned with the wires that transport the data.
(b) An application may be using a library of standard graph algorithms, which in turn relies on the basic data structures from the JDK.
(c) JFace provides mechanisms for mapping data to SWT widgets and spares the application the direct setting of strings and images; the SWT layer in turn spares JFace the details of the native window system and makes it platform-independent.
(d) The user interface orchestrates only the processing steps available in the model.
Figure 12.10 The Layers Pattern
101 59 The LAYERS pattern captures this idea. The notion of “layers” is, indeed, folklore in development practice. The pattern adds several details that the naive view neglects. These details are, however, crucial to gain the full benefits of a layered design.
Pattern: Layers
Structure a system by splitting its functionality into a stack of layers according to a chosen abstraction criterion.
1. Choose and fix a suitable abstraction criterion.
2. Align the functionality along the scale of abstraction.
3. Split the functionality into layers, focusing on cohesion.
4. Define the interfaces between adjacent layers.
The presentation in [59] proposes nine steps in the implementation. We have summarized them here to give a better idea of the overall intention. We will later fill in the details.
Fig. 12.11(a) gives the overall idea of the pattern. We arrange the responsibilities we find into modules along an increasing level of abstraction. The lowest module is layer 1. Each layer communicates only with its adjacent neighbors. In particular, a layer never reaches more than one layer away. Fig. 12.11(b) adds further details about that communication. Very often, the downward calls constitute service requests: to send data over the network, to display specific data on the screen, or to perform a particular computation on the application data. Upward calls, in contrast, are very often notifications, such as about the availability of network data, about clicks in the user interface, or changes to the data stored in the model.
The attentive reader will note immediately that the JFace/SWT example violates 9.3.1 the principle of never “going around” layers from Fig. 12.11(a), since the application will, indeed, access SWT widgets directly. We will come back to this point later.
Figure 12.11 The Layers Pattern
We will now examine these aspects in greater detail and analyze their 12.1 impact on the success of applying the LAYERS pattern. Decoupling is a cross-cutting concern: The particular structure from Fig. 12.11(a) enables higher-level and lower-level parts of the system to vary independently.
Formulate the abstraction criterion carefully.
The central choice in implementing the pattern is the abstraction criterion: It determines the order of the layers and the distribution of responsibilities between them. The criterion very often is just the amount of technical detail to be considered, such as when placing network functionality in a layer below the application’s business logic. But it may actually require some thinking to come up with a coherent solution.
To see the impact of the chosen abstraction criterion, let us reexamine a well-known and seemingly obvious example: the user interface stack of native toolkit, SWT, JFace, and application UI depicted in Fig. 12.10(c). The underlying abstraction criterion is the “amount of technical detail.”
A completely different sequence of layers results from starting with the 9.1 principle of model-view separation. That principle suggests that the foundation is the application’s business logic, so let us change the abstraction criterion to “distance from the business logic.”
Fig. 12.12(a) shows the result. The actual SWT interface is as far removed from the application model as possible and the intermediate layers connect the two endpoints. With a bit of imagination, even the communication between the layers can be interpreted as service requests and notifications: When the user clicks a button, this is a service request that gets translated through the layers to an eventual change of the application 9.2.1 model. In the reverse direction, the MODEL-VIEW-CONTROLLER pattern suggests that change notifications travel upward until eventually they reach the SWT widgets. All in all, the layering resulting from the new abstraction criterion expresses much better the ideas and design choices of model-view separation.
Figure 12.12 Alternative View on Layers Involving JFace
Even so, this interpretation of services and notifications is a little forced. At the technical level, as shown in Fig. 12.12(b), the “notifications” from the application’s special UI objects to the JFace viewers and from there to the SWT widgets are actually simple method calls and therefore service requests. In the other direction, the “service requests” from SWT and JFace to the application layer are actually transported through OBSERVERS—that is, they are notifications. Even if the new criterion explains the software structure well, we will not be able to claim the benefits of LAYERS, such as the exchangeability of the lower layers.
This example shows that the abstraction criterion is not arbitrary, but rather must be formulated explicitly and with care. On the one hand, seemingly obvious criteria may have alternatives. On the other hand, not all criteria lead to a proper implementation at the technical level.
Split the functionality into layers.
After defining an abstraction criterion, we start with responsibility-driven 11.1 design, using the layers as units instead of objects. However, we get some more help than from CRC cards alone: Whenever we find a new responsibility, we can place it by the linear measure of the abstraction criterion. We are not designing a full network of objects, but a linear stack with very restricted interactions. Very often, responsibilities will “fall” close to each other in this placement. In the example of network access, establishing a connection is naturally close to sending data over the connection. The layers then arise from clusters of responsibilities.
Maximize the cohesion of each layer.
In general, the number and the exact boundaries of the layers require some thought. It might even take some adjustments to the formulation of the criterion to reach a convincing structure.
Take the example of wizards in user interfaces. Should they be placed in the JFace or the SWT layer in Fig. 12.10(c)? In principle, a wizard is just a special compound widget that contains a series of pages to be displayed; it has nothing to do with mapping the application data. Wizards should therefore be in the SWT layer. However, we want the SWT layer to be minimal, because it must be cloned for different window systems. The 7.4 somewhat generic criterion “technical detail” used earlier should be made more precise as “distance to the native widget toolkit”: Mapping data is still a step above accessing the native widgets, but wizards are not actually offered as native widgets, so they are just one notch further up the abstraction scale and can be moved to the JFace layer, where they are shared between platforms.
The general goal is, of course, not some conceptually “pleasing” assignment of responsibilities to layers. Instead, the goal is to maximize the 12.1.3 cohesion within layers. Each layer then establishes one self-contained, meaningful abstraction step along the scale of the criterion, and it contains all the necessary software machinery to take this step effectively.
Design the intralayer structure carefully.
We have often seen beginners present a thoroughly designed layers concept for an application as a preparation for a lab exercise, only to end up with the most disastrous code base in the final submission. The reason is that they stopped thinking after having “fulfilled” the exercise’s goal of a layered design.
The layers are just the first step. They set the general scene, but you still have to fill in the objects and collaborations within the layers to make 11.1 the software work smoothly. Think of all the details of JFace’s rendering of wizards: the concrete dialogs, the interfaces of single pages and wizards, and the abstract base classes for implementing the interfaces easily. Without this careful work, introducing a JFace layer does not help at all.
Design the layer interfaces carefully.
The specific form of interaction between adjacent layers, which is shown in Fig. 12.11(b) on page 671, is responsible for many of the benefits of the LAYERS pattern. Here is the first point:
A layer is not aware of the identity of the next higher layer.
Fig. 12.11(b) demands that a layer communicate with its upper neighbor only through an interface specifically designed for the purpose. A layer is not aware of the exact identity of the next layer up, because that identity is hidden behind the interface. Think of a networking layer. It serves all kinds of applications as diverse as web browsers, telephone conference software, and remote sensing technology.
12.2.1We have seen the power of special-purpose interfaces several times in the techniques 2.1.2 for decoupling. In particular, the Observer pattern designs the observer interface from the perspective of the subject alone, without making assumptions about possible observers. This coincides with the idea of notifications for LAYERS. Similarly, the 11.5.6 Dependency Inversion Principle focuses the design on some important functionality and specifies its requirements on its collaborators.
Lower layers are insulated from higher-level changes.
The definition of a special notification interface for upward calls shown in Fig. 12.11(b) creates the immediate benefit of making lower layers independent of higher layers. As a result, the lower layers can be developed and tested independently. The technical, basic, detailed functionality is then stable and available early on in the project.
Higher layers can be insulated from lower-level changes.
In the other direction, higher layers are insulated from changes in the lower layers. With respect to the general guideline of localizing decisions, it is precisely the less abstract, the more intricate, and the potentially more obscure functionality that remains changeable. This is certainly a very desirable outcome for any design. But let us look at the details of why this really works out—it is easy to get them wrong in the implementation.
The fundamental approach is that each layer communicates only with its direct neighbors, so that changes further down in the stack will not affect its functionality. The most famous example is, of course, the Internet Protocol 91 stack [Fig. 12.13(a)]. The success of the Internet depends crucially on the fact that the lowest link layer, which deals with the physical networks to which a host is attached, can be exchanged: It must only relay packages, but the IP layer, which handles the global routing of packages, does not care whether a LAN, WLAN, or VPN is used for this purpose.
Figure 12.13 Exchangeability of Lower Layers
Of course, the exchangeability of lower layers does not come for free. One has to fix an interface on which the higher layer can rely [Fig. 12.13(b); dashed layers are exchangeable]. In effect, the interface defines a standard that any implementors of the layer have to obey. Defining the interface can be quite a challenge—the definition of the Internet Protocol as we know it took more than two decades, from the first research to the robust protocols.
Locally, one can resort to taking an existing layer with its working upward interface, and replace only its implementation [Fig.12.13(c)]. This idea 7.4 is seen in the design of the SWT layer: The classes’ public interfaces are the same on each window system, but their implementation must be developed nearly from scratch for each one.
Do not bypass layers.
We emphasize that all of the previously mentioned benefits result from the fact that layers communicate only with their immediate neighbors and therefore form a linear stack. Accessing lower layers directly destroys the benefits [see also Fig. 12.11(a)].
Layers can enable reuse of lower-level modules.
A further huge benefit gained by layers is the potential for reuse. Because a layer is not aware of the identity of its higher neighbor, chances are that it is reusable in a different context. Examples have been seen in Fig. 12.10(a)–(c) on page 670, where the lower layers contain reusable functionality.
It is again important to choose the right abstraction criterion. Rather than considering technical detail, distance to the user interface, or something similar, one must use the generality of the provided functionality. Also, one has to choose the specific layer interfaces to match this generality. For instance, the SWT widget set is reusable only because it is designed carefully to capture the requirements of standard desktop applications. The 237 layer of network sockets is so successful because its generic byte streams enable applications to encode their data as they see fit.
Translate errors to match the abstraction level.
One detail of a layer’s interface that often gives away the novice is error 1.5.1 handling. In general, errors must match the purpose of the component that raises them. Otherwise, the clients cannot interpret them properly. In the case of layers, errors must therefore match a layer’s abstraction level. Errors received from lower-level layers must usually be translated.
Handle errors in the lowest possible layer.
Layers offer a unique chance of shielding an application from low-level errors altogether. For instance, the IP layer of the Internet stack [Fig. 12.13(a)] 237 does not guarantee delivery. The TCP protocol on the next higher transport layer makes up for this shortcoming by arranging redelivery of unacknowledged packets. If some layer is therefore able to recover from the errors received from a lower level, it should do so.
Relaxed layers can mitigate some problems with strict layers.
Adhering to the communication discipline between layers strictly can lead to several problems. Very often, lower layers define extensive interfaces that enable their clients to take advantage of all technical details they have to offer. Higher-level layers then abstract over these details and the extra functionality is lost. For instance, the native widgets encapsulated by SWT offer all platform-specific features. However, SWT carefully exposes only those features that can be used reliably across platforms.
In general, all the functionality required by higher layers must be passed on throughout the stack. Sometimes, this is undesirable because it pollutes the higher-level interfaces with low-level detail, even if only some clients require them. The RELAXED LAYERS pattern acknowledges this by allowing 59 direct accesses around layers [contrary to Fig. 12.11(a)].
The JFace layer is a good example (Fig. 12.14). It relies on SWT for 9.3 the actual display of data, but does not hide that layer completely. The application still specifies layouts and visual attributes on the basic SWT 7.1 widgets, and it is always aware of the SWT-level widget tree. As a result, JFace can focus completely on adding higher-level services, which minimizes the implementation effort.
Figure 12.14 Relaxed Layers in SWT/JFace
Beware of rippling changes.
Layers are a folklore concept, and any team facing the design of a larger system will probably first think about chopping the task up into layers, if for no other reason than to separate the high-level (interesting) tasks from the low-level (tedious) tasks. It is very important not get carried away by enthusiasm at this point: All the benefits of the pattern rely on the stability of the layers’ interfaces. Once some interface of a low layer changes, chances are that the higher-level layers need to change as well. In fact, the pattern increases coupling rather than decreasing it. It is therefore very important 12.1.1 to plan the layers conscientiously from the start, rather than to start coding with a vague intuitive understanding.
In agile development, create LAYERS in vertical slices.
In many other places, we have pointed out the benefits of agile development and in particular of the approach of building “the simplest thing that could possibly work.” The LAYERS pattern introduces a possible snag here: The design guidelines imply that we find a perfect and fixed set of layers and their interfaces before we implement the internals. This goes against the grain of agile development.
The solution is to apply another principle, that of delivering a useful increment of functionality in each iteration. Rather than designing the layers 5.4.5 completely, we again let concrete use cases and test cases drive the design. In this way, we create a small part of each layer’s interfaces and implementation in each iteration. Effectively, we develop the stack of layers in small, vertical slices.
12.3 Extensibility
12.2Changeability is a crucial nonfunctional property for any professional software. One particularly important kind of changeability is extensibility. New use cases and new requirements keep arising all the time once a system is deployed. Very often, it is only at this point that users begin to realize what could be done in software—and then they continue asking for new features all the time. This section examines architectural approaches to extensibility, after providing an introduction to its technical aspects.
12.3.1 Basic Techniques and Considerations
One of the great attractions of object-oriented software development is that objects offer strong support for extensibility. At the most basic level, instantiating objects clones their functionality; extensibility here means doing the same thing as often as the users wish for. The technical basis for supporting 1.4.1 entirely new use cases of a system is polymorphism: Similar kinds of objects can be created by overriding particular methods to inject new behavior.
3.1.11 3.1.4 3.1.6 Because undisciplined coding quickly leads to problems, we have already examined many guidelines and techniques that tame polymorphism:
• 3.1.1 6.4The Liskov Substitution Principle dictates that new objects must obey the general behavior expected from their super-types. The interaction 3.1.2 between subclass and superclass should be defined explicitly through protected methods.
• 1.4.9The TEMPLATE METHOD pattern designates specific hook methods where subclasses can place new functionality without breaking the superclass mechanisms.
• The EXTENSION OBJECTS pattern enables an object to expose new 3.2.2 functionality without having to extend its interface. 3.2.5
• The COMPOSITE pattern supports new kinds of nodes in tree-structured 2.3.1 data such that recursive operations work smoothly for the new cases.
• The VISITOR pattern simplifies the introduction of new operations on 2.3.2 fixed object structures.
• The OBSERVER pattern transmits messages to any new kind of interested 2.1 object. New behavior can then be attached in an event-driven 12.2.1.8 7.1 way.
• The STATE pattern covers extensions of behaviors of individual objects 10.3.4 that are essentially state-dependent.
Extensibility: to be able to do similar things again with little additional work.
The techniques listed previously may seem rather comprehensive and their scope may seem promising. Perhaps it is, after all, very simple to build extensible systems, if we just combine the right techniques?
Unfortunately, this is not the case. The overall goal of extensibility is much broader than just making the implementation “flexible” at sufficiently many points or providing enough “hooks” where new code can be injected into the existing mechanisms.
Extensibility must aim at making extensions simple, while not overly complicating the design and implementation of the basic application. This means that the appropriate techniques must be applied at well-chosen spots; applying them throughout clutters the sources and makes it almost impossible to find the right spot to hook up a desired extension. Furthermore, the precise API of callbacks must be thought through in detail—extensions must be provided with enough information to go ahead with their tasks, yet they must not be overwhelmed. In the end, extensibility is attractive only if extensions are simple and follow well-defined patterns.
Extensibility requires keeping the application’s code base stable.
One of the crucial challenges of extensibility is to keep the overall application stable. Almost any software is “extensible” in the sense that it can be restructured and rewritten to accommodate new functionality. True extensibility means that no extra work on the existing code base is required to incorporate such additions. If this cannot be achieved, nothing is won and much is lost by trying for “extensibility”: The overhead for extensible mechanisms must be invested, but it never pays off, because each addition requires still more work on the base application. In other words, designing for extensibility involves a high potential for “speculative generality.” 92
Extensions are best integrated by inversion of control.
Extensions are kept simple if they perform just a small and well-defined task 7.3.2 at specific, well-defined points. Inversion of control is a prime approach to achieving this goal: The application takes care of running the software, and the extension can hang back until it is called.
To illustrate the previous two points, here is a prototypical example of a well-designed API for extensions. Eclipse’s Java editor includes a most powerful completion pop-up. As we saw in Part I, the “completions” can be rather complex code patterns. To keep the main Java plugin small, the 12.3.3.5 pop-up offers an extension point to which other plugins can contribute. The API is straightforward and minimal: Whenever the pop-up opens, the extension will be asked to supply proposals as self-contained objects. The given context contains the document and the current cursor position, so that the provider can pick up any textual context it requires.
org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer
public interface IJavaCompletionProposalComputer {
List<ICompletionProposal> computeCompletionProposals(
ContentAssistInvocationContext context,
IProgressMonitor monitor);
...
}
Anticipate the kinds and forms of additional behaviors.
One fundamental prerequisite for creating an extensible application is knowing which types or forms of extensions will likely be useful. If the scope is too broad, the individual extension becomes too complex. If it is too small, desirable extensions may not be possible. You can make accurate predictions only if you have worked on a variety of applications in the same area and know the variations in features that are likely to be required.
Solve two or three scenarios by hand before implementing extensibility.
Abstraction is always a challenging task to complete, and the abstraction over the desirable extensions is no exception. The “rule of three” applies, as usual: It is a good idea to build two or three instances manually, perhaps even choosing between them with crude if-then-elseconstructs. This implementation gives you two crucial insights. First, you know that the extensibility will really be necessary, because you know five to ten similar pieces of functionality that would be useful. Second, you get a good feeling for the technical requirements of all this functionality. Your chances of getting the extension interface right increase dramatically with this approach.
12.3.2 The Interceptor Pattern
There are many forms and variants of mechanisms that make a software extensible, but they all share a few common central characteristics. Knowing these characteristics will help you understand the concrete extensibility mechanisms—the individual API elements and code snippets will fall into place almost at once. The INTERCEPTOR pattern gives the overall picture. 218
Pattern: Interceptor
Allow new services to be added to a framework such that the framework remains unchanged and the services are called back when specific events occur.
The term “framework” is typically used in the sense of a semi-complete application 7.3 that captures the mechanisms common to a family of applications in a reusable form. Here, the term focuses on the aspect of inversion of control: The framework defines the 7.3.2 overall computation and calls back to the new services at its own discretion. The pattern also applies to ordinary software that is to be extensible at certain points.
The central elements of the pattern, which are also the recurring elements of extensible software, are gathered in Fig. 12.15(a). We will now describe them one-by-one, going left-to-right, top-to-bottom. As a running example, we will again use the Java auto-completion mechanism. Since our 12.3.3.5 aim is to obtain a common blueprint for the later studies, we will not go into the details of implementing the INTERCEPTOR pattern. 218
Figure 12.15 Overview of the Interceptor Pattern
Interception points designate useful hooks for adding new services.
The pattern’s first insight on extensibility is that one has to provide specific, well-defined points within the software that will receive the extensions. If we just provide arbitrary hooks in arbitrary places, the software becomes unmaintainable, because the services know too much about its existing internal 12.1.2 structure. The pattern calls these points of extensibility interception points because the services intercept the normal processing: They receive the control flow and perform their computation before the software resumes its operation. Fig. 12.15(a) indicates the control flow as a dashed, curved path.
In the running example, the interceptor must implement the interface IJavaCompletionProposalComputer. The service produces a list of proposals for the pop-up dialog.
org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer
public interface IJavaCompletionProposalComputer {
List<ICompletionProposal> computeCompletionProposals(
ContentAssistInvocationContext context,
IProgressMonitor monitor);
...
}
In practice, each interception point will offer several logically related callbacks, which are rendered as methods in a common interface that the service has to implement. The pattern calls these sets of callbacks interception groups. We have omitted the other methods in the interface in the12.3.3.5 preceding code snippet for brevity, but they are there.
Context objects enable interceptors to query and influence the framework.
A new service must usually interact with the framework to request data or to influence the future processing. Toward that end, the framework passes a context object, which offers a selection of access paths to the framework’s internals without revealing them. The context object can be seen1.7.2 3.2.7 12.2.1.3 as a FACADE; its API is usually specified through an interface to decouple the interceptors from the framework’s implementation.
In the example, the ContentAssistInvocationContext passed to the method computeCompletionProposals() gives access to the textual context where the completion was requested.
Extensibility works best with lean interfaces for interaction.
Fig. 12.15(b) summarizes the interaction between framework and interceptors. The framework invokes callbacks on the interceptors through a designated service interface; the interceptors in turn can access selected functionality from the framework through the context object. The point of the illustration, and indeed of the pattern, is that the interaction remains lean and focused, since all possible calls are specified by only two interfaces. In the example, knowing IJavaCompletionProposalComputer and Content AssistInvocationContext is sufficient to understand the interception point.
In fact, both the context object and the service interface in the example of Java 12.3.3.5 completions are subclassed, so that more specific services are accessible on both sides. This is an instance of the Extension Interface pattern. 3.2.5
The INTERCEPTOR pattern focuses on black-box frameworks. The lean API makes 7.3.3 for easy extensibility.
Callbacks to services occur for specific transitions in the framework state.
One crucial question with any arrangement for callbacks is when the callbacks will actually take place: the implementors of the interceptors can provide their services reliably only if they can be sure that callbacks will occur as expected.
The pattern proposes to create an abstraction on the framework’s internal state in the form of a state machine [Fig. 12.15(a), lower half]. Since it 10.1 is only an abstraction, it can be published to implementors of interceptors without revealing the framework’s internals. The state machine can, in fact, be read as a specification of the framework’s behavior, which will be useful in any case.
The link between the framework state and the interception points is established by the transitions of the state machine. Special transitions that are potentially relevant to new services are reported as events and lead to callbacks.
We examined the state machine for completion pop-ups in Fig. 10.8 on page 536. When the transition from a closed to an open pop-up window is made, the framework will request the completion proposals. While the pop-up remains open, the internal transition on text modifications will again request the updated completions.
The state machine abstracts over such details as delays, timeouts, and progress monitors. It highlights the points where the callbacks will occur.
Dispatchers manage interceptors and relay events.
Finally, there is a lot of boilerplate code involved in managing the interceptors attached to each interception point. The pattern proposes to see this management as a self-contained responsibility and to assign it to special dispatcher objects. The dispatchers also have methods that relay an event to all registered interceptors, by invoking the respective callback methods.
12.3.3.2 In the example, a (singleton) object CompletionProposalComputer Registry gathers and maintains the service providers implemented by different plugins. The completion pop-up can access them depending on the document partition (e.g., source code, JavaDoc).
org.eclipse.jdt.internal.ui.text.java.CompletionProposalComputerRegistry
public final class CompletionProposalComputerRegistry {
private final List<CompletionProposalComputerDescriptor>
fDescriptors;
List<CompletionProposalComputerDescriptor>
getProposalComputerDescriptors(String partition)
...
}
The dispatch of the callbacks is implemented for each computer separately, by CompletionProposalComputerDescriptor. The two classes together therefore form a typical dispatcher mechanism.
org.eclipse.jdt.internal.ui.text.java.CompletionProposalComputerDescriptor
final class CompletionProposalComputerDescriptor {
private IJavaCompletionProposalComputer fComputer;
public List<ICompletionProposal> computeCompletionProposals(
ContentAssistInvocationContext context,
IProgressMonitor monitor)
...
}
Interceptors usually have an explicit life cycle.
Interception points usually specify further callbacks that are independent of the framework’s state transitions but concern the interceptors themselves. The interceptors are notified when they are hooked to and unhooked from the framework, or when they are no longer required. The context object may also be passed to an initialization step.
In the example, the proposal computers are notified about the start and end of completion sessions. This enables the computers, for instance, to prepare for quick proposal retrieval by indexing and to discard any created indexes later on.
org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer
void sessionStarted();
void sessionEnded();
12.3.3 The Eclipse Extension Mechanism
The success of the Eclipse IDE is largely founded on its general mechanism of extension points: Any plugin in the platform can allow other plugins to contribute functionality at specific points. Examples are the items for menus and context menus, proposals for the Java editor’s completion popup, available database types in the Data Source Explorer, and many more. The mechanism is often said to be complex and cumbersome, but usually by people who insist on editing the plugin.xml files by hand, rather than through the provided editors. The conceptual basis and the practical use of the mechanism are really rather straightforward and neat.
The first goal of this section is to demonstrate the simplicity of the mechanism itself, by first introducing its concepts and then giving a minimal example. Having cleared away the technical issues, we can concentrate on the real challenge of building extensible systems: One has to find an API 12.3.1 for extensions that is general enough to cover all expected applications, but specific enough to keep the individual extensions small and straightforward. We will examine some existing extension points from the Eclipse IDE to approach this rather advanced topic on solid ground.
12.3.3.1 Overview
If extension points allow plugins to contribute new functionality at specific points, the key question is this: What does a typical piece of “contributed functionality” look like? At its heart, it usually consists of some callback that implements the new functionality. Many extensions can also be specified partly by data, which is often simpler than having an object return the data. For instance, a menu item is described by a string and an icon, beside an action object that implements the expected reaction. 9.3.4
Eclipse introduces yet another form of declarative indirection through commands. 174 The concrete code is contained in handlers for commands. This indirection helps in declarative bindings—for instance, from keystrokes to commands.
Extension points accept combinations of data and classes.
Fig. 12.16 illustrates the idea of passing data and code in the larger context of the overall mechanism. Some plugin A decides that it can accept contributions at some extension point E. Think of the Java completion pop-up that shows arbitrary lists of completion items. The plugin specifies a data format F and an interface I that all contributions must match. A plugin B that declares an extension must provide concrete data D and a class C implementing the interface I.
Figure 12.16 Extension Points in Eclipse
The Eclipse platform then does the main part of the job: It links all extensions belonging to an extension point across all installed plugins. It even 174 keeps track of changing sets of extensions when plugins are dynamically 12.3.3.2 added, removed, and updated. As we shall see later, the infrastructure that Eclipse provides makes it very simple for plugin A to retrieve all extensions to access their contributions.
Just in case you are wondering: Of course, extensions can also accept several pieces of data and several classes at once. Each plugin can also have several extension points and several extensions. Finally, it is common for a plugin to extend its own extension points, usually to establish some basic functionality.
Extension points use data attributes to enable lazy loading.
1.1Even if we do not usually talk about efficiency, we have to do so now to help you write professional extensions: The distinction between data and objects as attributes of extensions is key for the efficiency of the extension mechanism.
Think of a menu item sitting deep inside the menu hierarchy. It may never be invoked at all, or it may be invoked only in very special situations. To display the item, it is sufficient to know its icon and text. The actual action implementing the functionality is not required until the user happens to click on the menu item. The class mentioned in the extension needs to be loaded into memory before that.
This issue does not matter so much for single classes, but a large system will contain many thousands of such event handlers that should remain A.1 inactive until they are needed. And the OSGi framework used for managing Eclipse’s plugins can do even more: A plugin from which no class has ever been instantiated does not have to be loaded at all. Since loading a plugin involves much analysis of its structure, this is a huge benefit. It is only this setup that allows you to have hundreds of plugins installed without paying with an enormous cost at startup time: The PHP plugin will be loaded only when you open the first PHP source file; the WindowBuilder is read in only 7.2 when you try to edit a user interface widget graphically.
Learn to use the Eclipse tool support.
When developing Eclipse plugins with extensions, the same kind of editing steps recur very often. We will show the basic usage of the Plugin Manifest Editor 12.3.3.2 later with a concrete example. You should also investigate the Plugins view, from which you can easily access the core information about existing plugins. In many situations, you can take a shortcut to the information you are looking for by using the two tools described next.
Tool: Search for Plugin Artifacts
The Search/Plug-in menu opens a search dialog that can be used to find plugins, extension points, and extensions by identifiers and parts of identifiers.
Tool: Open Plugin Artifact
To jump quickly to a plugin, extension, or extension point, use Navigate/Open Plugin Artifact (Ctrl-Shift-A). The selection dialog performs quick filtering over the declarations in the workspace.
12.3.3.2 A Minimal Example
The Eclipse extension point mechanism is made to support big applications. Big mechanisms are best explained by tracing their workings through a minimal, but typical example. We will use a demo “service” provider that computes some result. See Section A.1.2 for working with plugin projects in general.
Capture the behavior of extensions with an interface or abstract base class.
The interface that follows captures the expectation for the provided “service.” It plays the role of I in Fig. 12.16. We will later add a description of the service as a string to illustrate the use of data slot F in Fig. 12.16.
extpoint.IDemoHook
public interface IDemoHook {
String computeResult();
}
In principle, the behavior of extensions can also be described by abstract 3.2.10 base classes. Using interfaces has, however, the advantage of decoupling plugins. The implementation of the interfaces can still be simplified by 3.1.4 providing a basic infrastructure in base classes.
Specifications of extension points can give both an interface and a base class for just this scenario. When you let the plugin manifest editor create the class for you, as shown later, you get a class extended to the base class and implementing the interface.
Keep declared extensions in a data structure.
Before we approach the actual connection at the center of Fig. 12.16, it is useful to look at the remainder of the implementation within the plugins A and B to get a firm grip on those details.
When retrieving the contributions, plugin A will keep them in a local data structure. In the example, it is a list of simple Contribution objects.
extpoint.DemoApp
private class Contribution {
String description;
IDemoHook hook;
...
}
private List<Contribution> contributions =
new ArrayList<Contribution>();
In the end, the contributions will be invoked whenever their functionality is required. Usually, this happens by a simple iteration, as seen in the next code snippet for the example.
extpoint.DemoApp