Producing a large software system is the most challenging technical
activity that the human race attempts to carry out.
The programmer, like the poet, works only slightly removed from pure
thought-stuff. He builds his castles in the air, from air, creating by
exertion of the imagination. Few media of creation are so flexible, so
easy to polish and rework, so readily capable of realizing grand
The Mythical Man-Month
Building software is vastly different from building computers, building bridges, and building skyscrapers. Software is at its core a conceptual entity, made up of algorithms and relationships between data items. It is very difficult to visualize with a schematic, a scale model, or a floor plan. It doesn't have verbatim repeated elements; these are compressed into loops and subroutines. Software has a very large number of states, most of which can never be tested. Software "maintenance" is not the same as computer, bridge, or skyscraper maintenance. Software parts do not wear out or need oiling. Instead, their functionality is changed to suit new cases. Software, even after it is fielded, is changed far more frequently than computers, bridges, and skyscrapers, mostly because it is much cheaper to change.
These differences create opportunities which are not possible in other forms of engineering, as well as create new challenges. Computer, bridge, and skyscraper engineering, because they use relatively few basic architectures, have detailed handbooks on design: when and why to use an architecture, how to use it, and consequences of using it. Algorithm and data structure catalogs, while useful, have not addressed system design at the same level. Software patterns are a first step toward a design handbook for software engineering: the enumeration and elaboration of the basic parts of the conceptual side of software.
Grow, don't build, software.
No Silver Bullet
For decades, software engineers have practiced the "waterfall" model of development. It proceeds in a straight line through five stages:
An alternative form of software development is instead based on the ability and necessity of software to change, and does not distinguish between change after delivery (maintenance) with change before delivery (construction). The idea is to iterate steps 1-4, each time producing a working system that has a few additional features. Darrel Ince calls it evolutionary prototyping, where a prototype is constantly refined, to distinguish it from throw-away prototyping, where a prototype is built in a quick-and-dirty manner and then rewritten. The motivation behind the latter is to minimize your investment; the motivation behind the former is to maximize the value of your investment. In evolutionary prototyping, features accumulate rather than needing to be rewritten each time. Tested code does not need to be constantly re-verified. Pieces of the problem are removed from consideration instead of being re-considered on every rewrite.
The key to evolutionary prototyping is to incorporate flexibility as an explicit design requirement. The program must stay flexible even after numerous enhancements, or it will turn into rubble. This isn't easy, mostly because we lack the ability to document exactly where and to what degree the program must remain flexible. This is the challenge of variabilities and decoupling, described below. Design patterns are well suited to providing such documentation.
A difficulty is that software design has not yet been simplified enough so that we can express a design as the combination of a few basic parts. This is necessary for a linguistic approach to work. Unicon is a first attempt at an architecture language which is based on a handful of basic architectures. Such languages potentially allow instantiation of ready-made architectures, verification that an architecture is being used properly, substituting one architecture for another, and composition of architectures. This is a more ambitious approach than the current use of patterns: as mental and documentation aids.
The obstacle to component software is not just standardization but also the complexity of its implementation. Object-oriented component software requires a distributed shared memory based on objects which can exist in and migrate between many different formats (distributed object management). It requires component containers which manage a coherent visual and persistent representation (compound documents). It also requires the ability to transfer data and objects between independently engineered components (uniform data transfer).
Compilers were once very complex and mysterious, consequently language development was slow, because it was quite a feat to implement a high-level language (e.g. Algol 68). But the patterns in compiler implementations were studied, and found to be similar to other programming tasks, like regular expression matching and rewrite systems. Then books appeared on compiler development and the basic patterns were taught in programming courses. Nowadays we write excellent compilers for sophisticated languages like C++, Common LISP, and Haskell. The same can happen with component software, once we find the patterns.
A global technique to reduce coupling is to distribute knowledge on a "need-to-know" basis. That is, module A doesn't know about module B unless B has an immediate role in A's job. This is the main reason to avoid sprawling global namespaces. Furthermore, what A knows about B is limited to the role that B has to A. This is the motivation behind encapsulation and multiple interfaces.
A more localized technique to reduce coupling is to anticipate where change will occur and then to facilitate that kind of change. Such places of likely change are called variabilities. Marshall Cline has used the analogy of joints along a skeleton. To extend the analogy, the degree of variability is like the range of motion, and coupling is like a stray vein connecting your elbow to your torso. Requirements analysis can help in determining the placement and degree of variabilities.
However, it is the job of the designer to make sure that encapsulation and variabilities are preserved through changes. Encapsulation says "these modules should not interact", while a variability says "this module should be replaceable." The word "should" stresses that these are policies, not mechanisms. Decoupling is a non-functional requirement: it does not, by itself, make code fulfill its job any better. Unfortunately, analysis and design has traditionally focused on functional requirements. Patterns are designed to express policies, and so form a valuable addition to the designer's vocabulary.
In the waterfall model, errors in design are much worse than errors in implementation or testing, because they occur earlier in the cycle. Evolutionary prototyping exacerbates this imbalance, because it also requires self-preserving designs.
Because designs are conceptual, we need novel ways to test them. The most common technique is to have the design reviewed by peers (see evaluating a design), including a "walk-through" where the design is mentally executed step-by-step. Patterns offer another, much simpler, technique: use a design which has proved successful in the past.
Many different architectures can be good designs for the same task. Hopefully, this course will expand your mind to the possible designs, as well as give you the ability to choose and articulate a design. In this course, we will use these four basic principles to evaluate patterns. Afterwards, you can use the patterns themselves to evaluate designs.
Designing object-oriented software is hard, and designing reusable
object-oriented software is even harder. Experience shows that many
object-oriented systems exhibit recurring structures or design
patterns of communicating and collaborating objects that promote
extensibility, flexibility, and reusability.
The pattern form used in this course is:
A pattern is not a new way of solving the problem: by definition, the solution has been used before, usually many times. The value of a pattern is how it is presented. Because of pattern form, patterns articulate design decisions concisely, including how they meet non-functional requirements. In this way, they communicate successful design knowledge between programmers. Patterns help develop good habits that make it easy to write good code.
Design patterns usually do not entail complex implementations. The difficulty in patterns is knowing when and why to use them. This course focuses on case study of patterns in action, not implementing the patterns yourself. Therefore, an important complement to this course is looking for where patterns could be applied in your own software projects.
Fredrick Brooks. "No Silver Bullet", in Communications of the ACM, April, 1987.
Darrel Ince. Software Development: Fashioning the Baroque. Oxford University Press, Oxford, 1988.
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, Reading, MA, 1994.
Mary Shaw and David Garlan. Software Architecture: Perspectives on an Emerging Discipline. Prentice Hall, 1996.
"Software Patterns" in Communications of the ACM, October, 1996.
Robert Orfali, Dan Harkey, Jeri Edwards. The Essential Distributed Objects Survival Guide. John Wiley & Sons, 1996.