There is a pleasing self-similarity about the Natural History Museum building in London. The alternation of coloured brick, and the busy style of the exterior, run through the interior in even the smallest annexe or corridor. This Victorian love of gratuitous form and structure could hardly stand in greater contrast to the museum's contents — the works of nature — which mock humanity's efforts to produce complex, large scale, and reliable systems.
Evolution rarely does anything for free, preferring replication over invention, and thus its great successes manifest repeatedly in myriad species — the vertebral column being one such inspiration. Perhaps surprisingly, this form is mirrored in many software systems, and this article examines the deep similarities, showing that, whatever the medium, we are dealing with instances of the Decorator pattern.
This article appeared originally in the Enterprise Development Journal, which was published online and edited by Danny Bradbury, former editor of Application Development Advisor magazine, but which is now defunct. |
Those with backache will be all too aware of their vertebral columns. Cylindrical segments of bone, separated by disks of cartilage, are held together by ligaments, and possess protuberances or 'processes' to which muscles attach. But the first endo-skeletal creatures had a simpler chassis, and such a precursor can be found in the coelacanth. This fish is notable in that it was thought to have died out some 85 million years ago, until a living specimen was caught in 1938. However, it is also of interest in that it possesses not the complex spine of higher orders, but a notochord — a bony tube running down the dorsal side of the animal.
While many species have featured this design, it has its limitations. A notochord is relatively inflexible, which precludes creatures that can bend or twist to any degree. In addition, its dismal shock absorption properties make catastrophic damage to such a monolithic component much more likely.
One solution would be to divide the rod in two by placing a joint at the mid point, thus conferring flexibility and a degree of resilience. But this would introduce a single, central point of vulnerability. An articulating joint is a complex structure that can fuse through injury or disease, leaving the creature with the very rigidity that we sought to escape.
Instead, a far better alternative emerged 535 million years ago, and underpinned the 'Cambrian Explosion', when the number of species on Earth expanded massively. Natural selection chose a system composed of many little backbones, each doing much the same job, with flexibility arising from the accumulation of small movements between each. Given that each vertebra moves little in relation to its neighbours, the bone-to-bone interfaces can be relatively simple (just disks of cartilage), which improves their reliability. This design also exhibits good shock absorption. Its high granularity means that each interface needs to disperse only some of the kinetic energy before transmitting the remainder into the next segment of bone.
Then there is the potential for optimisation. Larger, thicker vertebrae offer good load-bearing properties, but this is at the expense of reduced flexibility. Conversely, smaller, thinner vertebrae are poor load-bearers, but they yield greater flexibility. However, by opting for a mix of these in a given spinal column, natural selection can give the animal both strength and flexibility in different places. For example, the cervical vertebrae in the human spine are significantly smaller than the lumbar. As a result your neck is more flexible than your lower back, which allows you to look round corners but also to carry heavy loads.
Scalability is the other major degree of freedom, and it manifests along two axes, namely physical size and number of segments. The former can be seen when comparing the tiny vertebrae of the shrew with the massive ones of the blue whale. In the latter, the axis starts with the frog (a meagre five vertebrae) and extends to the python, whose nodal count reaches into the hundreds.
The spinal column, then, is an evolutionary triumph. By repetition of a simple principle it can generate a wide range of statically tuneable, dynamically flexible systems that are robust and fault tolerant. So how does this relate to software development?
Imagine an application where data must be dispatched to clients across a network, and where the data must be processed differently, depending on the client in question. For example, clients can request data in its raw form, or encoded as XML. Some require data to be encrypted before dispatch, and for others it is necessary to record administrative information. All clients require arbitrary combinations of these.
If we assume that the technology for performing the various operations is already established, then we must decide how to administer the processing of the data at run time. One option is to create a class that takes the data and performs the various steps under the control of conditional statements. Another is to create a base type containing a virtual function, and then sub-class that with derived types that override the function and that cover every permutation on a per-class basis.
However, neither of these approaches appeals. The first entails a large, monolithic class, which concentrates disparate functionality in one place, and which is therefore hard to code, understand, verify, and modify. The second creates a profusion of monolithic classes, which also conflate the relevant abstractions and replicate near identical call-sequences across the code. One of the warning signs here is the ugly and obscure class-names that can arise.
The third option (in C++) is to use multiple inheritance, such that one creates a set of base classes, each of which performs one aspect of the processing. One can then derive from an arbitrary combination of these to form a single class that does the job. However, and to paraphrase Alexandrescu [Alexandrescu 6], one is simply using a language mechanism to superpose elements of the system, and no more. It provides no implicit orchestration of the base classes, which in turn mandates explicit implementation in the derived class(es). This too forces the conflation of a wide set of abstractions within a single class, which by implication breaks encapsulation. A warning sign here is that, diagrammatically, such class hierarchies can be just plain painful to look at.
However, a fourth option is to create a set of classes, each of which encapsulates just one aspect of the processing, and all of which support the same interface. One is responsible for formatting the data as XML, one is responsible for encryption, one handles the administrative information, and one performs the dispatch to the client. To send data in XML with encryption, one creates a chain of objects composed of an XML encoder, an encryptor, and a dispatcher. The data is passed to the first object which transforms it into XML, this then passes it to the encryptor which encrypts it, and so on.
Given that each object is of the same type, regardless of its actual class, users of this structure always see the same interface, irrespective of which object is the first in the chain. This principle extends through the structure such that each object operates in almost total ignorance of its neighbours. The approach aggregates functionality, but preserves transparency, and therefore we can be said to be 'decorating' the first object with the second (and the third, and so on), making this an instance of the Decorator pattern.
The power of this approach is that it simplifies the problem by dividing it equally, and then captures the complexity by repeating that simplification to an arbitrary degree. Clean abstractions are preserved, and no explicit mechanism is required to orchestrate each combination of decorating objects. Such systems are also flexible and infinitely extensible: for example, during development we can insert a 'trace' object into the chain, such that debugging information can be written out, allowing us to monitor the behaviour of the system. Alternatively, we can swap one object for another.
Superficially, the forces that impel developers towards the above technique, and the selection pressures that brought about vertebrates are very similar. Both evolution and programmers tend towards approaches that are widely applicable, scalable, extensible, and which can be optimised. The vertebral column is a system that yields those properties, as is the preferred approach to the data-dispatch problem.
However, the comparisons run deeper. In the vertebral column we see a segmented structure, the properties of which arise from the aggregation of its sub-components' properties. In the software example we saw functionality effected by the very same means. Applicability, scalability, extensibility, and optimisation potential all arise through the commonality of type that runs through a chain of objects. Given that the means of pattern implementation are irrelevant, the vertebral column is as much an instance of a Decorator as its software equivalents.
Given this, we can capture the vertebral column in UML. Moreover, a little thought shows that the cartilage disks are decorating objects in their own right. The only difference is that they handle the input of force differently to the vertebrae and do not 'implement' muscle attachment. The other issue is that it is a bi-directional decorator. Force can be input at either end of the system and is transmitted to the opposite end.
Note, however, that where software has nature beat is that dynamic Decorator-implementations allow the runtime modification of the structure. Conversely, the number of vertebrae that a given animal possesses is fixed at birth (aside from injury in life).
Investigations such as this raise interesting questions. For example, the master body-plan of head-thorax-abdomen appears in many creatures other than vertebrates, but creatures with greater degrees of differentiated segmentation seem rare. In other words, how many species are there with a head, thorax, abdomen, and 'something else'? Creatures with great degrees of segmentation do exist, but in these each 'compartment' is essentially identical, as in worms and millipedes. Why then does anatomy appear to jump from three segments to many, with no intermediate forms?
Well, in evolution, what survives is what works. Therefore the apparent absence of four or more differentiated body-segments in the world's fauna suggests that such designs are unfavourable. If we could understand why this is then we might be able to map back into software with a valuable insight into object structures and the useful limits on progressive differentiation in type.
Other issues are less inspiring, in that one might feel that evolution always arrives at the best solution; but this is not the case. In the main, our eyes are impressive optical systems, but the photosensitive cells in our retinas are actually on the wrong side (i.e. brainward). This causes light to travel through layers of blood vessels, and other structures, before it is detected, which degrades the image. As the biologist George Williams put it, our eyes are 'stupidly designed' [Zimmer 129].
This arrangement appears to have descended from the very earliest proto-vertebrates and, because natural selection does not embody refactoring, compensating features evolved. However, eyes have developed independently at a number of times during the evolution of life, and in better ways to boot — as Zimmer says, 'Thanks to 530 million years of evolutionary constraints, our children will never be able to see like a squid' [Zimmer 131]. Moreover, in mapping from biology back to software, we see a parallel in the 'Stove Pipe' anti-pattern, a familiar instance of which would be the implementation of early versions of Windows on top of DOS.
Finally, one useful concept that we can retrieve from biology is that every variation on a particular body plan is referred to as a 'homologue' of that generic design. For example, the spines of a rat, kestrel and salamander are all homologues of the vertebral column. By the same principle, every decorating structure in software that is created from the same set of classes could be said to be a homologue of one particular 'decoration space'. Perhaps we should adopt the term.
Modern C++ Design — Generic Programming and Design Patterns Applied
Andrei Alexandrescu
Addison Wesley
ISBN 0 201 70431 5
Evolution. The Triumph of an Idea
Carl Zimmer
William Heinemann, 2002
Anti Patterns. Refactoring Software, Architectures, and Projects in Crisis.
Brown, Malveau, McCormick III and Mowbray
John Wiley & Sons, Inc, 1998
ISBN 0 471 19713 0
Design Patterns. Elements of Reusable Object-Oriented Software
Gamma, Erich, Johnson and Helm
Addison-Wesley, 1995
ISBN 0 201 63361 2
The Self-Made Tapestry. Pattern Formation in Nature
Phillip Ball
Oxford University Press, 2001
In a Cambrian explosion of our own, the mid-1990s saw the emergence of Design Patterns onto the software-development landscape, with the publication of Gamma et al's seminal book. In essence, the concept is that disparate software systems embody common archetypal forms, and that, at the least; these can be identified, named, and communicated between developers.
Patterns have stirred great interest, and there is disagreement as to their precise nature. A common definition is that they are a proven solution to a common problem in a given context. Yet this also applies to algorithms, data structures, and the full stop at the end of this sentence. Others define them as 'micro architectures', yet they manifest on large and small scales equally, aside from the questionable use of the word 'architecture'. Ultimately, these characterisations leave us without a qualitative distinction.
What can be shown is that they are implementation-independent, and that they often appear in combination with others. They are also easily confused: consider the Observer pattern, where actions performed on a given (logical) object are 'witnessed' by others. An example would be graphical charts in a spreadsheet that reflect the values in a range of data. If one or more values change then the charts must be updated to reflect this, and the charts can therefore be said to be 'observing' the data.
Now consider the Chain of Responsibility. Here an object executes only those actions for which it is equipped, delegating down (or yielding up) the remainder to entities that are better able to handle the task. This reflects the chain of command in, say, a military organisation, and could therefore be called the 'buck stops somewhere' pattern. An excellent example is the hierarchy of catch clauses in systems that implement exception handling.
Superficially, these two patterns seem to be identical to the Decorator. In the Observer a number of objects are treated equally with regard to a given process, which mirrors the commonality of interface in the Decorator. In the Chain of Responsibility, we are effectively speaking of a layered filter, which reflects the chained nature of the Decorator. However, this does not imply a case of the Emperor's New Clothes: the Observer is concerned with the maintenance of a dependency relationship, while in Chain of Responsibility the issue is one of scope.
Given that patterns are implementation independent, Decorators can manifest in software in many ways and, contrary to many texts, they do not have to involve polymorphism (which, arguably, is a 'pattern' in itself).
Two explicitly coded ways represent a choice between a dynamically or statically-composed structure. In the dynamic case, each decorating object has a pointer/reference to the next object in the chain. In a static implementation, each class declaration for a decorating object is a template (assuming a language such as C++). This has another decorating object as a member, and the type of this is supplied as a parameter at the point of instantiation.
In the dynamic case, deriving from a common base and then overriding its interface in the sub-classes is the principle way of differentiating what each object does. With the template approach, the member type can be anything as long as it supports the same interface as the others.
One issue of note is the boundary condition that arises when an object can occupy any position in the chain, including the endmost. The last object in the structure cannot make a call to a non-existent 'next one along', and this contingency must be accounted for.
One solution, in a dynamic implementation, is to put logic into each class to test for this at run time. However, this is messy and inefficient, and it will not work in the static case.
The better approach in both is to derive/create (respectively) a class that overrides/supports the interface but which does absolutely nothing - a Null Decorator. This is appended to the chain, acting as a 'cap' on the structure, and will be elided by the compiler in the static scenario. Note that this is an instance of the Null Object pattern, and given that this is one pattern manifesting in conjunction with another, this is an example of a pattern composite.
Naturally, there is a trade-off between the dynamic and static approaches. The dynamic route enables the insertion and removal of objects at run time, allowing the properties of the overall structure to be adjusted on the fly - logically, a form of dynamic sub-classing.
In the case of static implementations (logically, an alternative to language-supported sub-classing), there is the loss of runtime flexibility at the gain of massive compile-time guarantees. This permits the structure and its operation to be optimised massively.
Note, finally, that a criticism of the Decorator is that implementations are often composed of many, very similar, little classes. However, considering the alternatives, this borders on the churlish. There is a minimum level of complexity required of a model for it to defeat the complexity of the problem at hand. Obviously, some approaches are cheaper than others, but you still cannot have something for nothing. The model must embody the complexity somewhere, and it is far better to do so through a consistent and cohesive approach.
// /////////////////////////////////////////////////////// // // Simplified example of a dynamically-constructed // decorator in C++. Namespace details etc. ommitted // class Data { ... }; class Client { public: void OnDataReceived (Data &DataPacket) { ... } ... }; // -------------------------------------------- class Decorator { private: Decorator *NextDecorator; public: Decorator () : NextDecorator (0) { } Decorator *GetNextDecorator () { return NextDecorator; } void AddDecorator (Decorator * const NewDecorator) { NextDecorator = NewDecorator; } Decorator *RemoveNextDecorator () { NextDecorator = NextDecorator->GetNextDecorator (); } virtual void Dispatch (Data &DataPacket, Client &Client) = 0; }; // -------------------------------------------- class XMLFormattingDecorator : public Decorator { public: virtual void Dispatch (Data &DataPacket, Client &Client) { // Convert Data to XML GetNextDecorator ()->Dispatch (DataPacket, Client); } }; // -------------------------------------------- class EncryptingDecorator : public Decorator { public: virtual void Dispatch (Data &DataPacket, Client &Client) { // Encrypt Data GetNextDecorator ()->Dispatch (DataPacket, Client); } }; // -------------------------------------------- class DataDispatcher : public Decorator { public: virtual void Dispatch (Data &DataPacket, Client &Client) { Client.OnDataReceived (DataPacket); } }; // // Barebones example of creating, using and destroying // a decorator using the above classes. // void DispatchData (Data &DataPacket, Client &Client) { XMLFormattingDecorator *Formatter = new XMLFormattingDecorator; EncryptingDecorator *Encryptor = new EncryptingDecorator; DataDispatcher *Dispatcher = new DataDispatcher; Formatter->AddDecorator (Encryptor); Encryptor->AddDecorator (Dispatcher); Formatter->Dispatch (DataPacket, Client); delete Encryptor->GetNextDecorator (); delete Formatter->GetNextDecorator (); delete Formatter; } // -------------------------------------------- int main (int argc, char* argv[]) { Data DataPacket; Client Client; DispatchData (DataPacket, Client); return 0; } // /////////////////////////////////////////////////////// // // Simplified example of a statically-constructed // decorator in C++. Namespace details etc. ommitted // class Data { ... }; class Client { public: void OnDataReceived (Data &DataPacket) { ... } // ... }; // -------------------------------------------- template <typename NextDecoratorType> class XMLFormattingDecorator { private: NextDecoratorType NextDecorator; public: void Dispatch (Data &DataPacket, Client &Client) { // Convert Data to XML NextDecorator.Dispatch (DataPacket, Client); } }; // -------------------------------------------- template <typename NextDecoratorType> class EncryptingDecorator { private: NextDecoratorType NextDecorator; public: void Dispatch (Data &DataPacket, Client &Client) { // Encrypt Data NextDecorator.Dispatch (DataPacket, Client); } }; // -------------------------------------------- class DataDispatcher { public: void Dispatch (Data &DataPacket, Client &Client) { Client.OnDataReceived (DataPacket); } }; // // Barebones example of creating, using and destroying // a decorator using the above classes. // void DispatchData (Data &DataPacket, Client &Client) { XMLFormattingDecorator <EncryptingDecorator <DataDispatcher> > DecoratedDispatcher; DecoratedDispatcher.Dispatch (DataPacket, Client); } // -------------------------------------------- int main (int argc, char* argv[]) { Data DataPacket; Client Client; DispatchData (DataPacket, Client); return 0; }