By Steven Black
Read this article in Serbo-Croatian.Introduction
Some design patterns, like most of those described by Gamma and Helms Design Patterns, describe the implementation of micro-architectures. These micro-architectural design patterns are useful, for the most part, to describe small-scale object interactions.
Other design patterns are useful for abstracting large systems of objects. These are architectural design patterns. An architectural pattern is any pattern concerned with the construction context of a whole system, rather than just some part of a system.
The distinction between micro-architectures and system architectures depends on your point of view and the scale of your system. If your system has 5 objects then the micro-architecture-style design patterns are architectural patterns because they consider the structure, relative communication, and design philosophy for the system. But more commonly, architectural design patterns are used to describe the structure of bigger systems where the number of objects is measured in hundreds or thousands.
One common architectural design pattern is called Layers. Here it is:
Layers is an architectural design pattern that structures applications so they can be decomposed into groups of subtasks such that each group of subtasks is at a particular level of abstraction.
The traditional 3-tier client server model, which separates application functionality into three distinct abstractions, is an example of layered design. Much has been written about the 3-tier client-server model and I wont discuss it further, other to say that this is the result of layered design thinking.
Figure 1: A simplified view of the 3-tier client-server architecture.
In a more general sense, the OSI 7-layer networking model and the Internet Protocol Stack, both illustrated in Figure 2, are networking protocols that illustrate the use of layering in network architecture.
Figure 2: The OSI 7-layer model, largely supplanted by the more recent and popular Internet Protocol Stack.
Here is a brief table describing the layers of the OSI 7-layer model.
Provides services to user. Examples include telnet, TCP, HTTP.
Structures information and attaches semantics.
Provides dialog control and synchronization facilities.
Responsible for segmenting long messages into packets. Recovers lost packets with acknowledgments and retransmissions. Flow control. Congestion control. Breaks messages into packets and guarantees delivery.
Responsible for routing packets from source to destination host. Selects a route from sender to receiver.
Responsible for moving packet from one node (host or packet switch) to next node. Error detection and correction. Medium access.
The physical layer, the lowest layer in the OSI stack, is responsible for moving information between two systems connected by a single physical link. The physical layer provides the abstraction of bit transport, independent of the link technology. Specifies voltage levels, bit spacings.
The OSI 7-layer model is a cool example because it neatly shows the general types of services required for computers to talk to each other. The Internet protocol stack is a refinement of the OSI 7-layer model, minus the Presentation and Session layers, whose services are either not needed or abstracted into the neighboring layers.
Note that each of the layers in the OSI stack dont necessarily function on distinct hardware or memory space. For example, its common to find the Data Link and Physical layers tightly coupled and interleaved (for performance reasons) within the same Ethernet network interface card.
A large system requires decomposition. One way to decompose a system is to segment it into collaborating objects. In large systems a first-cut rough model might produce hundreds or thousands of potential objects. Additional refactoring typically leads to object groupings that provide related types of services. When these groups are properly segmented, and their interfaces consolidated, the result is a layered architecture.
Segmentation of high-level from low-level issues. Complex problems can be broken into smaller more manageable pieces.
Since the specification of a layer says nothing about its implementation, the implementation details of a layer are hidden (abstracted) from other layers.
Many upper layers can share the services of a lower layer. Thus layering allows us to reuse functionality.
Development by teams is aided because of the logical segmentation.
Easier exchange of parts at a later date.
The trouble with layers of computer software is that sooner or later you loose touch with reality. Layers are abstraction boundaries, and the more they encapsulate their works the more one is unaware of the applications inner works.
Layering is a form of information hiding. A layering violation occurs in situations where a layer uses knowledge of the implementation details of another layer in its own operations. At the limit this leads to changes to one layer resulting in changes to every other layer, which is an expensive and error prone proposition.
Layering can lead to poor performance. To avoid this penalty, in situations where an upper layer can optimize its actions by knowing what a lower layer is doing, we can reveal information that would normally be hidden behind a layer boundary.
The layers must be engineered at the outset, before the system is built.
The following is a partial list of forces that bring about layered architectures. Note that some of these forces are present to varying degrees in all software systems.
Late source code changes should not ripple through the system
Interfaces should be stable.
Parts should be exchangeable.
Possibility of building other systems at a later date with the same low-level issues as the system currently being designed.
Similar responsibilities should be grouped to help understandability and maintainability.
The system will be built by a team of programmers, and work has to be subdivided along clear boundaries.
A Layered model does not imply that each layer should be in a separate address space. Efficient implementations demand that layer-crossings be fast and cheap. Examples: User Interfaces may need efficient access to field validations.
Class: Layer J
Responsibility: Provides services used by Layer J+1 and delegates subtasks to Layer J-1
Collaborator: Layer J-1
Here are some typical interactions in layered architectures.
Messages that percolate downwards between layers are called Requests. For example, a client issues a request to Layer J. What Layer J cannot fulfill, it delegates to Layer J-1. Note that Layer J often translates requests from Layer J+1 into several requests to Layer J-1.
Messages that percolate upward between layers are called Notifications. A notification could start at layer J where, for example, an observer object detects an observable event. Layer J then formulates and sends a message (notification) to Layer J+1.
Layers are logical places to keep information caches. Requests that normally travel down through several layers can be cached to improve performance.
A systems programming interface is often implemented as a layer. Thus if two applications (or inter-application elements) need to communicate, placing the interface responsibilities into dedicated layers can greatly simplify the other applications layers and, as a bonus, make them more easily reusable.
The principle of separating the user interface from the application proper is old. It is rarely practiced, for all the talk we devote to it. The principle of separating the user interface from the application has the hardest consequences and is hardest to follow consistently. For the most part most applications barely separate the GUI from the application code, though they claim otherwise.
Imagine the application is totally program driven, with the user interface just one driving program. Im not talking simply of separating the interface code from the application code, I mean separate GUI and application components.
In other words: The GUI is not a part of the application. It is the first client of the application.
A typical VFP application has too much special-purpose code in the GUI. This is mostly a fundamental problem with the IDE: The easiest thing to do, by far, is to put code in those GUI control methods.
Why so much GUI code? One cause of ballooned of GUI code is that it does things that should be done by the model which well define, for now, as simply another layer somewhere. Another cause of GUI code bloat is many people tend to embed code to maintain various kinds of integrity among objects. The result of these things is know it all controls.
Another mistake many developers fall into is GUI elements that pull the data to display directly in from the domain model and then having the GUI elements update the domain objects on any changes being made. Again, nothing could be easier in VFP!
Possibly better is a system of nave controls that rely on separate Renderer objects to fill them with data from the domain model objects, and to update the domain objects with the changes made by the user.
It is easier on the user if input errors are brought up directly upon entry. Having the UI outside the application separates input from error detection. First the UI will change, so isolate and make an interface to it. Then someone will remove the human from the picture entirely, with electronic interchange or another application driving the program. Therefore, just making an interface to the UI component is not sufficient, it has to be an interface that does not care about the UI.
Therefore, put the UI totally outside the application. The application proper is bounded by a program-driven interface, and the UI is just one user of that interface, perhaps not even the first. Other users of the application could be another controlling application, or a testing application. Once the GUI is separate, almost anything is possible.
If the UI is really outside the application, what about when the user starts and then cancels a modification - where are the editing and rollback copies of the object kept? In the UI, or outside the application? What about typing errors - are they detected in the GUI or inside the application?
Answer: Keep edits in the GUI, of course! The user fumbling around in a GUI is a reality of a GUI. You want idiomatic GUI behavior and effects encapsulated in the GUI. Imagine writing a second program-driven interface, and having to deal with all these messages reminding the automated program in real-time that, say, a field is required. Yech!
Building systems requires us to bring many varied and unrelated concepts together. A typical medium or large sized system might involve diverse concepts like domain functionality, transactions, meta-data, database technology, network communication protocols, OS API calls, a GUI, etc.
Given pressure to quickly produce a reasonably fast system, its tempting to tie these concepts closely together and so embed transaction-control code in the GUI code, or OS API code in the business code. This leads systems that are:
Hard to change: if you want to change the transaction-control system you need to scour all the GUI code to find all transaction-control related stuff
Hard to understand: business code and OS API code are, in their own right, complex and hard to understand. Mix them together and the complexity multiplies before your very eyes.
Hard to write: if you're writing business code the last thing you want to be worrying about is catching os exceptions.
Write a layer of software to isolate each disparate concept or technology. These layers should isolate at the conceptual level (perhaps business code really needs to know nothing about the OS API - this is all handled transparently by some object management code) and/or at the technical level (handling unhandled exceptions raised by the object management code so they don't find their way into the business code). Isolation should be two-way (the business code 'knows' nothing of the OS API code and vice-versa).
This leads systems that are:
Easier to change: by isolating the database from the communication code we can change one or the other with minimum impact
Easier to understand: each 'bit' of the system deals with only one concept: business, networks, database
Easier to write: business people can write business code that isn't polluted with code to display dialogue boxes or handle network exceptions.
Of course the Isolation Layer itself may be complex and, possibly, represents a single point of failure. Over-application of the pattern leads to a system where everything is strongly decoupled and so the effects of system events may be unpredicatable and design or change is always 'selfish': distribution is hidden from the business designer and so they design without any thought for distribution - something which could bring the system to its knees.
Discussion: The choice a layered architecture can have many beneficial effects on your application if it is applied in the proper way. First, since the architecture is so simple, it is easy to explain to team members and so demonstrate where each object's role fits into the "big picture". If a designer is very strict about clearly defining where objects fit within the layers, and the interfaces between the layers, then the potential for reuse of many objects in the system can be greatly increased.
A common problem with many object designs is that they are too tightly constrained to the limits of the particular application being built. Many designers tend to put too much of the logic of an application in the GUI layer. In this case, there are few, if any, domain objects that are potentially available for reuse in other applications.
Another benefit of this layering is that it makes it easy to divide work along layer boundaries. It is easy to assign different teams or individuals to the work of coding the layers in a four-layer architectures, since the interfaces are identified and understood well in advance of coding. Finally, a four-layer architecture makes it possible to code the bulk of your system (in the domain model and application model layers) to be independent of the choice of persistence mechanism and windowing system.
Layers can make for great abstractions. But remember: abstractions are illusions! Beautiful, clean, elegant abstractions are still illusions. Don't be afraid to "cheat" for example when you need better performance and poke through a layers formal boundaries. This is the essence of programming. Once you have a layered architecture don't be afraid to hack, just be elegant and rigorous about how you surface your hacks to your consumers.
Buschman, F et al (1996), A System of Patterns, John Wiley & Sons, West Sussex, England, ISBN 0-471-95869-7.
Gamma, E., Helm, R., Johnson, R, and Vlissides, J. (1994), Design Patterns, Elements of Object Oriented Software, Addison Wesley, Reading, MA, ISBN 0-201-63361-2.
Keshav, S, An Engineering Approach to Computer Networking, Addison Wesley, Reading, MA, ISBN 0-201-63442-2.
Rubel, Barry, Patterns for Generating a Layered Architecture, published in: Coplien, J, and Schmidt, D (1995), Pattern Languages of Program Design, Addison Wesley, Reading, MA, ISBN 0-201-60734-4.
I also wish to thank the many contributors to the Wiki Web at http://www.c2.com who have provided many ideas.