Pattern-Oriented Software Architecture
By Douglas Schmidt, Michael Stal
Hans Rohnert, Frank Buschmann
John Wiley & Sons, New York
666 pp./$54.99 ISBN 0-471-60695-2
IT'S CERTAINLY TRUE that building distributed systems is a difficult business. This is due to the inherent and accidental complexity of both the design and implementation of networked software solutions. Programmers not only have to deal with different network protocols and the possibility of hardware failure, but must also manage concurrency, interoperability, high data throughput, configurability, portability in a heterogeneous environment, and efficient event handling. That's why the latest volume of Pattern-Oriented Software Architecture: Patterns For Concurrent and Network Objects is so important for anyone working on distributed systems.
It's the first in-depth treatment of how to deal with these issues in a systematic way. Those familiar with the Gang of Four's classic Design Patterns1 book will find the meaning of the term "pattern" to be more elastic in this book. Design Patterns restricted itself to micro-architectural patterns that showed how classes and objects could be used to resolve recurring problems across multiple domains of software architecture. The 17 patterns presented in Pattern-Oriented Software Architecture range from small, programming language-specific idioms to large-scale solutions for complex engineering problems that lead to the definition of pattern-based programming components. All of the patterns are centered around using object-oriented techniques to build distributed systems.
The patterns and idioms discussed in Pattern-Oriented Software Architecture form a how-to guide for creating server processes in general, and are ubiquitous across middleware implementations. However, these patterns don't actually represent application-level or architectural solutions for end-user applications. For example, there's plenty of material in this book that suggests how to build the internals of a CORBA ORB, but nothing that deals with how to scale a CORBA-based application. In general, these patterns are important for any server development, not just for frameworks. Most of these patterns fall cleanly into three areas: configuration and management of services, event handling, and concurrency.
To better illustrate the kinds of patterns found in this book and the types of problems they solve, let's take a close look at a specific example from the event-handling category. The Reactor pattern describes how to structure event-driven applications to demultiplex requests from multiple clients and dispatch them to services. The reactor is responsible for waiting synchronously for indication events on one or more sources from clients, and delegating response handling to appropriate service handlers. The service handlers registered with the reactor handle the actual request processing. In other words, service handlers react to client-initiated events. The Reactor pattern relies on the principle of "inversion of control" and pushes application or service-handler development to the edges of the design; the reactor itself is responsible for handling indication events.
A servlet engine is a familiar application of the principle of inversion of control (also known as the "Hollywood Principle": Don't call us, we'll call you!). Developing a servlet is easy, because the low-level details of detecting and demultiplexing an HTTP request are handled by an app server. Servlets are invoked reactively, and servlet developers only have to worry about application-level request handling logic. In the language of the Reactor pattern, the servlet is a "service handler."
The Reactor pattern has several key benefits. First, it completely decouples application-specific code from event detection and demultiplexing. This means application-independent mechanisms can be separated into reusable components that detect events and call into hook methods for registered event handlers. The service or event-handler element of the Reactor pattern isolates only the application code. This leads to an important consequence: clean, object-oriented, modular, and configurable component design. The reactor also serializes the invocation of event handlers, which leads to simplified, course-grained concurrency control.
A more naïve approach to handling the same problem would be to intermingle the event demultiplexing and application logic into a set of monolithic application classes. This results in excessively complicated and error- prone code. Where application developers should concentrate on writing business logic appropriate to their main business problem domain, they are forced to write low-level code to manage system events. Worse still, they are forced to do this repeatedly in every new server program. Clearly, this is undesirable.
Like all patterns, the Reactor pattern has some liabilities. Experience with application frameworks shows that inversion of control in a framework leads to increased complications in debugging and testing. The Reactor pattern also has a more subtle liability: the canonical Reactor pattern is non-preemptive. This means that concurrency strategies, though they may be manageable, are often necessary, complicating design.
The reactor component can be difficult to implement in Java because Java doesn't offer a synchronous demultiplexing call like the select() function in Unix. For Java application developers interested in using the Reactor pattern to handle events generated by system-level constructs (like sockets), it is best to look closely at existing, open-source Java implementations of the Reactor pattern.
The Reactor pattern is fundamental in middleware development. It is used in Web servers, CORBA ORBs, application servers, and just about any other event-driven, distributed system. The Reactor pattern is also at the core of the Apache/XML Cocoon project for serving XML-based content over the Web.
A Pattern Language
The Reactor pattern is not used in isolation. It does not, for example, address how connection management should be handled or how concurrent request processing should be managed. One of the strengths of using the Reactor pattern is a clean separation between event demultiplexing, connection management, and event handling.
These issues can be handled in-dependently in ways appropriate to the problem domain. They are also addressed by other design patterns. Connection management, for example, is resolved with the Acceptor-Connector pattern described in this book; concurrent event processing can be addressed with either the Active Object or Half-Sync/Half-Async pattern. By weaving patterns together, a robust description of how to cleanly manage communication end-point associations, event handlers, and request dispatching emerges.
In combination, the patterns form a rich pattern language that can describe the detailed, internal workings of server processes concisely and meaningfully; they allow developers and architects to talk in high-level terms about potential designs and their consequences. Moreover, they become part of a broader pattern language that extends beyond networked systems. A typical implementation of the Reactor pattern, for example, will involve familiar design patterns like Strategy, Template Method, and Singleton. Though the authors describe each pattern in a self-contained way, the central theme of this book is how patterns are used together to solve problems.
Adaptive Communication Environment (ACE)
There is real power in building a pattern language to describe complex software in short order; there is even more power in the capacity to build complex software itself with pattern-based components. One very nice feature of Pattern-Oriented Software Architecture is the inclusion of complete and thorough examples from the ACE toolkit.
ACE is an open-source, layered set of software components that reifies many of the distributed communication patterns presented in this book. It's been widely applied in application and middleware development within the telephony and aeronautics industries. The ACE examples given in Pattern-Oriented Software Architecture put the discussion into concrete terms.
More importantly, ACE provides a set of robust and reusable middleware implementations, including a high-performance POA-based CORBA ORB and a Web server. This real-life software—an essential companion to the book—shows how these patterns are applied in building many different types of servers. What's most striking are the similarities, not the differences, in the implementations of what are often thought of as completely unrelated categories of distributed systems infrastructure.
Note: Unfortunately for Java developers, ACE is really a C++ toolkit. A Java port of ACE was developed, but it does not appear to have been maintained and has not been proven in real-life software products like the C++ version. Java developers should also be aware that many of the examples and discussions in this book are heavily biased toward C++.
What's Missing in the JDK?
What the ACE toolkit provides at the highest levels of abstraction is exactly what's missing from the JDK. Java provides a set of object-oriented wrapper facades for system-level constructs. A key example used in distributed systems development with Java is the Socket abstraction in the java.net package (an example of the Wrapper Façade pattern). Programming with the JDK essentially means working with system-level primitives and, unfortunately, continuous re-invention of distributed systems components that implement the fundamental patterns used to build distributed server software.
Using Pattern-Oriented Software Architecture as a guide, re-invention doesn't have to mean rediscovery. With an ACE-like set of classes in the JDK, re-invention itself wouldn't be necessary, eliminating much of the repetition and cost that too often leads to inflexible software or even project failure in developing Java servers. Developers building middleware should work with high-level components based on fundamental patterns to build flexible, configurable server software correctly and quickly.
Because Pattern-Oriented Software Architecture helps you do just that, the book is an essential companion for anyone building OO distributed systems and infrastructure in Java.
- Gamma, E. et al., Design Patterns: Elements of Reusable Object-Oriented Software, Addison–Wesley, 1995.