Design Strategies - Responsibility-Driven Design - How to Use Objects: Code and Concepts (2016)

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 couplingImage 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 concreteImage 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.

Image

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.Image 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.

Image 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 couplingImage 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.

Image 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,Image 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.

Image 12.2.1.4 Image 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 anotherImage 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.


Image 12.2.1.8One special case of achieving loose coupling is to channel collaborationImage 2.1 through the OBSERVER pattern. The subject remains entirely independentImage 2.1.2 of its observers, as long as the observer interface is, indeed, defined from the possible state changes to the subject.

Image 12.2.1.9 Image 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 particularImage 59,101 collaborations.

For instance, the MODEL-VIEW-CONTROLLER pattern insulates theImage 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 observerImage 9.2.3 relation between the model and the view, as suggested earlier.

The PRESENTATION-ABSTRACTION-CONTROL (PAC) pattern decouplesImage 59 different elements of the user interface further by making them communicate through messages. In the LAYERS pattern, each layer insulates theImage 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-FILTERSImage 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,Image 12.2.1.6 Image 1.7.2 Image 2.4.1 a mapper is an adapter that translates requests in both directions. InImage 93 general, such constructs are instances of shields; in other words, they areImage 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.

Image

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 toImage 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 simplyImage 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 haveImage 11.2 a well-defined purpose and can be implemented and tested effectively. TheImage 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 projectImage 12.2.1.5 Image 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.


Image 12.3 Image 12.4 Loose coupling, like extensibility and reusability, often serves as an excuseImage 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 previousImage 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 dataImage 9.1 behind the Package Explorer, Outline, and Navigator views and it is used for many source manipulations. The implementation hierarchy below Java ElementImage 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 correspondingImage 12.2.1.5 interface hierarchy belowIJavaElement shields other modulesImage 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 aImage 2.2 String. What is that supposed to mean? Why don’t we get the type’s treeImage 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.

Image 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 soImage 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.

Image

Figure 12.3 Coupling as Shared Assumptions


Image 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” conveysImage 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,Image 4.1 Image 6.2.3 and within the inheritance hierarchy. They think about their objects’Image 6.4.2 Image 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,Image 11.1 but much less experience than anticipating the necessity and likelihood of changes. It is similar to learning contracts, which involves describingImage 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 isImage 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 couplingImage 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 obviousImage 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 PrincipleImage 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.

Image 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 resultImage 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 armsImage 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 minimizingImage 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 andImage 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? WhatImage 11.1 are its responsibilities? The Single Responsibility Principle states that theImage 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)].Image 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.

Image

Figure 12.4 Cohesion and Separation of Concerns as Forces

An example is found in the MODEL-VIEW-CONTROLLER pattern. TheImage 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.Image 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 exampleImage 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, namedImage 158 Image 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 followImage 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 calledImage 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 elseImage 1.1 but reinforce the idea of encapsulation in compound objects.Image 2.2.3

Image

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 mayImage 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.Image 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.Image 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.


Image 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 existenceImage 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 internalImage 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,Image 1.8.2 rather than mere data structures that keep references to others. InImage 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 thereforeImage 1.1 have clearly outlined responsibilities, ideally fulfilling the Single ResponsibilityImage 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.Image 224 Already the original authors have recognized that following the law strictlyImage 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 noImage 11.2 Image 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.Image 1.8.1 It might also be a factory for objects that hides the creation itself, butImage 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'>Image12.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 EXTENSIONImage 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 objectsImage 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.

Image 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,Image 11.5.1 tactical considerations about proper information hiding, separationImage 11.5.2 Image 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

Image 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.Image 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 couplingImage 44(Item 16) Image 232 between the base class and its derived classes. We have discussed at severalImage 1.4.8.3 Image 3.1.2 points that inheritance is, indeed, a powerful construct that needs some taming to be useful. The Fragile Base Class Problem highlights mostImage 3.1.11 clearly the idea of coupling as the inability to change the base class withoutImage 12.1.1 breaking the derived classes. We have also seen that for this reason deepImage 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 moveImage 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;
...
}



Image 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,Image 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()Image 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 aImage 3.1.2 special, semi-private interface. It is given by protected services offered toImage 1.4.8.2 the derived classes and hooks, mostly for TEMPLATE METHODS, that mustImage 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.Image 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,Image 11.2.1 and the callbacks are mostly notifications using the OBSERVER pattern. InImage 2.1 Image 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.

Image

Figure 12.6 Optimizing the Public Interface


Image Image 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

Image 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 behaviorImage 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).

Image

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 commonalitiesImage 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 subsystemImage 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 accessibleImage 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 responsibleImage 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 rulesImage 8.1 act as soft locks. The workspace can also check pre-conditions of intendedImage 4.1 operations—for instance, to validate a desired project location. Dialogs can use these methods to be robust and to ensure that subsequent operationsImage 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 theImage 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 inImage 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 actuallyImage 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, leadImage 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 taskImage 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

Image 11.5.6The Dependency Inversion Principle proposes to make central parts of an application depend on abstractions, rather than concrete implementations.Image 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 allImage 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 atImage 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 TextFieldImage 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, theImage 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 onImage 7.5 Image 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 EclipseImage 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 thatImage 1.1 Image 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.

Image 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 partlyImage 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 objectsImage 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.

Image

Figure 12.8 Boundary Objects Between Subsystems

We have already seen several instances of boundary objects. For example,Image 1.7.2 facades facilitate communication from clients to service providersImage 2.4.3 inside a different subsystem. Remote proxies channel method invocationsImage 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, butImage 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.Image 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 XMLImage 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 definingImage 2.1 the listener interface, which also belongs to the boundary of the “pictureImage 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.


Image 172(Ch.8) It is often useful to combine the technique of boundary objects with theImage 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 andImage 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-conditionsImage 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. TheImage1.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 weImage 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 generalImage 12.2.1.3 enough to enable different implementations. Specifying an abstract baseImage 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 aImage 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 casesImage 11.3.2 with CRC cards, we trace through these concrete collaborations. One stepImage 4.1 Image 6.1.2 Image 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.


Image 2.1The OBSERVER pattern offers the chance of more flexible collaborationImage 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.

Image 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 remainImage 12.1 largely independent and are coupled very loosely, because none of them makes too many assumptions about the event receivers.

Image

Figure 12.9 Flexibility Through Inversion of Control

We may ask, for instance, why SWT is such a flexible and versatileImage 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.


Image The object-oriented perspective on method calls is message passing. We do not thinkImage 1.4.1 Image 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, notImage 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.Image 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 OutlineImage 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 aImage 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);
...
}



Image Image 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.

Image 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.


Image 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 worksImage 12.3 with different kinds of editors. Then, the overall strategy is covered by the INTERCEPTORImage12.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 connectImage 9.3 widgets directly to the application’s data structures instead of placing strings and images into the widgets. In addition, the user interface focusesImage 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—thatImage 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):Image 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.

Image

Figure 12.10 The Layers Pattern

Image 101 Image 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.



Image 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.


Image The attentive reader will note immediately that the JFace/SWT example violatesImage 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.

Image

Figure 12.11 The Layers Pattern


We will now examine these aspects in greater detail and analyze theirImage 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 theImage 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 applicationImage 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.

Image

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-drivenImage 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. TheImage 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 theImage 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 makeImage 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.


Image Image 12.2.1We have seen the power of special-purpose interfaces several times in the techniquesImage 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, theImage 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 ProtocolImage 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.

Image

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 ideaImage 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. TheImage 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 errorImage 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)]Image 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 allowingImage 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 forImage 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 SWTImage 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.

Image

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 importantImage 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 layersImage 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

Image 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 supportingImage 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.

Image 3.1.11 Image 3.1.4 Image 3.1.6 Because undisciplined coding quickly leads to problems, we have already examined many guidelines and techniques that tame polymorphism:

Image 3.1.1 Image 6.4The Liskov Substitution Principle dictates that new objects must obey the general behavior expected from their super-types. The interactionImage 3.1.2 between subclass and superclass should be defined explicitly through protected methods.

Image 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 newImage 3.2.2 functionality without having to extend its interface.Image 3.2.5

• The COMPOSITE pattern supports new kinds of nodes in tree-structuredImage 2.3.1 data such that recursive operations work smoothly for the new cases.

• The VISITOR pattern simplifies the introduction of new operations onImage 2.3.2 fixed object structures.

• The OBSERVER pattern transmits messages to any new kind of interestedImage 2.1 object. New behavior can then be attached in an event-drivenImage 12.2.1.8 Image 7.1 way.

• The STATE pattern covers extensions of behaviors of individual objectsImage 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.”Image 92


Extensions are best integrated by inversion of control.


Extensions are kept simple if they perform just a small and well-defined taskImage 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, theImage 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.Image 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.



Image The term “framework” is typically used in the sense of a semi-complete applicationImage 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 theImage 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 ourImage 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.Image 218

Image

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 internalImage 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 theImage12.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 seenImage1.7.2 Image 3.2.7 Image 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.


Image In fact, both the context object and the service interface in the example of JavaImage 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.Image 3.2.5



Image The INTERCEPTOR pattern focuses on black-box frameworks. The lean API makesImage 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 itImage 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.


Image 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.

Image 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 APIImage 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.Image 9.3.4


Image Eclipse introduces yet another form of declarative indirection through commands.Image 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.

Image

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 evenImage 174 keeps track of changing sets of extensions when plugins are dynamicallyImage 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.


Image 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.


Image 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 remainImage 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 onlyImage 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 EditorImage 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 abstractImage 3.2.10 base classes. Using interfaces has, however, the advantage of decoupling plugins. The implementation of the interfaces can still be simplified byImage 3.1.4 providing a basic infrastructure in base classes.


Image 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


private void invokeContributions() {
for (Contribution c : contributions) {
String descr = c.getDescription();
String result = c.getHook().computeResult();
... Use description and result
}
}



Extensions provide contributions by implementing the given interface.


The plugin offering a contribution implements the interface, as seen next. The description string will be added in a minute.

extprovider.DemoExtension1


public class DemoExtension1 implements IDemoHook {
public String computeResult() {
return "Demo extension 1";
}
}


These steps have established the main points of the connection we are interested in: the implementation of new functionality, its description in a generic interface, and its use by invoking a callback method.


Use the plugin manifest editor to define extension points.


It remains to inject a concrete DemoExtension1 object into the list contributions introduced earlier. This step is the responsibility of the Eclipse mechanism (see Fig. 12.16). In Eclipse, each plugin can contain a file plugin.xml, which declares both the extension points and the extensions that the plugin offers. The structure of the plugin.xml file is rather too detailed for editing by hand, so Eclipse provides extensive support for its modification. The Plugin Manifest Editor keeps track of the structureImage A.1.2 and understands the definitions of extension points needed to provide specialized editing support. Just click on the plugin.xml or the MANIFEST.MF of a plugin to bring up the editor. We will show only screenshots of this editor for the examples to encourage you to forget about the XML files immediately. The screenshots are also cropped slightly to save space and to focus on the essentials.

In the example, we first define the extension point E in Fig. 12.16. The definition consists of an entry in the plugin.xml [Fig. 12.17(a)], which references an extension point schema in a separate XML file [Fig. 12.17(b)]. The editing is largely done through context menus: To create a new extension point or extension, use the context menu on the list; to specify an extension further, select the extension and use its context menu.

Image

Figure 12.17 Working with Extension Points in Eclipse

Extension point schemas capture your structural assumptions about extensions. They work analogously to XML schemas or DTDs; that is, they specify the allowed structure of the extensions’ declarations. For the minimal example, we specify that the extension consists of a sequence of one or more hookProvider elements. Each such element has attributes description and class, which have types string and java, respectively. The type assignment is not shown, because it is straightforward: Just select the attributes of hookProvider in Fig. 12.17(b) and fill in the details that the editor shows. The class attribute is described further by the expected interface IDemoHook that extensions must implement.


Image The attribute name class for designating executable code is customary, but not mandatory. Any attribute can be assigned type java.



Image Extension points are declared with local IDs such as demoExtensionPoint. References in extensions are always global, prefixing the local ID with the plugin name (see Fig. 12.18 for the example).

Image

Figure 12.18 Working with Extensions in Eclipse



Use the plugin manifest editor to link extensions to extension points.


At the other end of the example, we wish to declare extensions that contribute new functionality (see plugin B in Fig. 12.16). Again, the editor in Fig. 12.18(a) provides thorough support. It interprets the schema from Fig. 12.17(b) to display fields description and class in Fig. 12.18(b). TheBrowse button is preconfigured to accept only classes implementing IDemo Hook. When creating a new extension [through the context menu of the extension point in Fig. 12.18(a)], the class link in the details even pops up a preconfigured New Class wizard.


The tool support in Eclipse makes extensions a lightweight mechanism.


These few steps finish the connection between the plugins. Using Eclipse’s tooling, they take no more than a few minutes once the interfaces and classes on both sides have been created—just don’t try to edit the plugin.xml files by hand.


Gathering all extensions for an extension point is straightforward.


From the setup in Fig. 12.16, it remains only to actually retrieve the contributions for the extension point. The required logic largely consists of boilerplate code. In the end, line 13 in the following code dumps each contribution separately into the list contributions introduced earlier. Before that, the code in lines 3–9 asks the platform for all extensions and traverses them. As can be gathered from Fig. 12.18(a), each extension has several elements, represented as IConfigurationElements. Each such element gives us access to the attributes and also helps us to interpret them: We get the description as a String in Line 10 and an instance of the class given in Fig. 12.18(b) in line 12.

extpoint.DemoApp


1 protected void queryExtensions() throws CoreException {
2 IExtensionRegistry registry = Platform.getExtensionRegistry();
3 IExtensionPoint extPoint = registry
4 .getExtensionPoint("chapter12.demoExtensionPoint");
5 IExtension[] extensions = extPoint.getExtensions();
6 for (IExtension ext : extensions) {
7 IConfigurationElement[] elems =
8 ext.getConfigurationElements();
9 for (IConfigurationElement e: elems) {
10 String descr = e.getAttribute("description");
11 IDemoHook obj =
12 (IDemoHook) e.createExecutableExtension("class");
13 contributions.add(new Contribution(descr, obj));
14 }
15 }
16 }
17



Image Image 12.3.3.3If you develop several related extension points, you can think about extracting the boilerplate code into reusable helper classes. For instance, the platform uses the (internal) AbstractWizardRegistry for different kinds of wizards that can be hooked into the IDE. Its addExtension() method performs the traversal for all cases.



Image The method createExecutableExtension() is more than a convenience: You cannot simply get the class name as a String and then use Class.forName() to loadImage A.1.1 and later instantiate the class. The reason is that OSGi associates a special class loader with each plugin. It enforces the access restrictions given in the plugin’s MANIFEST.MF file; only packages exported explicitly can be accessed from the outside. The method createExecutableExtension() therefore runs with the class loader of the plugin declaring the extension to have access to all of its defined classes. Furthermore, the OSGi class-loader allows only acyclic dependencies between plugins, as defined in the MANIFEST.MF. The plugin declaring the extension must, however, usually depend on the plugin declaring the extension point; its classes must implement the interfaces given in the extension point schema. The method createExecutableExtension() therefore accesses the class against the direction of the plugin dependencies.



Image Image 81Traversing the registered extensions involves a performance bottleneck. Since the number and sizes of plugin manifests plugin.xml in the system have become rather huge, the platform’s extension registry keeps them in memory only briefly. If the registry is required afterward, all manifests must be reloaded completely. The iteration givenImage 1.6.1 earlier must therefore not be run frequently. A common technique is to initialize the contributions lazily when they are first requested. An alternative would be to run the loop when the plugin gets initialized, but this has the drawback of making startup slower.


12.3.3.3 The Case of New Wizards

After seeing the extension mechanism at work, let us turn to the conceptual challenges of extensibility. We started our investigation of object-orientedImage 11.1 design with the example of the New Wizards in Eclipse. The overall mechanism for creating new file types was rather elaborate, as shown in Fig. 11.2 on page 571. Nevertheless, it had the advantage of being extensible: If some part of the IDE introduces a new type of file, it can also introduce a corresponding New Wizard to help the user create and initialize such a file. We will now examine how this goal of extensibility ties in with the Eclipse mechanisms.


Enable extensions by decoupling parts of mechanisms through interfaces.


As a first step, we split the network of objects shown in Fig. 11.2 along the involved plugins (Fig. 12.19). The generic mechanisms for having all kinds of New Wizards in the IDE is implemented in org.eclipse.ui. Here, we find the action to trigger the dialog and the management of the extensions. The WizardDialog itself is reused from the JFace layer. On the other side ofImage 9.3 Fig. 12.19, the Java tooling in org.eclipse.jdt.ui supplies the specifics of creating a new source file with a class definition.

Image

Figure 12.19 The New Class Wizard as an Extension

Between the two plugins, the interfaces IWizard and INewWizard (which is actually a tagging interface extending IWizard) hide the concreteImage 3.2.8 implementation classes and decouple the org.eclipse.ui plugin from the provider org.eclipse.jdt.ui. It is this decoupling that enables arbitrary other plugins to create their own wizards and to integrate them with the existing infrastructure.


Real-world extension points are no more complex than the minimal one.


To understand the interaction between the plugins in more detail, let us look at the definition of the extension point org.eclipse.ui.newWizards. The main points of its schema are shown in Fig. 12.20(a): The class must implement INewWizard [Fig. 12.20(b)]. The attributesname, icon, and category are data elements that describe the wizard for the user’s benefit.

Image

Figure 12.20 Definition of the New Wizard Extension Point (Abbreviated)


Manage extensions in separate objects and base classes.


As a final level of detail, we will dig down into the code that actually creates the connection between the two plugins in Fig. 12.19. We saw earlier thatImage 12.3.3.1 the plugin declaring the extension point must actively look for all extensions (Fig. 12.16 on page 686). The implementation in Eclipse takes into account that much code in a naive implementation is actually boilerplate and factorsImage 12.3.3.2 this code out into reusable helper objects and reusable base classes. It isImage 1.8.5 Image 3.1.4 worth studying this infrastructure: It helps when one is implementing new extensions, and more importantly it demonstrates how professionals capture common logic and keep variabilities apart.Image 12.2.1.2 Image 70

For the New Wizards, the class NewWizard initiates this processing of extensions when the wizard dialog is opened.

org.eclipse.ui.internal.dialogs.NewWizard


public class NewWizard extends Wizard {
...
public void addPages() {
IWizardRegistry registry =
WorkbenchPlugin.getDefault().getNewWizardRegistry();
IWizardCategory root = registry.getRootCategory();
IWizardDescriptor[] primary = registry.getPrimaryWizards();
...
}
...
}


In fact, the IWizardRegistry here is implemented by NewWizard Registry, which derives from AbstractExtensionWizardRegistry. That class is also used for import and export wizards. Its method doInitialize()Image 1.6.1 is called during lazy initialization. It delegates the actual work to a WizardsRegistryReader. (The methods getPlugin() and getExtensionPoint() delegate to the derived classes, here to newWizardRegistry.)

org.eclipse.ui.internal.wizards.AbstractExtensionWizardRegistry


protected void doInitialize() {
...
WizardsRegistryReader reader =
new WizardsRegistryReader(getPlugin(),
getExtensionPoint());
...
}


The class WizardsRegistryReader derives from RegistryReader, which is a helper that basically encapsulates the processing loop for extensionsImage 12.3.3.2 Image 1.4.9 shown in the minimal example. Using TEMPLATE METHOD, the specific subclasses process the single elements—in our case, the wizard contributions.

org.eclipse.ui.internal.registry.RegistryReader


public void readRegistry(IExtensionRegistry registry,
String pluginId,
String extensionPoint) {
IExtensionPoint point = registry.getExtensionPoint(
pluginId, extensionPoint);
IExtension[] extensions = point.getExtensions();
extensions = orderExtensions(extensions);
for (int i = 0; i < extensions.length; i++) {
readExtension(extensions[i]);
}
}


We stop at this point, because the remainder is clear: Through a number of indirections introduced by TEMPLATE METHOD, the wizard extensions arrive back in WizardRegistryReader, which creates WorkbenchWizard Elements. If you are interested, use the tools Open Declaration and Open Implementation to trace the calls through the sources.


Image This setup might seem unduly cumbersome: Why should a sane designer dispatch a simple iteration through the extensions up and down through an inheritance hierarchy rather than implementing a quick-and-dirty solution? More critical team members might even ask who will ever understand this mess. The answer is that object-oriented systems are not understood by tracing method calls at all. Instead, they are understoodImage 11.1 by looking at the responsibilities of the different objects and their distribution across anImage 11.4 inheritance hierarchy. In the end, the goal is to enact the Single Responsibility Principle:Image 11.2 Each class in the hierarchy addresses a single task, which maximizes the cohesion withinImage 12.1.3 the classes of the system. Try to check this out in the example; you will find that the design obeys the single-sentence rule for describing an object’s purpose.Image 11.2.1


12.3.3.4 The Case of Views in Eclipse

Designing useful extension points hinges on finding a clean abstraction that covers the intentions of many possible extensions. The challenge is to find that abstraction, to formulate it succinctly, and to express the behavior in an interface. Aiming for the Single Responsibility Principle helps a lot.Image 11.2.2 Here is a concrete example.


Views are just windows that appear in the Eclipse IDE.


You are familiar with views from your everyday work in Eclipse. The Package Explorer, the Console, the Variables, and many more—they all provide extra information that is useful while editing or debugging sources. Views are, of course, not the only kind of windows in Eclipse. For instance, the actual sources are accessed in editors, which appear in the central editor area. We focus on views here because they are a little simpler and still exhibit the essential aspects.

Let us examine the software infrastructure that makes it simple to add a new view. Fig. 12.21(a) shows the extension point org.eclipse.ui.views. The central attribute is a class deriving from ViewPart [Fig. 12.21(b)] or implementing IViewPart, as explained in the documentation of the extension point. We will look at their methods in a minute. The remaining attributes of the extension point describe the view using simple data such as a name, an icon, and a category to be used in menus and the Show View dialog. The class will not be loaded until the view is shown.Image 12.3.3.1

Image

Figure 12.21 The Extension Point org.eclipse.ui.views


Image ViewParts can be edited with the WindowBuilder. The tool then fills in the creationImage 7.2 of the SWT widget tree but leaves you free to implement the other methods as necessary.



Image The extension point and some infrastructure are defined in the plugin org.eclipse.ui.Image A.1.2 If you wish to build a new view, be sure to include that plugin in your dependencies. The online supplement contains a minimal example MiniView with which you can start playing around.



Ask yourself about the minimal requirements on extensions.


Let us now look at the structure of a view in more detail. Most facets result from the fact that Eclipse shows views in tab folders and decorates them with a title, an icon, and a tooltip. The interface IWorkbenchPart, from which IViewPart derives, deals with these general aspects. The viewImage 7.1 also has to create its SWT widget tree. The method dispose() is a life-cycle callback enabling the view to clean up—for instance, by de-registeringImage 1.1 listeners or freeing resources. Method setFocus() allows the view to moveImage 7.4.1 the keyboard focus to some default widget when the user clicks on the view title. The view must notify Eclipse about its properties. Views have onlyImage 2.1 a title; editors additionally can be dirty or change the edited resources, as seen in their interface IEditorPart. The super-interface IAdaptable hasImage 3.2.2 already been discussed.

org.eclipse.ui.IWorkbenchPart


interface IWorkbenchPart extends IAdaptable {
public String getTitle();
public Image getTitleImage();
public String getTitleToolTip();
public void createPartControl(Composite parent);
public void dispose();
public void setFocus();
public static final int PROP_TITLE =
IWorkbenchPartConstants.PROP_TITLE;
public void addPropertyListener(IPropertyListener listener);
public void removePropertyListener(IPropertyListener listener);
public IWorkbenchPartSite getSite();
}


The last method getSite() in this code needs some more background. Each view and each editor in Eclipse is anchored to the site at which it currently appears. The site enables each window to access its own context on the screen. For instance, the page contains all views and editors andImage 174 manages their layout; the workbench window holds the top-level shell withImage 7.1 menu, toolbar, and status bar.

The site is passed to the view during initialization. Since its type differsImage 1.6.2 between editors and views, the corresponding life-cycle method init() is declared in the IViewPart interface shown next. The methods dealing with IMemento, as well as the interface IPersistable, enable a view to save andImage 9.5.1 restore its own state between IDE sessions. Eclipse manages the mementos for all views, which again simplifies the creation of new views.

org.eclipse.ui.IViewPart


public interface IViewPart extends IWorkbenchPart, IPersistable {
public void init(IViewSite site) throws PartInitException;
public void init(IViewSite site, IMemento memento)
throws PartInitException;
public void saveState(IMemento memento);
public IViewSite getViewSite();
}



Image The VIEW HANDLER pattern provides an abstract analysis of applications managingImage 59 internal windows. It explains the general form of protocols that negotiate the life cycle and manipulation of these windows.



Aim at making extensions simple.


Image 12.3.1So why does this setup simplify the creation of new views? The answer is clear: Eclipse provides a substantial amount of machinery for showing and managing individual views; it even takes care of any internal state that the view might wish to preserve between sessions. All that Eclipse asks for in return is a little collaboration at those points that are really special for each view: the title, the SWT widget tree, the handling of their state, and so on. But Eclipse does even more. The ViewPart base class provides theImage 3.1.4 infrastructure for the menial tasks such as managing the property listeners. In the end, developers of views can really concentrate on their own tasks and can trust Eclipse to integrate their views into the overall IDE.

12.3.3.5 Completion Proposals

One of the most versatile features of Eclipse’s Java editor is its completion pop-up. It does more than simply list the names of types, methods, and fields: It also includes common code patterns such as a main() method,Image 1.8.6 System.out.println(), or Runnables, and goes on to code recommenders supplying example snippets for API usage. This large variety of functionality could never be maintained effectively in a monolithic implementation, because the Java editor component would become huge. Instead, the editor provides a common abstract structure of proposals that subsumes all individual entries.

The example is particularly interesting in the current context, because it demonstrates the step away from the concrete functionality to the abstract structure of expected functionality. It also shows how additional infrastructure classes can bridge the contrasting goals of an abstract, general API andImage 12.3.2 the simplicity of extensions. Compared with the earlier analysis of the same example, we therefore switch our perspective and focus on the abstraction, rather than on the mechanics.


Proposals are provided dynamically, depending on the pop-up’s context.


Let us start from the goal: We want to show the pop-up window that you are familiar with. Many of the proposals will be generated dynamically, such as by looking up field and method names in the Eclipse Java Model. The extension pointorg.eclipse.jdt.ui.javaCompletionProposalComputer [Fig. 12.22(a)] therefore accepts sequences of objects implementing the interface IJavaCompletionProposalComputer [Fig. 12.22(b)].

Image

Figure 12.22 Extension Point for Java Completions

That interface is simple enough: Each completion proposal computer at the core provides a list of ICompletionProposals, given a context in which the completion takes place. The context includes, among other things, the text document and completion position, so that the textual context can be accessed directly. The sessionStarted() and session Ended() callbacks are life-cycle methods that notify the computer when a pop-up is shown and is removed, respectively. This enables the computer to allocate resources or caches temporarily and free them once no completion operation is immediately pending. The computer can provide an error message explaining why it has not been able to find any proposals.

org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer


public interface IJavaCompletionProposalComputer {
List<ICompletionProposal> computeCompletionProposals(
ContentAssistInvocationContext context,
IProgressMonitor monitor);
void sessionStarted();
void sessionEnded();
String getErrorMessage();
...
}


One detail of this API is worth noting, because it demonstrates the experience that is required for designing extensible systems: The core methodImage 7.10.2 completeCompletionProposals() is also handed a progress monitor. The designers have obviously anticipated the need to search indices or raw lists of items, maybe even opening and analyzing JAR files from the class path.


Image If you wish to implement new proposals, note that the context passed to the callback is actually a JavaContentAssistInvocationContext. That object already holds some analysis of the syntactic context that may help to identify relevant proposals quickly. Please look at existing proposal computers for example usages.



A proposal is an executable modification together with its description.


Let us now dig one layer deeper into the abstraction. The ICompletion Proposal aims at showing a simple text-and-icon list to the user. It isImage 9.3 independent of the Java editor and is defined in the JFace abstraction over SWT’s raw text widgets. A proposal is described by a text, an icon, and some additional information that pops up beside the list. The final two methods capture the execution of the proposal: apply() changes the document to incorporate the proposal; getSelection() guides the editor to an editing position so that it is convenient for the user after accepting the proposal.

org.eclipse.jface.text.contentassist.ICompletionProposal


public interface ICompletionProposal {
String getDisplayString();
Image getImage();
String getAdditionalProposalInfo();
void apply(IDocument document);
Point getSelection(IDocument document);
...
}


The first interesting point in this API is the generality of the apply() method. Many proposals are about completing some method or type name. Focusing on those would lead to a method getCompletionString (prefix), which would be simple to implement, because it does not involve any logic about the context. However, that design would not coverImage 1.8.6 the more advanced cases such as wrapping code in Runnables. By comparison, the general version can be hard to implement if each type of completion proposal would actually have to manipulate documents. The solution is to provide base classes with infrastructure that keeps the API general butImage 3.1.4 simplifies the common case. Here is the apply() method in Completion Proposal that keeps simple proposals purely data-driven; one simply has to store the textual replacement in fields.

org.eclipse.jface.text.contentassist.CompletionProposal


public void apply(IDocument document) {
try {
document.replace(fReplacementOffset, fReplacementLength,
fReplacementString);
} catch (BadLocationException x) {
// ignore
}
}


The new cursor position is set to the end of the proposal, so that the user can continue typing immediately.

org.eclipse.jface.text.contentassist.CompletionProposal


public Point getSelection(IDocument document) {
return new Point(fReplacementOffset + fCursorPosition, 0);
}



Use extension interfaces for backward compatibility.


Another interesting point of the completion API is that it has already been updated seven times to provide more advanced feedback for the user and more detailed control over the application of proposals. This goes to show that even experts are not omniscient and cannot anticipate all desirable application scenarios of an API. The EXTENSION INTERFACE pattern canImage 3.2.5 be used to keep existing proposals working while new proposals can take advantage of the new facilities. Look through the interfaces ICompletion ProposalExtension,ICompletionProposalExtension2, and so on for the details: There are special provisions for an on-the-fly validation, selection by trigger characters, disabling auto-application, styled strings rather than plain text for displays, and more.


Extension interfaces complicate the reusable mechanisms.


EXTENSION INTERFACE ensures backward compatibility, but it comes with a price: The code invoking the new functionality must use case distinctions and downcasts. As a result, it quickly becomes very complex. In the example,Image 3.1.9 extension 6 introduces styled strings—for instance, to highlight parts of a proposal in boldface font. The code that displays a proposal must take this new capability into account whenever it looks for the display string. Lines 5–9 in the following code perform the check and downcast; the meanings of the variables in lines 3–4 depends on the case distinction.

org.eclipse.jface.text.contentassist.CompletionProposalPopup


1 private void handleSetData(Event event) {
2 ...
3 String displayString;
4 StyleRange[] styleRanges = null;
5 if (fIsColoredLabelsSupportEnabled
6 && current instanceof ICompletionProposalExtension6) {
7 StyledString styledString =
8 ((ICompletionProposalExtension6) current)
9 .getStyledDisplayString();
10 displayString = styledString.getString();
11 styleRanges = styledString.getStyleRanges();
12 } else
13 displayString = current.getDisplayString();
14 ...
15 }



Strike a balance between simplicity now and later.


The discussion so far highlights a central dilemma when designing the API of extensions: You can start out with the cost of a simple, straightforward definition that works for all cases currently on the radar, or you can invest more time to build for more sophisticated applications that might arise in the future. The second choice clearly comes with the cost of a higher implementation complexity, while the first choice involves the danger of deferring that complexity to the future, where the amount of work and the ugliness of the code will be very much increased.

In the end, there is no general guideline. The idea of building theImage 28 simplest thing that could work may help a bit: “Work” can be read to mean “cover all desirable extensions,” an understanding that asks you to look conscientiously for applications that you do know about. “Simplest” then means to ask which collaborations are really indispensable for these applications. Styled strings are a case in point: Eclipse’s auto-completion capability would still be immensely useful, even if slightly less beautiful, without them. Since they involve advanced uses of the SWT Tablewidget, it is a good idea to defer their addition until some users actually ask for them.

12.3.3.6 Extension Points and Extensibility

The Eclipse mechanism for extensions is great as a mechanism: It is conceptually clean, poses few restrictions on the format of extensions, and comes with a lot of supporting infrastructure and IDE support. It is certainly worth studying by itself, as an example of craftsmanship in software design. We learn even more, however, by taking a step back and asking how it contributes to the overall goals of extensibility and, one level beyond, to decoupling in general.


Extension points keep extensions small and simple.


One of the main requirements for extensibility is that one can add new functionalityImage 12.3.1 in a simple and well-defined way. If you look back at the examplesImage 12.3.3.2 Image 12.3.3.3 so far, especially to Fig. 12.19 on page 694, you will notice that almost the entire overhead for making things extensible lies with the provider of the extension point. The individual extensions consist of objects implementing usually small interfaces. Registering these objects with the mechanism is done through a few mouse clicks using Eclipse’s editors.


Extension points help to keep the extensible plugin stable.


Extensibility requires that the application’s code base remain stable despiteImage 12.3.1 the added functionality. The Eclipse mechanism encourages the necessary decoupling in that it forces the plugin offering the extension point to define an interface or abstract base class that captures all of its expectations (see Fig. 12.16 on page 686). Defining this abstraction can be hard, but at least it is approached early on, before many extensions exist that must possibly be rewritten. The design of this interface is best achieved by asking about the future behavior that extensions are expected to exhibit.Image 3.2.1

From one perspective, extensibility therefore leads to an instance of the Dependency Inversion Principle. In this case, the main application is theImage 11.5.6 stable, crucial, valuable part of the software. The extensions are incidental details whose implementation must not influence or constrain the design of the main application.


Extension points encourage decoupling through interfaces.


The necessity of specifying an interface or abstract base class as the single link between the plugins participating in an extension implicitly introduces decoupling between the modules. On the one hand, changes are likely to remainImage 3.2.7 Image 12.1.1 confined in the individual extensions. On the other hand, the interface makes explicit the assumptions that the participating plugins share.Image 12.1.2

12.3.4 Pipes and Filters

Many applications today feature graphical user interfaces and are largely event-driven. Nevertheless, the good old input–process–output model stillImage 7.1 holds its own at the heart of many computations. Indeed, the advent of affordable multicore and multiprocessor hardware has made the model everImage 57,207,8 more important: Throughput in bulk processing of data is best obtained by organizing the processing steps in producer–consumer chains in which dataImage 7.10.1 is copied along. In this way, each processor can run at full speed, without being hampered by waiting for locks on shared data structures.Image 8.1 The PIPES-AND-FILTERS pattern captures the essence of such data processing systems.Image 101,59


Networks of filters and pipes make for extensible systems.


Fig. 12.23 illustrates the idea of the pattern. Data flows into the system, often at several points (A and B). Then it is split and recombined through several processing steps (C, D, E) and finally the results leave the system, perhaps again at several points (F, G).

Image

Figure 12.23 A Network of Filters for Data Processing

The nodes AG are called filters. Filters receive data, process it, and hand the resulting data on. Filters at which the data enters the system are called sources; those where the data leaves the system are called sinks. The connections between the filters are called pipes. Briefly put, they are responsible for transporting the data between the processing steps. If the network is simply a linear sequence of filters, one speaks of a pipeline.

The most famous examples of pipelines are perhaps the versatile UNIX text processing tools. Suppose you have received the year’s banking transactions as a CSV file, with one transaction per line. Now you wish to know which people you have sent money to most often. Here is the trick: Fetch the data from disk (cat), splice out the relevant columns (cut), and select only those with a negative flow (grep). Finally, you splice out only the names (cut) and count the number of occurrences. This works most simply by sorting (sort) and counting the number of consecutive lines with the same content (uniq -c). As a final touch, you sort numerically by the number of transactions, in descending order (sort -nr).


cat transactions.csv|cut -f6,8,9|grep -e '-[0-9.]*$'\
|cut -f1|sort|uniq -c|sort -nr



Pattern: Pipes-and-Filters

The Pipes-and-Filters approach provides a structure for systems that process streams of data. The filters process the data; the pipes connect the filters and transport the data from one filter to the next. Filters form a directed acyclic network, where sources receive data from the outside and sinks pass the result on to other systems.


Image 59The pattern provides extensibility in two directions. First, one can introduce new kinds of processing steps by implementing new types of filters and inserting them into the network. Second, one can recombine existing filters to add new processing steps to existing data flows. As a further benefit, the pattern enables the rapid construction of new applications from a toolbox of existing filter types.

To reach these goals, three technical constraints have to be observed: a common data format, the nature of data as streams, and the resulting need for incremental processing. We will introduce these requirements now, before proceeding to a more detailed analysis of the pattern itself.


All filters share a common data format for input and output.


The pattern’s first and main constraint concerns the format of the data. To enable the free combination of existing filters and the easy integration of new filters, they have to agree on a common data format: The output of any filter must be a suitable input to the next.

In many applications, the data is just a sequence of bytes—that is, one filter produces bytes to be consumed by the next one. However, the bytes usually have further structure. For instance, when processing audio data, the bytes encode samples, but they must be grouped according to the number of bits per sample and maybe further into blocks with checksums to enable error correction. UNIX text processing tools read text files and produce text files, but they usually work on lines terminated by a linefeed (LF) character. Database systems, in contrast, work in terms of pages or individual records of fixed formats.


Assume that the data arrives in the form of a potentially infinite stream.


A further central point of the pattern shapes the later implementation decisions to a large degree: One assumes that the data is large or even unbounded. A stream is such a potentially infinite sequence of data items. For instance, when building the audio processing software for a conference system, you can never be sure whether the current speaker will ever stop. And even if the data is known to end at some point, it may be infeasible to store a modified copy at each filter.


Filters aim at incremental processing for low latency.


The way to cope with large or unbounded data is to process it incrementally (Fig. 12.24): Each filter reads just enough data to be able to perform its computation, does all the processing, and passes the result on to the next filter as soon as it is done.

Image

Figure 12.24 Incremental Processing of Data Streams

In effect, the first pieces of the result stream appear at the sinks of the overall filter network at the first possible moment. The time taken for a piece of data to travel through the network is called the network’s latency. In many applications, such as real-time signal processing, a low latency is crucial to achieve a satisfactory user experience. But even in data processing systems and application servers, it is a good idea to start sending data over the network as soon as possible: Transmitting the computed data will take some time, and during that time the next batches of data can be processed. As a result, the overall time that elapses before the user sees the complete web page is minimized.


Pipes synchronize processing between filters.


Having set the goals, we can now come back to the elements of the pattern. First for the more obscure ones: If all the processing is done by the filters, what is the purpose of the pipes? What do they contribute?

Pipes are responsible for transporting the data between filters. This also means making sure that no data gets lost, even in unfavorable circumstances where the producer is much quicker than the next consumer in the network. Simply dropping data is usually not an option. Instead, the pipes have to synchronize the processing between filters so that fast producers get slowed down a bit and quick consumers get to wait until new data is available.

The simplest form of pipes occurs with passive filters. Here, the transport of data happens by method calls. Either the consumer asks the producer for new data or the producer delivers new data to the consumer. In any case, the method call will not return until the operation is complete. This type of filter can be studied by examining the readers and writers of Java’s I/O library. For instance, it is instructive to study how the GZIP OutputStream and GZIPInputStream achieve an on-the-fly compression and decompression of byte data.

Image 8.1 With active filters, in contrast, each filter runs in a separate thread. It usually has a main loop that repeatedly reads data from its input, processes it, and writes it to the output. Active filters support parallel and distributed processing. To synchronize active filters, the pipes must buffer incoming data. They keep the consumer waiting if no data is available and stop theImage 8.3 producer when the buffers are full. We have already studied the standard BlockingQueue implementations that fulfill this purpose.

Pipes can also include buffering to improve the throughput of the overall network. The throughput is the amount of data processed in a given time. This amount is maximized if all filters run in parallel and at top speed. To do so, they must have sufficient input data and must hand on their data without delay. Such a scheme will work as intended only if the pipes buffer data to level out temporary discrepancies in processing speed.

In summary, pipes are not merely auxiliary connectors. They form theImage 11.5.2 backbone of the overall application. They also establish a separation of concerns: The filters can assume that their input data is available and will be transported reliably, so that they can focus on the actual processing of the data.


Filters implement elementary processing steps.


When designing with the PIPES-AND-FILTERS pattern, it is important to get the granularity of the filters, and therefore that of the individual processing steps, right. If you have just a few large-scale blocks, the pattern structures your application, but you do not gain the ability to recombine filters into new applications quickly. If you have a huge number of tiny microprocessing steps, the time spent in transporting the data dominates the overall runtime.

A good guideline is to look at the application domain. Which kinds of “operations” on data are discussed there? Which elementary steps are mentioned in the descriptions of overall systems? Making the meaning of operations—rather than the size of their implementations—the unit of filters helps to strike a balance between the extremes. At the same time, it ensures that the structure of the network resembles the description of the desired application.


Filters are reusable between applications.


We have placed PIPES-AND-FILTERS into the context of extensibility, because its central feature of a common data format shared between filters makes it simple to extend the toolkit of filters. However, there is a second major gain: Once the filters have been developed, they can be reused inImage12.4 new applications.


PIPES-AND-FILTERS enables rapid prototyping and product lines.


Furthermore, the overall structure and the strong basis established by the pipes makes it simple to assemble applications quickly, either for rapid prototyping or in the sense of product lines, where a number of very similar software products must be maintained side-by-side.


PIPES-AND-FILTERS focuses on stateless computing.


Objects are usually stateful, which enables them to be self-contained andImage 1.1 active entities. It must be said, however, that purely functional, immutableImage 1.8.4 objects do have some advantages when it comes to debugging complex computation processes.

The main point of the PIPES-AND-FILTERS pattern is the transformation of data, and that data is stateless: You can inspect the input and output data of filters and be sure that the same stream of input data will lead to the same stream of output data. Filters are essentially functions from theirinputs to their outputs. For the implementation, this means that even if you do pass objects along the pipes, you must not modify them as long as any other filter might have access to them.

However, the filters themselves can and very often will, in fact, be stateful. As a result, even if the same group of bytes repeats at different positions within the overall stream, different output may result. For instance, compression algorithms usually optimize their dictionaries dynamically. Encryption algorithms even take great care to avoid repeating the same output for the same input in different positions.


PIPES-AND-FILTERS defines specific responsibilities and collaborations.


At first glance, PIPES-AND-FILTERS is not particularly object-oriented: After all, object design is first of all about behavior and not about data. However, the pattern can also be understood as a particular style of assigning responsibilities and defining collaborations. The responsibility of each filter is a particular data transformation. All collaborations rely on handing on data for further processing.

Image 92 Of course, a central objection remains: Published data structures are usually considered a “bad smell” in a design. However, we must make a fine distinction. The published part of the data is what the system is all about: An audio system processes audio data, a database management system processes tuples, and so on. The internal state and data of the individual filters, in contrast, is still private. This distinction aims at the question ofImage 11.5.1 Image 12.1.1 which aspects are likely to change. We practice information hiding because we wish to change an object’s implementation if necessary while the remainder of the system remains untouched. But we will never try to turn an audio system into a database management system, so it does not really matter if their central data structures are published and therefore fixed.

12.4 Reusability

Reusability is all about solving a problem once and for all. Indeed, computer science itself would be nothing without software reuse. In the beginning, when we punched our source code into cards, reuse happened through “libraries,” where one could literally borrow a batch of cards and pack it with one’s own computation jobs. Since then, we have come a long way: Libraries are just JAR files that we download from the web, possibly even using anImage 173 automatic build system such as Maven. Nevertheless, creating reusable software and reusing software created by others is still a tough job. Even if the technical challenges are largely solved, the conceptual ones have increased with the growing size and complexity of the systems we build.

12.4.1 The Challenge of Reusability

Let us start from a simple definition: Reusable software solves a particular problem and works in many contexts. You can just use the off-the-shelf software in your own applications. Unfortunately, this characterization assumes that the software already exists. To get a grasp on the process of creating reusable software, it is better to start from one application. Assume for a moment that you are thinking about a task for an object and you suddenly realize that this task will reoccur in many places. How do you transform that insight into reusable software?


Reuse means extracting a component and embedding it into a new context.


Fig. 12.25 illustrates the core of the reuse challenge. We have written a component R for one system, but we think it is reusable somewhere else. In a truly object-oriented design, R is embedded in a network of objects:Image 11.1 A accesses R as a service provider, indicated by the direction of the arrow. B additionally expects some callbacks, such as notifications about state changes. On the right-hand side of Fig. 12.25, R itself uses collaborators to fulfill its responsibilities. It calls on C for some aspects and collaborates with D more closely, including some callbacks.

Image

Figure 12.25 The Challenge of Reuse

The challenge of reusing R in a different context then is that we have to cut these connections as sketched by the dashed lines and reestablish them in the new context, with different objects. This perspective of extracting R from its original context is useful even if you have not yet implemented R: You will find R first useful at one place of a design, and only then will you start thinking about making it reusable in other places.


Single objects are usually not a suitable unit of reuse.


Before we proceed, it must be clear that the component R in Fig. 12.25 is usually not a single object but really a group of objects that together accomplish a purpose that is worthwhile reusing—the Single ResponsibilityImage 11.2 Principle would not allow for anything else, because it keeps objects small and focused. In particular, R will have internal collaborations. C and D stand for the external ones that vary between contexts. However, it might be aImage 1.7.2 good idea to make the functionality of R accessible through a FACADE object to keep the API simple for the users A and B.


Enable reuse by designing general and conceptually clean APIs.


Replacing object A in Fig. 12.25 means designing the API of R such that it is generally useful. A prime example is the operating system API for accessing data in the form of byte streams. It has survived since the 1960s and even transfers to TCP network connections, because it is lean, clean, and general: Making the byte the smallest unit of information enables any application A to encode and decode its own data structures by marshaling them into some convenient format, while the operating system R serves all applications the same way.


Reusability often induces inversion of control.


Image 7.3.2Inversion of control is a characteristic of frameworks that distinguishes them from libraries: Frameworks implement generic mechanisms that work on their own. The application’s own logic is called back only at specific and well-defined points.

Fig. 12.25 shows that this principle evolves naturally when designing reusable objects. In the figure, collaborators C and D must be plugged into R in each new context. They get called back whenever R judges this to be useful. Only reusable components R that rely entirely on their own objects for help do not have such callbacks—they are the libraries.

The challenge in designing R is to find interfaces for C and D such that these collaborators can be implemented easily by different users of R. One particular aspect, which will arise in many later examples, is the questionImage 12.1.1 of which information R must pass to C and D. If R makes available too much or reveals its own details, it cannot change. If it offers too little information, C and D cannot implement any useful functionality and R is not reusable.

The remainder of this section explores examples of reusable components to make more concrete these challenges and their solutions.

12.4.2 The Case of the Wizard Dialogs in JFace

To start with reuse, let us look at an example of a successful, polishedImage 9.3 component: the WizardDialog object provided in the JFace layer. We haveImage 11.1 already seen it at work in the NewWizard mechanism of Eclipse, which enables users to create arbitrary files with dialogs specific to the type of file. The overall network of objects is rather complex, because it obeysImage 11.2 the Single Responsibility Principle and aims at several higher-level goals (Fig. 11.2 on page 571). One of these goals is the reusability of the wizard infrastructure.

Let us now examine the relationships of the WizardDialog to the other classes of the overall structure. Fig. 12.26 casts the first dialog page for selecting the type of file [Fig. 11.1(a) on page 570] into the general structure of the reuse scenario from Fig. 12.25.

Image

Figure 12.26 Reusing the WizardDialog for New Wizards


Provide a simple service API for accessing reusable components.


On the left-hand side of Fig. 12.26, the WizardDialog offers a general and straightforward service API for showing wizards. In this API, one creates the dialog, passes an IWizard, and opens the dialog. The specific wizard becomes part of the dialog and the dialog starts managing the wizard.Image 2.2.1

org.eclipse.ui.actions.NewWizardAction


NewWizard wizard = new NewWizard();
... initialize the NewWizard
WizardDialog dialog = new WizardDialog(parent, wizard);
dialog.create();
...
dialog.open();


This is simple enough. The crucial design decisions take place on the right-hand side of Fig. 12.26: How will the generic WizardDialog and the specific contained wizard object collaborate to provide the overall user experience?


Keep the interfaces for callbacks conceptually clean.


In the direction from the WizardDialog, the interfaces IWizard and IWizardPage capture the expectations of the dialog toward the specific wizard provided by the concrete application. First, there are some life-cycle methods that connect the wizard to its container and set up and tear down the wizard (lines 2–5). We will discuss the interface IWizard Container further later in this section. Next up, the dialog enables navigation through the pages by its Next and Previous buttons; it calls back on the wizard to decide on the actual page selected (lines 6–9). Finally, the dialog has buttons Finish and Cancel. It lets the wizard decide whether the Finish button should be active, based on whether sufficient information is available to finish immediately (line 10). Finally, it notifies the wizard when one of the buttons is actually clicked.

org.eclipse.jface.wizard.IWizard


1 public interface IWizard {
2 public void setContainer(IWizardContainer wizardContainer);
3 public IWizardContainer getContainer();
4 public void createPageControls(Composite pageContainer);
5 public void dispose();
6 public IWizardPage[] getPages();
7 public IWizardPage getStartingPage();
8 public IWizardPage getNextPage(IWizardPage page);
9 public IWizardPage getPreviousPage(IWizardPage page);
10 public boolean canFinish();
11 public boolean performCancel();
12 public boolean performFinish();
13 ...
14 }



Image The alert reader will notice some apparent redundancy between the method getPages() and the subsequent ones. The difference is in their semantics. The method getPages() returns the pages in the physical order of their creation, while getStarting Page(),getNextPage(), and getPreviousPage() represent the logical order in which the pages appear in the wizard. For instance, some steps may be optional depending on previous user input. It is instructive to look through different implementations within the Eclipse platform. (Use the tool Open Implementation from page 180.)


Image 12.4.1This interface underlines the earlier statement that reusable object-oriented components usually introduce some level of inversion of control. It is the wizard that decides when pages flip, what the next page will be, and when the overall operation should be triggered. The application—that is, the software reusing the dialog—has to wait until it is called back.


Aim at covering all application scenarios.


The methods omitted in the presentation of IWizard so far deal with seemingly minor issues of the graphical presentation. However, they demonstrate a further challenge in creating truly reusable components: It is not sufficient to target 90% of the possible applications; one must reach 100% to avoid forcing clients to adopt copy-and-paste programming in more subtle cases.

In the case of wizards, it is useful to specify the window title and the color of the upper part of the dialog. Also, the appearance of the help, next, and previous buttons might not be sensible for all wizards. Finally, if wizards perform long-running jobs, the user will appreciate the display of a progress bar.

org.eclipse.jface.wizard.IWizard


public String getWindowTitle();
public RGB getTitleBarColor();
public boolean isHelpAvailable();
public boolean needsPreviousAndNextButtons();
public boolean needsProgressMonitor();


The other interface IWizardPage was discussed earlier (see page 572).Image 11.1 It is worth reexamining its methods from the perspective of reusability: What does the WizardDialog expect of the single pages of a contained wizard?


Strike a balance between completeness and effort.


The general aim is to cover 100% of the later applications. But hold on for a minute—this is quite some deviation from the earlier principles. What about “build the simplest thing that could work” and “you ain’t gonna need it (YAGNI)”? There are really three answers.

The first answer is: Very often, covering 95% of the applications is good enough, because these are all the scenarios you are ever going to need in your software. And creating a component that covers 95% is often substantially simpler than building one to cover 99% or 100%. So you should still be building the simplest possible thing.

The second answer is that building frameworks and reusable software shifts the priorities slightly: If you decide to build a reusable component, then do it once and be done with it. Reusability introduces an extra effort and you should make sure that the effort is spent well.

The third answer is: If you find it hard to create a reusable component that covers even 95% of the applications, then you probably should not try to create it right now. Perhaps it is better to follow the “rule of three”: Build three concrete applications by hand before attempting the abstraction.


Think carefully about the information and services offered to callbacks.


But the right-hand side of Fig. 12.26 holds yet another challenge: Not only does the WizardDialog call back on the wizard and its pages for specific events, but it also offers services to the wizard. These services are given by an interface IWizardContainer, so that wizards can also be shown in other contexts. Through the interface, the wizard can access the shell—for instance, to show pop-up dialogs. It can also directly influence the pageImage 7.6 shown by the wizard, in case the concrete data entered by the user or any errors require a deviation from the standard, linear page sequence. The final update ... methods are really notifications sent to the dialog: Whenever a page detects that the data has changed in a significant way, it must notify the dialog that the display must be updated.

org.eclipse.jface.wizard.IWizardContainer


public interface IWizardContainer extends IRunnableContext {
public Shell getShell();
public IWizardPage getCurrentPage();
public void showPage(IWizardPage page);
public void updateButtons();
public void updateMessage();
public void updateTitleBar();
public void updateWindowTitle();
}


The challenge of reusability is perhaps most prominent in these service interfaces for collaborators. Let us go back to the general situation in Fig. 12.25 on page 711, which is also reflected in Fig. 12.26.

The services for A and B on the left-hand side are really clear-cut, because they derive from the purpose of the reusable component R itself. The interfaces from the component R to its collaborators C and D on the right-hand side of Fig. 12.26 are also rather straightforward, because they can be defined along the minimal requirements of the component. But the final interfaces, from collaborators C and D to the reusable R are really hard: There are no general guidelines that would enable us to gauge the expectations and requirements of these collaborators. Only experience and analysis ofImage 12.4.4 concrete usage scenarios will tell. In fact, even in the case of the Wizard Dialog, it was only later on that the designers recognized that wizards might wish to adapt the dialog size to cater to pages of varying sizes. TheyImage 3.2.5 captured this new service in the extension interface IWizardContainer2:

org.eclipse.jface.wizard.IWizardContainer2


public interface IWizardContainer2 extends IWizardContainer {
public void updateSize();
}



Image 11.1 Image We have already discussed (page 582) that the update ... methods in IWizard Container could be replaced by making the dialog a generic observer. We have dismissed this approach because it would complicate matters and there would always beImage 2.1.4 only one observer, contrary to the intentions of the Observer pattern.

The current question of reusability offers yet another perspective if we ask a new question: Which object should determine the callback interface? The Observer patternImage 2.1.2 insists that the subject, in this case the contained wizard, define the callbacks according to its possible state changes. However, we are building a generic WizardDialog that must be reusable in different scenarios (Fig. 12.25). Since that object is in the focus of theImage 11.5.6 design, the Dependency Inversion Principle suggests that the WizardDialog should also define the notification methods.



Image A further view of the IWizardContainer derives from the fact that the wizard uses it to access its context, in this case the WizardDialog in which it happens to beImage 12.3.2 Image 7.3.2 displayed. In the INTERCEPTOR pattern, the WizardDialog plays the role of a framework linking to some application-specific functionality by inversion of control. In such situations, callbacks usually receive a context object, which enables them to query the framework’s status and to influence the framework’s behavior.


12.4.3 Building a Reusable Parser

To appreciate the challenge of reuse fully, one cannot simply look at polishedImage 12.4.1 examples of reusable components. One has to struggle through the process of creating such components, even if it means grappling with the details of API design and performing many tedious refactorings.

Let us therefore go back once more to the example of the function plotter,Image 11.3 through which we explored the challenges of responsibility-driven design itself. There, we reused the Parser from the section on tree structures andImage 11.3.3.1 Image 2.3.3 the MiniXcel application. The process of creating this parser is instructive.Image 9.4 On the one hand, the example is small enough to get through by tactical considerations, taking one step after the other. On the other hand, it does include some of the real-world obstacles to reuse. We also use the example to formulate and explore a few fundamental guidelines for reusability.


Design the services of reusable components along established concepts.


So, let us picture Parser as the component R in Fig. 12.25. We start designing on the left-hand side of the figure. To arrive at a general service API for the parser, we ask: What does a parser really do for its clients? Well, it transforms a string conforming to some formal grammar into an abstractImage 2 syntax tree (AST). Since we are mainly aiming at arithmetic expressions, this would suggest roughly the following interface:


public class Parser {
ArithmeticExpression parse(String expr)
...
}


It is usually a good idea to start designing reusable components from generally known concepts, such as “parsing.” Other developers will easily understand the purpose and usage of the component. Also, well-known concepts are well known precisely because they tend to pop up in many different contexts. The potential for reuse is then particularly high.


Mold the concept into an API carefully.


The API presented earlier involves a rather subtle problem: We cannot simply fix the type of the syntax tree, because MiniXcel and the function plotter differ in the detail of cell references and variables. Some tools, such as ANTLR, circumvent such questions by using generic structures ofImage 206 nested linked lists. But this approach prevents the use of the COMPOSITEImage 2.3.1 pattern, which neatly embeds recursive operations into the recursive object structure.

The only solution to defining the AST properly is to let the client do it: Only the client knows the best structure for the syntax trees for the language it is parsing. We render this idea in the API by parameterizing the Parser by the type T of these trees. The parser will then juggle around AST nodes without inspecting them. If you are wondering how the parser will ever create objects of type T—we will come to that in a minute. For now,Image 1.8.6 recognize that the Parser has become an object encapsulating a (semi-) complex algorithm in a simple method parse():

parser.Parser


public class Parser<T> {
...
public T parse(String expr) throws ParseError {
...
}
...
}



Design collaborators as roles, starting from general concepts.


Let us now turn to the collaborators C and D in Fig. 12.25—that is, to those objects whose services the reusable component R itself requires from the context for proper operation. Since they vary between contexts, technicallyImage 3.2.1 R has to access them through interfaces. The interfaces capture the expected behavior without constraining the implementation.

It is important to realize that, again, these interfaces must derive fromImage 11.3.3.7 a conceptual understanding of roles: Concrete objects C and D will fulfill the requirements not by coincidence, but only if the roles’ responsibilities can be implemented in different but meaningful ways.

The reusable Parser must somehow construct the syntax tree. However, it cannot do so itself because the tree’s type T is left open. For this scheme to work, the parser needs a collaborator to undertake the construction—anImage 1.4.12 ABSTRACT FACTORY. The collaborator is passed to the parser’s constructor to create the object structure sketched in Fig. 12.25.

parser.Parser


private NodeFactory<T> factory;
public Parser(NodeFactory<T> factory) {
this.factory = factory;
}


What, then, is a suitable set of methods for the NodeFactory? Conceptually, the parser recognizes constructions in arithmetic expressions, each of which ends up as a node in the AST. It is therefore sensible to introduce one method for each type of syntax construct recognized. The parameters contain a description of the construct. The last method creating “references” involves a deliberate generalization, which is often necessary for successful reuse in different contexts. We discuss the details next.

parser.NodeFactory


public interface NodeFactory<T> {
T createNumber(double num) throws ParseError;
T createBinaryOperator(String op, T left, T right)
throws ParseError;
T createFunctionApplication(String fun, List<T> args)
throws ParseError;
T createReference(String token) throws ParseError;
}



Image One point worth discussing is the type of op in method createBinaryOperator(). Technically, a shift-reduce parser will recognize only a fixed set of operators, because it needs to assign a precedence to each of them. An enum type would therefore probablyImage 2 be more suitable. We have left our first version of the API here to raise precisely this discussion about a necessary decision, rather than presenting a polished version that glosses over it. Also, you may want to try your hand at a further generalization: Why not parameterize the parser from the online supplement by using an OperatorTable that maps operator symbols to precedences? The parser can then classify any consecutive sequence of punctuation characters as an operator.



The implementation of reusable components should reflect the used concepts.


The construction and handling of AST nodes in the Parser is a good example of a collaboration between a generic, reusable component R and its application-specific helpers C and D in Fig. 12.25.

In principle, a shift-reduce parser reads through the input string fromImage 2 left to right. It groups the characters into tokens such as numbers and operators. When it finds a leaf node, such as a variable or a number, it pushes the node on a stack, because it cannot know whether the node is the right operand of the previous operator or the left operand of the next. For instance, in a+b+c, b belongs to the left operator to yield the result (a+b)+c. In contrast, in a+b*c, b must be kept until c has been seen to yield a+(b*c). Keeping a node for later reference is called a shift step. Creating a new node from an operator and its operands is called a reduce step.

Our Parser keeps the currently recognized subexpressions on an operand stack and the currently open binary operators on an operator stack.

parser.Parser


private Stack<Op> operatorStack = new Stack<Op>();
private Stack<T> operandStack = new Stack<T>();


When it encounters a number, it pushes a node on the operand stack:

parser.Parser.endToken


if (NUM_PATTERN.matcher(tokStr).matches()) {
operandStack.push(factory.createNumber(Double.parseDouble(
tokStr)));
}


Reducing means finishing a subexpression by creating a new node for the syntax tree. The parser pops an operator and its two operands. It calls on the factory to create the node for the syntax tree. That node becomes a newly found operand to surrounding operators.

parser.Parser.reduceUpTo


T r = operandStack.pop();
T l = operandStack.pop();
String op = ((BinOp) operatorStack.pop()).op;
operandStack.push(factory.createBinaryOperator(op, l, r));


The interesting point about this implementation is that it works entirely at the level of shift-reduce parsing, without any thoughts about the applications of function plotting or spreadsheets. In our experience, one indication that you got your abstraction right is that your code reflects your understanding of the concepts you have chosen as the basis of your reusable component.


Image Image 11.2In fact, this focus on the abstract concepts is a prime example of the Single Responsibility Principle. Previously, we have applied the principle to split separate tasks into separate objects or classes. Now we see that it also serves to separate the abstract mechanisms from the concrete applications. While previously the split was vertical, puttingImage 12.2.2 one task beside the other, the new split is horizontal, in the sense of the Layers pattern:Image 70,71 It puts the high-level mechanisms above the low-level details and the common aspects above the variations between the applications.



Creating a reusable API requires experience and deliberate generalization.


Reusability to novices often seems to be the ultimate goal in software design: What can be more elegant than a general solution that you build once and never have to touch again? Besides, the personal prestige to be gained from finding such “ultimate” solutions is extremely attractive.

To counteract such misconceptions, we reveal that the solution presented here is the third attempt at building a shift-reduce parser. We have never said to ourselves, “Hey, parsing is cool and we have some experience, so let’s build a reusable parser so we never have to do it again.” In fact, our previous experience with parsing told us precisely that such a component could not be built easily. Here is the story of how we arrived at the Parser class through repeated refactoring and generalization.

The first parser version dates back to a lecture from 2006. It had a hard-codedImage 9.4 syntax tree for the MiniXcel application, which the students were to use in a project. In a later lecture in 2008, we asked the students to come up with the syntax tree for simple arithmetic expressions and introduced the type parameterization and the node factory. In essence, this carved out the reusable concept of shift-reducing parsing from the tangle of interacting objects within MiniXcel. Only after this step did the scenario resemble Fig. 12.25, with the Parser class in the position R.

Nevertheless, there remained one reference to the original MiniXcel context: For lack of time, the second version still accepted cell references like A3 or B$2. The NodeFactory had the two creation operations shown in the following code snippet, and we simply asked the students to ignore the one for CellRefs in the exercise. While this version was reusable enough for the purpose, we would certainly not commit it to paper.


T createVariable(String name) throws ParseError;
T createReference(CellRef r) throws ParseError;


The third version prepared for this book required a major refactoring to get rid of the dependency on CellRefs scattered through the implementation and to come up with the general creation for “references,” whether they are variables or cell references. The reusable parser’s collaboratorNode Factory, indicated as role C in Fig. 12.25, now creates the appropriate node, or throws a parse error if the format is unexpected. For convenience, here is the generalized method from the NodeFactory once more:

parser.NodeFactory


T createReference(String token) throws ParseError;


In many situations, you will find that generalizing the API of a reusable component also requires generalizing its internal mechanisms, because the existing ones take shortcuts only valid in the special case treated so far—after all, the guideline is to build the simplest thing that could possiblyImage 28 work.

For instance, the old parser distinguished variables and cell references first lexically, by whether they were uppercase or lowercase. In a second step, variables before an opening parenthesis were taken to be function names. Since this distinction between uppercase and lowercase characters is invalid for general “references,” the new parser stores any characters that are neither whitespace nor reserved characters in a string buffer token. When it hits an opening parenthesis afterward, it checks whether the token is a valid function name. For any other special character or whitespace, it delegates the recognition of the token to the NodeFactory’s create Reference() method.

parser.Parser.endToken


if (inFunPos) {
if ((m = FUN_PATTERN.matcher(tokStr)).matches()) {
operatorStack.push(new Fun(m.group(), operandStack.size()));
} else {
error(pos - token.length(), pos, "illegal function name");
}
} else {
operandStack.push(factory.createReference(tokStr));
}



Generalize such that the previous applications remain covered.


The purpose of the whole generalization effort is, of course, to use the same code for different applications. In this book, we would like to use the Parser for the trees in Chapter 2, for MiniXcel in Chapter 9, and for the example of the function plotter in Section 11.3. The fundamental principleImage 11.5.5 is always “don’t repeat yourself” (DRY): Once you start on copy-and-paste programming, you have to keep changes and bug fixes coordinated. The effort of generalizing at the first instance of reuse may be well spent, because you save on maintenance, and may even find further places in the system where the reused component can be deployed.

The challenge was, of course, to keep the original MiniXcel applicationImage 5.4.8 working while refactoring for the new uses. To avoid losing time in debugging the online supplement for Chapter 9, we started by extending the existing test suite for the parser before attempting any changes. Generalizing is a large effort itself. You should not increase it by breaking the product at the same time.

12.4.4 Strategies for Reuse

As the examples in the previous subsections have shown, reusability is a complex and challenging goal. At the same time, it often requires large investments in a general software infrastructure that complicates the code base. Worse still, the investments may never pay off because the “reusable” components turn out not to cover all expected applications. Because every component is essentially special, it is very hard to come up with general guidelines on how to proceed toward successful reuse. This section gives a few general hints.


Reusability is not an aim in itself.


Image 258The first pitfall to be avoided is apparently a human weakness: Developers love to come up with “elegant” solutions, in particular if they can define “elegance” according to their own personal taste. Reusability always has a strong attraction here: You can do a thing once and never have to bother with it again. However, reusability invariably takes development time away from the concrete application functionality, because it requiresImage 12.4.3 generalization and abstraction. It simply does not do to waste development time on “reusable” components if these are never reused. The danger ofImage 228 running into “you ain’t gonna need it” (YAGNI) is very high.


Make reusable design pay early.


The first guideline is therefore to evaluate the potential of a reusable component early on, before you start to design or to implement. Try to be completely fair: Count the possible scenarios where the component applies. Estimate the ease of use and the savings in development time in each scenario. Gauge the simplicity of reusing compared to hand-coding the functionality: If the configuration of the reusable component is very complex, this indicates that the variability between the uses exceeds their commonality andImage 70 the basis for reusability is rather weak. In many cases, you will find but a few application spots, so you must aim at making the generalization pay early on.

To avoid personal biases, you should also discuss the proposed component with your teammates. If they find applications or expect them to arise from their experience, the probability that your component will actually be useful increases dramatically.


Do the simplest thing that could possibly work.


One particular temptation in designing for reusability is to get carried away with abstraction. While you are implementing a reusable component, you will come across many hooks that might be useful in some future application. However, you should focus on the usage scenarios you have identified and leave other generalizations until later. A good constraining idea is to start testing your component immediately: If you have to write a test forImage 5.2 each adaptation point, you will think twice about whether you really need the generality.


Aim for cohesion and conceptual clarity.


The other point that helps to focus the design is cohesion. Do not try toImage 12.1.3 build the ultimate solution that anticipates every possible detail of every possible application scenario. Instead, define the purpose of your component clearly and then build only those features that are covered by the purpose. This guideline is useful in object-oriented design in general, but it is evenImage 11.2 more important when designing for reusability: Because you are dealing with more abstract mechanisms, it is harder to judge whether a particular piece of functionality is really required.


Keep reusable components decoupled from their context.


The initial intuition in Fig. 12.25 hints at the need to decouple a reusableImage 12.1 component from its context, because the component will have to work in many different scenarios. It is useful to keep this caveat in mind when creating CRC cards for the component: Think about the minimal assumptionsImage 12.1.2 that enable your component to work. Once you have written code, it becomes much harder to reduce these assumptions.


Design reusable components along established concepts.


A good guideline for finding parts of the implementation that make reuse worthwhile is to look at general concepts, both from the application domain and from computer science. A parser for arithmetic expressions might comeImage 12.4.3 in handy, as could a framework for rendering charts from raw data. Eclipse is all about editing code, so a custom-tailored, powerful SourceViewer willImage 214 be required. Proven concepts such as graphical editing might also warrant a general implementation.

Once you have found the central concept of the reusable component, you should also get a precise idea of where the reusable functionality ends: Which services does the component comprise? Which parts of the behavior will differ between the uses and should better be implemented elsewhere? Which aspects have a default implementation that can be overridden?


Look for available reusable components first.


Image 7.3Once you have formulated the concept you want to capture, you should sit back and browse for available libraries and frameworks that already cover that concept. If you do find a solution, you do not save just the implementation effort; you also save the effort that is required for first understanding the problem at the necessary level of abstraction. Do notImage 11.3.3.5 insist on making all the initial blunders yourself, and do not fall for the not-invented-here syndrome. Of course, the available components might not have exactly the API that you would have built, but that can often beImage 12.2.1.6 Image 2.4.1 solved by creating a few ADAPTERs.


Reusability is not a technical issue.


Many novices like reuse because it gives them license to try their hand at all kinds of nifty indirections and patterns that object-oriented programming has to offer. They often start by generalizing the implementation without having a clear understanding of the abstraction they are aiming at.

Professionals start from the conceptual questions, which give them a clear picture of the commonalities between the different uses of a component. Only then do they start to think about how the differences, or variabilities,Image 71,70 between these uses can be captured technically: Should they useImage 1.4.8 Image 1.8.5 Image 3.1.4 TEMPLATE METHOD, STRATEGY, OBSERVER, plain collaboration throughImage 1.1 interfaces, or abstract base classes? The trick is to start from the client’s perspective: Which API will be easiest and best for your teammates when they start using your component?


Build a toolbox that helps to build your applications.


One effective way of recognizing candidates for reusable components is to be perceptive in your daily work. If you find that you have implemented variations of some piece of functionality several times, chances are that you will encounter it some more later on. You can then start to think about the concepts that are common to all occurrences and to turn them into reusable components.

The general idea is that you are building a toolbox that helps you build your applications. Fill the toolbox while you go on with your daily work. This will also help you to focus on the essentials and to avoid over-designing. It will encourage you to forge only tools that you can use immediately.


Start from working code and concrete mechanisms.


Another approach is to let reusable functionality find you, rather than actively searching for it. While aiming for DRY, you can try to generalizeImage 11.5.5 existing objects so that they cover more and more situations. The ParserImage 12.4.3 example shows that this can be done gradually, incorporating one new application after the other. If you keep testing diligently, you will start saving implementation effort almost immediately, because you avoid the bugs introduced by copy-and-paste programming. And the more you master the Eclipse refactoring tools, the more progress you make. Throughout the process, be sure to identify concepts in your implementation; then you can refactor to exhibit those concepts more clearly.


Practice reuse in the small.


Software engineering, like any craft, is often a matter of practice and experience. Designing reusable components is particularly tricky, so you should give yourself the time to learn. Design individual objects, then compound objects, then small neighborhoods. Afterward, you can go on to components.