6.1 Design patterns for decoupling

To get the true benefits of polymorphism - or 'pluggability' - in a program, it is important to declare variables and parameters not with explicit classes, but via an interface (abstract class in C++, interface in Java, deferred class in Eiffel). Looking at the metaphor of a café used in Figure 39, our initial model has Food used by both Kitchen and Cash Register. But these clients need different behaviour from Food, so we separate their requirements into different interfaces: Edible for the kitchen's requirements of food, and Saleable for the till's. By doing this, we have made the design more flexible - and the business too: because now we can consider edible things that are not saleable (ingredients such as flour), and saleable things that are not edible - we could start selling newspapers in our café. Our original Food class happens to implement both interfaces. Some good programmers insist that we should always use interfaces to declare variables and parameters. Simple multiple inheritance of rôle-types can be considered a pattern for composing collaborations. (In untyped languages such as Smalltalk, the difference appears only in our design model, and does not appear in the program.)

Using interfaces is the basic pattern for reducing dependencies between classes. A class represents an implementation; an interface represents the specification of what a particular client requires. So declaring an interface pares the client's dependency on others down to the minimum: anything will do that meets the specification represented by the interface.

 

Figure 39 Interface decoupling.

When we add a new class to a program, we want to alter code at as few places as possible. Therefore, one should minimize the number of points where classes are explicitly mentioned. As we have discussed, some of these points are in variable and parameter declarations: use interface names there instead. The other variation points are where new instances are created. An explicit choice of class has to be made somewhere (using new Classname, or in some languages, by cloning an existing object).

Factory patterns are used to reduce this kind of dependency. We concentrate all creations into a factory. The factory's responsibility is to know what classes there are, and which one should be chosen in a particular case. The factory might be a method, or an object (or possibly one rôle of an object that has associated responsibilities). As a simple example, in a graphical editor, the user might create new shapes by typing 'c' for a new circle, 'r' for a new rectangle and so on. Somewhere we must map from keystrokes to classes, perhaps using a switch statement or in a table. We do that in the shape factory, which will typically have a method called something like make ShapeFor(char keystroke). Then, if we change the design to permit a new kind of shape, we would add the new class as a subclass of Shapes and alter the factory. More typically, there will be a menu that initializes from a table of icons and names of shape types.

Normally one would have a separate factory for each variable feature of the design: one factory for creating shapes and a separate one for creating pointing-device handlers.

A separate factory class can have subclasses. Different subclasses can provide different responses as to which classes should be created. For example, suppose we permit the user of a drawing editor to change mode between creating plain shapes and creating decorated ones. We add new classes like FancyTriangles to accompany the plain ones but, instead of providing new keystrokes for creating them explicitly, we provide a mode switch - whose effect is to alter this pointer in the Editor:

ShapeFactory shapeFactory;

Normally, this points to an instance of  PlainShapeFactory, which implements ShapeFactory. When given the keystroke 'c' or the appropriate file segment, this factory creates a normal circle. But we can reassign the pointer to an instance of FancyShapeFactory, which, given the same input, creates a FancyCircle. Gamma et al. (1995) call classes like ShapeFactory abstract factories. It declares all the factory messages like makeShapeFor(keystroke), but its subclasses create different versions.

Programmers new to object-oriented design can get over-enthusiastic about inheritance. A naïve analysis of a hotel system might conclude that there are several kinds of hotel, which allocate rooms to guests in different ways; and a naïve designer might therefore create a corresponding set of subclasses of Hotel in the program code, overriding the room-allocation method in the different subclasses:

class Hotel {...

public void checkInGuest(...)

...

abstract protected Room allocateRoom (...);

...}

class LeastUsedRoomAllocatingHotel extends Hotel

{ protected Room allocateRoom (...)

{ // allocate least recently used room

...}

}

class EvenlySpacedRoomAllocatingHotel extends Hotel

{ protected Room allocateRoom (...)

{ // allocate room furthest from other occupied

This is not a very satisfactory tactic: it cannot be repeated for other variations in requirements, for example if there are several ways of paying the staff. Overriding methods is one of the shortest blind alleys in the history of object-oriented programming. In practice, it is useful only within the context of a few particular patterns (usually those concerned with providing default behaviour). Instead, the trick is to move each separate behavioural variation into a separate object. We define a new interface for room allocation, for staff payment, and so on; and then define various concrete implementations for them. For example, we might write:

class Hotel {

Allocator allocator; ...

public void checkInGuest (...)

{... allocator.doAllocation(...);..}

...}

interface Allocator{

Room doAllocation (...); // returns a free room

...}

class LeastUsedAllocator implements Allocator

{ Room doAllocation (...) {...code ...}}

class EvenSpaceAllocator implements Allocator

{ Room doAllocation (...) {...code ...}}

This pattern of moving behaviour into another object is called delegation. It has some variants, described differently depending on your purpose. One benefit of delegation is that it's possible to change the room allocator (for example) at run time, by 'plugging in' a new Allocator implementation to the allocator variable. Where the objective is to do this frequently, the pattern is called State.

Another application of delegation is called policy: this separates business-dependent routines from the core code; so that it is easy to change them. Room allocation is an example. Another style of policy checks, after each operation on an object, that certain business-defined constraints are matched, raising an exception and cancelling the operation if not. For example, the manager of a hotel in a very repressed region of the world might wish to ensure that young people of opposite genders are never assigned rooms next to each other; the rule would need to be checked whenever any room-assigning operation is done.

Interfaces decouple a class from explicit knowledge of how other objects are implemented; but in general there is still some knowledge of what the other object does. For example, the RoomAllocator interface includes allocateRoom(guest) - it is clear what the Hotel expects from any RoomAllocator implementation. But sometimes it is appropriate to take decoupling a stage further, so that the sender of a message does not even know what the message will do. For example, the hotel object could send a message to interested parties whenever a room is assigned or becomes free. We could invent various classes to do something with that information: a counter that tells us the current occupancy of a room, a reservations system, an object that directs the cleaning staff, and so on.

These messages are called events. An event conveys information; unlike the normal idea of an operation, the sender has no particular expectations about what it will do; that is up to the receiver. The sender of an event is designed to be able to send it to other parties that register their interest; but it does not have to know anything about their design. Events are a very common example of decoupling.

To be able to send events, an object has to provide an operation whereby a client can register its interest; and it has to be able to keep a list of interested parties. Whenever the relevant event occurs, it should send a standard notification message to each party on the list.

An extension of the event pattern is observer. Here the sender and listener are called Subject and Observer, and an event is sent whenever a change occurs in the sender's state. By this means, the observers are kept up to date with changes in the subject. New observers may be added easily as subtypes as shown in Figure 40.

 

Figure 40 Observer.

A very common application of observer is in user interfaces: the display on the screen is kept up to date with changes in the underlying business objects. Two great benefits of this usage are:

  1. the user interface can easily be changed without affecting the business logic;
  2. several views of a business object can be in existence at a time - perhaps different classes of view.

For example, a machine design can be displayed both as an engineering drawing and as a bill of materials; any changes made via one view are immediately reflected in the other. Users of word processors and operating systems are also familiar with changes made in one place - perhaps to a file name - appearing in another. It is only very old technology in which a view needs to be prompted manually to reflect changes made elsewhere. Another common use of observer is in blackboard systems.

The observer pattern has its origin in the Model-View-Controller (MVC) pattern (or 'paradigm' as it is often mistakenly called), first seen in the work of Trygve Reenskaug on Smalltalk in the 1970s, and now visible in the Java AWT and Swing libraries. The MVC metaphor also influenced several object-oriented and object-based visual programming languages such as Delphi and Visual Basic.

An MVC model is an object in the business part of the program logic: not to be confused with our other use of the term 'modelling'. A view is an observer whose job it is to display the current state of the model on the screen, or whatever output device is in use: keeping up to date with any changes that occur. In other words, it translates from the internal representation of the model to the human-readable representation on the screen. Controller objects do the opposite: they take human actions, keystrokes and mouse movements, and translate them into operations that the model object can understand. Note, in Figure 41, that Observer sets up two-way visibility between model and controller as well as model and view. Controllers are not visible to views, although part of a view may sometimes be used as a controller: cells in a spreadsheet are an example.

 

Figure 41 Model-View-Controller instantiates Observer.

Views are often nested, since they represent complex model objects, which form whole-part hierarchies with others. Each class of controller is usually used with a particular class of views, since the interpretation of the user's gestures and typing is usually dependent on what view the mouse is pointing at.

In MVC, View and Controller are each specializations of the adapter pattern (not to be confused with the ADAPTOR pattern language). An adapter is an object that connects two classes that were designed in ignorance of each other, translating events issued by one into operations on the other. The View translates from the property change events of the Model object to the graphical operations of the windowing system concerned. A Controller translates from the input events of the user's gestures and typing to the operations of the Model.

An adapter knows about both of the objects it is translating between; the benefit that it confers is that neither of them needs to know about the other, as we can see from the package dependency diagram in Figure 42.

Any collection of functions can be thought of as an adapter in a general sense; but the term is usually used in the context of event; that is, where the sender of a message does not know what is going to be done with it.

 

Figure 42 Adapters decouple the objects they connect.

Adapters appear in a variety of other contexts beside user interfaces, and also on a grander scale: they can connect components running in separate execution spaces. Adapters are useful as 'glue' wherever two or more pre-existing or independently designed pieces of software are to be made to work together.

Decoupling with ports and connectors

Adapters generally translate between two specific types - for example, from the keystrokes and mouse clicks of the GUI to the business operations of a business object. This means, of course, that one has to design a new adapter whenever one wants to connect members of a different pair of classes. Frequently, that is appropriate: different business objects need different user interfaces. But it is an attractive option to be able to reconfigure a collection of components without designing a new set of adapters every time.

To illustrate an example of a reconfigurable system, Figure 43 shows the plugs that connect a simple electronic system together, using the real-time UML instance/port or capsule notation. Ports are linked by connectors. A connector is not necessarily implemented as a chunk of software in its own right: it is often just a protocol agreed between port designers. We try to minimize the number of connector types in a kit, so as to maximize the chances of any pair of ports being connected. In our example, two types of connector are visible, which we can call event connectors and property connectors - which transmit, respectively, plain events and observed attributes. You can think of the components as physical machines or the software that represents them a piacere. The button pressed interface always exports the same voltage (or signal) when it is pressed. The start and stop interfaces interpret this signal differently according to the motor's state machine. The point about this component kit is that careful design of the interface protocols and plug-points allows it to be used for a completely (well, not quite completely!) different purpose as shown in Figure 44. The members of this kit of parts can be rewired to make many different end products, rather like a construction toy. The secret of this reconfigurability is that each component incorporates its own adapter, which translates to a common 'language' understood by many of the other components. Such built-in adapters are called ports; they are represented by the small boxes in the figure.

 

Figure 43 Component ports and event and property connectors.

Figure 44 Creating different products from the same components.

The connector is such a useful abstraction that it deserves a notation in its own right: we have used the UML-RT notation here (with some artistic licence to highlight our two types of connector). The objects in this notation are stereotyped «capsule». The small black squares in this example are output ports and the white ones input ports. A 'p' indicates a continuous 'property' port: these transmit values as often as necessary to keep the receiving end up to date with the sender - in other words, an observer. Unmarked ports transmit discrete events. Property connectors are shown by bolder lines than event connectors.

Once we have understood what the ports represent, we can get on and design useful products from kits of components, without bothering about all the details of registering interest, notification messages, and so on.

Ports can contain quite complex protocols. Each component must be well-specified enough to be usable without having to look inside it. So let's consider one way a port might work, as shown in Figure 45. An output port provides messages that allow another object to register interest in the event it carries; when the relevant event occurs (for example, when the user touches a button component) the port sends a standard 'notify' message to all registered parties. An input port implements the listener interface for these notify messages; when it receives one, it sends a suitable message into the body of its component. For example, the 'start' port of the Motor component, when it receives a standard notify( ) message, will pass start ( ) to the principal object representing the motor.

 

Figure 45 Connectors abstract protocols.

Property output ports in this scheme transmit events whenever the named attribute in the sender object changes - in other words, they implement the observer pattern. Property input ports update the named attribute in the receiver object. Thus the meter's value keeps up to date with the motor's speed, while they are connected.

A component kit is a collection of components with a coherent interconnexion architecture, built to work together; it is characterized by the definitions of its connectors - the protocols to which the ports conform. A component kit architecture defines how plugs and sockets work and what kinds there are. Of course, these definitions have to be standardized before the components themselves are written. Common object types (int, Aeroplane, etc.) should be understood by all kit members. The kit architecture is the base upon which component libraries are built. Applications may then be assembled from the components in the library - but without the kit architecture, the whole edifice of CBD collapses. There are usually many architectures that will work for any scheme of connectors: the Java Beans specification provides a slightly different (and better performing) set of protocols to the ones we have described here.

Designing the architecture involves design decisions. A port should be realized as an object in its own right. The port connectors abstract away from the details of the port's protocol, but this must be specified eventually. For example, the sequence diagram in Figure 45 shows just one way in which the button-motor interface can be implemented. The property coupling port could be similarly implemented, but with regular update of new values.

Although we have illustrated the principle of the connector with small components, the same idea applies to large ones, in which the connector protocols are more complex and carry complex transactions. The current concern with Enterprise Application Integration can be seen as the effort to replace a multiplicity of point-to-point protocols with a smaller number of uniform connectors.

When specifying ports, or capsules, specify the types of parameters passed, the interaction protocol and the language in which the protocol is expressed. The interaction protocol could be any of:

  • Ada rendezvous;
  • complex transaction;
  • asynchronous call;
  • continuous dataflow;
  • broadcast;
  • FTP;
  • buffered message;
  • function call;
  • call handover;
  • HTTP, and so on.
The interaction language could be:

  • ASCII, etc.;
  • plain procedure call;
  • CORBA message or event;
  • RS232;
  • DLL/COM call;
  • TCP/IP;
  • HTML;
  • UNIX pipe;
  • Java RMI;
  • XML;
  • Java serialized;
  • zipped.