The seminal and definitive work on software design patterns
Design patterns describe simple and elegant solutions to specific problems in OOD which reflect untold refactoring and effort. While they often take extra effort to implement, they are worth it given the increased flexibility and reusability that is gained.
Expert designers don’t solve every problem from first principles. Design patterns make it easy to reuse successful design and architecture.
1.1 What is a design pattern
A pattern has four essential elements: name, a handle to describe a design problem; problem, which explains the problem and its context; solution, which explains the elements that make up the problem but doesn’t proscribe an implementation; and consequences, any results or trade-offs that need to be considered for this pattern.
Design patterns describe OO designs.
1.2 Design Patterns in Smalltalk MVC
MVC helps to decouple views from models through the use of the Observer Pattern (293). A change in the model can update any number of Views (pub/sub). Views can be nested in MVC, and that is representative of the Composite (163) pattern which allows you to treat a group of objects as an individual object.
MVC allows you to change how an application responds to user input without changing the View, through swapping out the controller which is an example of the Strategy (315) pattern.
Other patterns MVC uses are Factory Method (107) (to pick the default controller) and Decorator (175) (to add scrolling to a view).
1.5 Organizing Design Patterns
Design pattern organization is broken up into purpose and scope. Purpose describes what a pattern does: creational, structural, or behavioral. Scope defines whether the pattern applies to classes or objects (i.e., to inheritance or composition/relationships).
See here for how scope affects the different purposes.
1.6 How design patterns solve design problems
Finding Appropriate Objects
An object encapsulates both data and procedures. There are several design methodologies including finding names and verbs, determining collaboration and responsibilities, and modeling the real world.
Of note is that many classes have no real-world counterparts and a strict modeling approach won’t reflect the realities of tomorrow nor allow enough flexibility for inevitable change.
Many of these objects are found when trying to make a design more flexible.
Specifying Object Interfaces (13)
Several basic OO terms are reviewed.
Dynamic and polymorphism discussed. Polymorphism allows for substitutability, decouples objects, simplifies definitions, and lets them vary their relationships.
Specifying Object Implementations (14)
An object’s implementation is defined by its class. Instantiation, inheritance, abstract classes, and operations, concretions, and mixin classes discussed.
Class vs. Interface Inheritance (16)
A type only refers to an object’s type. An object supports the interface defined by the class. Class inheritance defines an objects implementation/representation in terms of another object’s implementation.
Interface inheritance describes when an object can be used in place of another. Many languages don’t make the distinction explicit wherein inheritance means both interface and implementation inheritance. In these languages, people usually act as if subclasses were subtypes.
Chain of Responsibility (223) objects usually don’t share a common implementation. Composite (163) defines a common implementation. Command (233), Observer (293), State (305), and Strategy (315) are often implemented with abstract classes that are pure interfaces.
Programming to an Interface, not an Implementation (17)
Class inheritance lets you define a new kind of object rapidly in terms of the old one, but it also lets you define families of objects with identical interfaces (polymorphism depends on it).
First principle of OOD:
Program to an interface, not an implementation.
- clients remain unaware of the objects they use
- classes remain unaware of the classes they implement
Don’t declare variables to be instances of concrete classes. Commit only to an interface defined by an abstract class.
Putting Reuse Mechanisms to Work
Inheritance versus Composition
The second principle of OOD:
Favor object composition over class inheritance
Class inheritance is a form of white-box reuse (OOD Designers often overuse inheritance) wherein the subclass knows the internals of the parent class (the services that it uses). It is straightforward and easy to use, but that ease of use breaks encapsulation since the parent classes define part of the child classes representation. You also can’t swap out the parent’s implementation at runtime (like you can with dependency injection and interfaces).
Object composition is defined dynamically at runtime (interfaces, dependency injection, creational patterns) and has the added benefit of not breaking encapsulation. It keeps classes focused on one task and the communication thereof between objects inheritance and composition work together. Composition designs are often more straightforward.
Delegation makes composition just as powerful for reuse as inheritance. It allows you to compose behaviors at runtime by fulfilling the contracts it requires. However, it is harder to understand dynamic, highly parameterized software. Delegation is proper when it simplifies more than it complicates. It works best when used with standard patterns.
Inheritance versus Paramerterized Types (Generics—21)
Parameterized types are essentially generics, and they give you another method to compose object behavior. However, like inheritance, they cannot be changed at runtime.
Relating Run-Time and Compile-Time Structures
An OO program’s runtime structure doesn’t resemble its code structure. Aggregation is where one object is responsible for another and thus are fewer and more permanent. Acquaintances just know of another object, and it is much loser coupling. Both of these are determined more by intent than language constructs.
Designing for Change (24)
The key to maximizing reuse lies in anticipating new requirements and changes to existing requirements, and in designing your systems so that they can evolve accordingly.
Unanticipated changes are invariably expensive.
Common causes of redesign and their patterns:
- Creating objects directly: Abstract Factory (87), Factory Method (107), Prototype (117)
- Specific Operations Dependency: Command (233), Chain of Responsibility (223)
- Dependency on an External Entity: Abstract Factory (87), Bridge (151)
- Dependence on Specific Implementations: Abstract Factories (87), Bridge (151), Memento (283), Proxy (207)
- Algorithmic dependency: Algorithms likely to change should be isolated. Builder (97), Iterator (257), Strategy (315), Template Method (325), Visitor (331).
- Tight coupling: hard to reuse in isolation since they depend on each other. Leads to monolithic systems since you can’t change a class without understanding or changing many. Loose coupling increases the portability of a class. Abstract Factory (87), Bridge (151), Chain of Responsibility (223), Command (233), Facade (185), Mediator (273), Observer (293).
- Extending Functionality by Subclassing: requires an in-depth understanding of the parent class but heavy use of composition can make a system hard to understand. Bridge (151), Chain of Responsibility (223), Composite (163), Decorator (175), Observer (293), Strategy (315).
- Inability to Alter Classes: library, package, etc. Adapter (139), Decorator (175), Visitor (331).
Appllcation Programs (25)
Internal reuse ensures that you don’t design and implement any more than you have to.
Loose coupling increases object collaboration and extensibility. Design patterns make an application more maintainable when used to limit dependencies and layer a system.
Toolkits (packages & libraries) (26)
Composed of general purpose functionality that doesn’t impose a particular design on your application. Designing a package is harder than application design.
Dictate the architecture of your application and, subsequently, emphasize design reuse over code reuse. Frameworks are the hardest to design.
Applications are dependent on the framework, and as such, they are particularly sensitive to changes in the framework interfaces. Therefore, the application should only be loosely coupled to the framework.
1.7 How to Select a Design Pattern (28)
Ways to select a Design Pattern:
- Consider how design patterns solve design problems
- Scan intent sections
- Study how patterns interrelate
- Study patterns of like purpose
- Examine a cause of redesign
- Consider what should be variable in your design (encapsulate the concept that varies)
1.8 How to use a Design Pattern (29)
Design patterns should not be applied indiscriminately. The flexibility and variability are often gained by additional levels of indirection which often complicate a design needlessly.
A design pattern should only be applied when the flexibility it affords is actually needed.
2.2 Dcoument Structure
Recursive composition entails building increasingly complex elements out of simpler ones. The Composite (163) pattern captures this in OO terms.
2.3 Formatting (40) (Strategy)
Discusses encapsulating the formatting algorithm. The Open Closed Principle (OCP), dependency inversion principle (DIP), and Liskov substitution principle (LSP) as related to the formatting algorithm implied. Strategy pattern discussed.
2.4 Embellishing the User Interface (Decorator)
Transparent enclosure combines the notions of single child/single component composition and compatible interfaces.
What it basically equates to is an inheritance hierarchy wherein subclasses reimplement operations, but calls the parents method, to not replace it.
This is essentially the Decorator (175) pattern. It keeps clients free of embellishment code. Embellishment refers to anything that adds responsibility to an object.
2.5 Supporting Multiple Look and Feel Standards (Abstract Factory)
Abstracting object creation assists with design goals in that it allows changing a look and feel at runtime.
An Abstract Factory (87) abstracts families of objects without instantiating the objects directly. The key participants are factories and products. Factories can be selected via something as simple as an if/else construct or something more complex such as a Registry (480). A Singleton (127) can be used to manage the instances (today a DI container can do the same thing better).
2.6 Supporting Multiple Window Systems (Bridge & Abstract Factory)
Different Windowing systems (Mac, PC, Linux) change in different ways for different reasons. Creating a single abstraction, based on an intersection of functionality at one extreme or a union of functionality at the other, creates a system that is either only as powerful as the least capable system or immense and incoherent.
To solve, you encapsulate the concept that varies, which in this case is the window implementation for the particular system. The window implementation is an abstraction, possibly large but stable, that the various window variations use. By hiding this behind an implementation class, you avoid polluting the window classes with window system dependencies. The window base class constructor can use a window system factory to get the window implementation. The window object interface matches the application programmers view of the world and makes it easier to work with.
The Bridge (151) pattern allows multiple class hierarchies to work together even as they evolve independently. Its interface can be biased toward an intersection or union of functionality. The boundary, created by a Bridge, creates a stable interface for systems that change often. This allows a more programmer friendly interface to be created by other classes for general usage and also allow for application-specific business rules to use it.
The Bridge pattern could be used to isolate changes in multiple, unstable APIs that are called for the same reason.
2.7 User Operations
Functions are difficult to extend, and it’s hard to reuse parts of them. It’s hard to associate state with a function.
Encapsulate the concept that varies!