Patterns are everywhere! In architecture, patterns help architects plan buildings and discuss their projects. In programming, they help programmers organize programs and think about the code. They also help to create beautiful knitwear, and help people navigate safely through traffic—in short, they affect your everyday life.
The human brain is a pattern—finding analog computer, so it is not surprising that we humans like to base our life around patterns. We programmers are especially fond of organized, pattern—based thinking.
There are different areas of programming where patterns can be applied, from organizational aspects to coding. This book deals mostly with a subset of programming patterns, namely design patterns. Before we start describing and implementing different design patterns, however, I'd like to talk to you a bit about the history of patterns, their best points, and how they are often misused in practice.
This chapter will cover the following topics:
- What design patterns are
- Why patterns are useful
- The difference between patterns and idioms
- The origins of design patterns
- The classification of common patterns
- Pattern misuse and anti-patterns
The concept of a pattern is simple to define. A pattern is something that you did in the past, was successful, and can be applied to multiple situations. Patterns capture experiences in software development that have been proven to work again and again, and thus provide a solution to specific problems. They are not invented: they arise from practical experience.
When many programmers are trying to solve similar problems, they arrive again and again at a solution that works best. Such a solution is later distilled into a solution template, something that we programmers then use to approach similar problems in the future. Such solution templates are often called patterns.
Good patterns are problem and language agnostic. In other words, they apply to C++ and Delphi, and to Haskell and Smalltalk. In practice, as it turns out, lots of patterns are at least partially specific to a particular environment. Lots of them, for example, work best with object-oriented programming (OOP) languages and do not work with functional languages.
In programming, patterns serve a dual role. Besides being a template for problem solving, they also provide a common vocabulary that programmers around the world can use to discuss problems. It is much simpler to say, for example, that we will use an observer pattern to notify subsystems of state changes than it is to talk at length about how that part will be implemented. Using patterns as a base for discussion therefore forces us to talk about implementation concepts, and not about the detailed implementation specifics.
It is important to note that patterns provide only a template for a solution and not a detailed recipe. You will still have to take care of the code and make sure that the pattern implementation makes sense and works well with the rest of the program.
Programming patterns can be split into three groups that cover different abstraction levels. At the very top, we talk about architectural patterns. They deal with program organization as a whole, with a wide, top-down view, but they do not deal with implementation. For example, the famous Model-View-ViewModel approach is an architectural pattern that deals with a user interface-business logic split.
Architectural patterns are not a topic of this book, but still I'll dedicate some space to them in Chapter 11, Other Kinds of Patterns.
A bit lower down the abstraction scale are design patterns. They describe the run—time behavior of a program. When we use design patterns, we are trying to solve a specific problem in code, but we don't want to go fully to the code level. Design patterns will be the topic of the first ten chapters.
Patterns that work fully on the code level are called idioms. Idioms are usually language specific and provide templates for commonly encountered coding problems. For example, a standard way of creating/destroying objects in Delphi is an idiom, as is iterating over an enumerable container with the
Idioms are not the topic of this book. I will, however, mention the most important Delphi idioms, while talking about their specific implementation for some of the patterns.
This is not a book about the theory behind patterns; rather, this book focuses on the aspects of their implementation. Before I scare you all off with all this talk about design patterns, their history, modern advances, anti-patterns, and so on, I have decided to present a very simple pattern using an example. A few lines of code should explain why a pattern—based approach to problem solving can be a good thing.
In the code archive for this chapter, you'll find a simple console application called
DesignPatternExample. Inside, you'll find an implementation of a sparse array, as shown in the following code fragment:
type TSparseRec = record IsEmpty: boolean; Value : integer; end; TSparseArray = TArray<TSparseRec>;
Each array index can either be empty (in which case
IsEmpty will be set to
True), or it can contain a value (in which case
IsEmpty will be set to
Value contains the value).
If we have a variable of the data:
TSparseArraytype, we can iterate over it with the following code:
for i := Low(data) to High(data) do if not data[i].IsEmpty then Process(data[i].Value);
When you need a similar iteration in some other part of the program, you have to type this short fragment again. Of course, you could also be smart and just copy and paste the first two lines (
if). This is simple but problematic, because it leads to the copy and paste anti-pattern, which I'll discuss later in this chapter.
For now, let's imagine the following hypothetical scenario. Let's say that at some point, you start introducing nullable types into this code. We already have ready to use nullable types available in the Spring4D library (https://bitbucket.org/sglienke/spring4d), and it was suggested that they will appear in the next major Delphi release after 10.2 Tokyo, so this is definitely something that could happen.
In Spring4D, nullable types are implemented as a
Nullable<T> record, which is partially shown in the following code:
type Nullable<T> = record ... property HasValue: Boolean read GetHasValue; property Value: T read GetValue; end;
As far as we know, Delphi's implementation will expose the same properties:
You can then redefine
TSparseArray as an array of
Nullable<integer>, as the following code:
type TSparseArray = TArray<Nullable<integer>>;
This is all well and good, but we now have to fix all the places in the code where
IsEmpty is called and replace it with
HasValue. We also have to change the program logic in all of these places. If the code was testing the result of
IsEmpty, we would have to use
not HasValue and vice versa. This is all very tedious and error prone. When making such a change in a big program, you can easily forget to insert or remove the not, and that breaks the program.
Wouldn't it be much better if there were only one place in the program when that
if iteration construct was implemented? We would only have to correct code at that one location and— voila!—the program would be working again. Welcome to the Iterator pattern!
We'll discuss this pattern at length in Chapter 7,Iterator, Visitor, Observer, and Memento. For now, I will just give you a practical example.
The simplest way to add an iterator pattern to
TScatteredArray is to use a method that accepts such an array and an iteration method, that is, a piece of code that is executed for each non empty element of the array. As the next code example shows, this is simple to achieve with Delphi's anonymous methods:
procedure Iterate(const data: TSparseArray; const iterator: TProc<integer>); var i: Integer; begin for i := Low(data) to High(data) do if not data[i].IsEmpty then iterator(data[i].Value); end;
In this example,
data is the sparse array that we want to array over, and
iterator represents the anonymous method that will be executed for each non null element. The
TProc<integer> notation specifies a procedure accepting one
integer argument (
TProc<T> is a type declared in
As we don't want to make a full copy of the array data each time
Iterate is called, the
data parameter is marked with a
const qualifier. This can make a big difference in the execution speed. The
const on the
iterator parameter is just a minor optimization that stops the iterator's reference count being incremented while the
Iterate is executing. Anonymous methods are internally implemented as interfaces in Delphi, and they are managed in the same way.
In the following code, we call
Iterate and pass it the array to be iterated upon (
data), and an anonymous method will be executed for each non empty element:
Iterate(data, procedure (value: integer) begin Process(value); end);
If we had to adapt this code to a nullable-based implementation, we would just edit the
Iterate method and change
not data[i].IsEmpty into
data[i].HasValue—simple, effective, and, most importantly, foolproof!
Delphi also offers us a nice idiom that we can implement in an iterator pattern: enumerators and the
for..in language construct. Using this idiom we can iterate over our sparse array with the following elegant code:
for value in data.Iterator do Process(value);
I will leave the implementation details for Chapter 7, Iterator, Visitor, Observer, and Memento. You are, of course, welcome to examine the demonstration project
DesignPatternExample to see how
data.Iterator is implemented (hint: start at
Patterns are mostly language independent. We could have written an equivalent of the
Iterate method from the previous sections in most languages, even in old Turbo Pascal for DOS or in an assembler. The
for..in construct, however, is specific to Delphi. We call such a low-level pattern an idiom.
Idioms are not that useful when we are thinking about or discussing the program design. The knowledge of a language's is, however, necessary for you to become fluent in a language. Idioms teach us about the best ways of performing common operations in a particular environment.
The most important Delphi idiom concerns how object creation and destruction should be handled in code. It is used whenever we require a common three-step operation: create an object, do something with it, destroy the object.
It must be said that this idiom applies only to Windows and OS X development. Compilers for Android, iOS, and Linux support Automatic Reference Counting (ARC), which means that objects are handled the same way as interfaces.
This idiom also shows how we can run into problems if we stray from the path and try to manage objects in a different manner. But first, I'd like to show you the recommended ways of handling objects in code. All examples can be found in the demonstration project
For simplicity's sake, we'll be using two objects:
obj2 of type
TObject, as shown in the following code:
var obj1, obj2: TObject;
In practice, you'll be using a different class, as there's not much that a
TObject could be used for. But all other details (that is, the idiom) will remain the same.
The first idiomatic way of handling objects is shown in the following code. Let's call it Variant A:
obj1 := TObject.Create; try // some code finally FreeAndNil(obj1); end;
Firstly, we create the object. Then we enter a
try..finally construct and execute some code on object
obj1. In the end, the object is destroyed in the
finally part. If the
// some code part raises an exception, it is caught, and the object is safely destroyed in the
Is it better to use
FreeAndNil(obj1)? There is a big debate regarding this in the Delphi community, and verdict is inconclusive. I prefer
FreeAndNil because it doesn't leave dangling references to uninitialized memory.
Variant A is short and concise, but it becomes unwieldy when you need more than one object. To create two objects, do something with them, and then destroy them, we have to nest the
try..finally constructs, as shown in the following code fragment:
obj1 := TObject.Create; try obj2 := TObject.Create; try // some code finally FreeAndNil(obj2); end; finally FreeAndNil(obj1); end;
This approach correctly handles the
obj1 destruction when an exception is raised inside any code dealing with
obj2, including its creation.
The long-windedness of Variant A makes many programmers adopt the following designed approach:
try obj1 := TObject.Create; obj2 := TObject.Create; // some code finally FreeAndNil(obj1); FreeAndNil(obj2); end;
Let me say it loud and clear: this technique does not work correctly! If you are using such an approach in your code, you should fix the code!
The problem here is that creating
obj2 may fail. The
TObject.Create phrase will succeed for sure (unless you run out of memory), but in a real-life example, a different object may raise an exception inside the constructor. If that happens, the code will firstly destroy
obj1 and then it will proceed with destroying
This variable, however, is not initialized, and so the code will try to destroy some random part of memory, which will in most cases lead to access violation—that is, if you're lucky. If you're not, it will just corrupt a part of another object and cause a random and potentially very wrong behavior of the program.
try obj1 := TObject.Create; // some code finally FreeAndNil(obj1); end;
obj1 constructor fails, the code will try to free the object by referencing the uninitialized
obj1, and that will again cause problems.
In such situations, we can use Variant B of the idiom as follows:
obj1 := nil; try obj1 := TObject.Create; // some code finally FreeAndNil(obj1); end;
Now, we can be sure that
obj1 will either contain
nil or a reference to a valid object. The code will work because
TObject.Free, which is called from
FreeAndNil, disposes of an object in the following manner:
procedure TObject.Free; begin if Self <> nil then Destroy; end;
If the object (
Self in this code) is already
nil, then calling
Free does nothing.
Variant B also nicely expands to multiple objects, as shown in the following code:
obj1 := nil; obj2 := nil; try obj1 := TObject.Create; obj2 := TObject.Create; // some code finally FreeAndNil(obj1); FreeAndNil(obj2); end;
Again, all object variables are always correctly initialized and the destruction works properly.
The only problem with Variant B is that
obj2 doesn't get destroyed if the destructor for
obj1 raises an exception. Raising an exception in a destructor is, however, something that you should definitely not do, as that may also cause the object to be only partially destroyed.
The design pattern movement (as it applies to programming) was started by the Gang of Four. By Gang of Four, we don't mean the Chinese Cultural Revolution leaders from the seventies or a post-punk group from Leeds, but four authors of a prominent book: Design Patterns: Elements of Reusable Object-Oriented Software. This book, written by Erich Gamma, Richard Helm, Ralph Johson, and John Vlissides, was published in 1994, and thoroughly shook the programming community.
Back in 1994, when C++ was becoming more and more prominent, object orientation was all the rage, and people were programming in Smalltalk. Programmers were simply not thinking in terms of patterns. Every good programmer, of course, had their own book of recipes that work, but they were not sharing them or trying to describe them in a formal way. The GoF book, as it is mostly called in informal speech, changed all that.
The majority of the book is dedicated to 23 (now classic) software design patterns. The authors started with a rationale for each one, providing a formal description of the pattern and examples in Smalltalk and C++. These patterns now provide the very core of a programmer's toolset, although later, more patterns were discovered and formalized. Notably missing from the book are design patterns that relate to parallel programming (multi-threading).
In the first two chapters of their book, the authors explored the power and the pitfalls of OOP. They drew two important conclusions: you should program to an interface, not an implementation, and favor object composition over class inheritance.
The latter contradicts the whole object-oriented movement, which preached class hierarchy and inheritance. As the distinction between the two approaches is not well known in the Delphi world, I have prepared a short example in the next section.
If you are a programmer of a certain age, it will be hard for you, as it was for me, to accept the don't inherit—compose philosophy. After all, we were taught that OOP is the key to everything and that it will fix all our problems.
That was indeed the dream behind the OOP movement. The practice, however, dared to disagree. In most real-life scenarios, the OOP approach leads only to mess and ugly code. The following short example will succinctly demonstrate why this happens.
Let's say we would like to write a class that implements a list of only three operations. We'd like to add integer numbers (
Add), get the size of the list (
Count), and read each element (
Items). Our application will use this list to simulate a data structure from which elements can never be removed and where data, once added, can never be modified. We would therefore like to prevent every user of this class, from calling methods that will break those assumptions.
We can approach this problem in three different ways. Firstly, we can write the code from scratch. We are, however, lazy, and we want Delphi's
TList to do the actual work. Secondly, we can inherit from
TList and write a derived class
TInheritedLimitedList that supports only the three operations we need. Thirdly, we can create a new base class
TCompositedLimitedList that uses
TList for data storage. The second and third approach are both shown in the project called
CompositionVsInheritance, which you can find in the code archive for this chapter.
When we start to implement the inherited version of
TList, we immediately run into problems. The first one is that
TList simply implements lots of functionality that we don't want in our class. An example of such methods would be
Clear, and so on.
The second problem is that inheriting from
TList was simply not a factor when that class was designed. Almost all of its methods are static, not virtual, and as such cannot really be overridden. We can only
reintroduce them, and that, as we'll see very soon, can cause unforeseen problems.
Another problematic part is the
Clear method. We don't want to allow the users of our class to call it, but, still, it is implicitly called from
TList.Destroy, and so we cannot fully disable it.
We would also like to access the elements as
integer and not as
Pointer data. To do this, we also have to reintroduce the
A full declaration of the
TInheritedLimitedList class is shown next. You will notice that we have to reintroduce a whole bunch of methods:
type TInheritedLimitedList = class(TList) strict private FAllowClear: boolean; protected function Get(Index: Integer): Integer; procedure Put(Index: Integer; const Value: Integer); public destructor Destroy; override; function Add(Item: Integer): Integer; inline; procedure Clear; override; procedure Delete(Index: Integer); reintroduce; procedure Exchange(Index1, Index2: Integer); reintroduce; function Expand: TList; reintroduce; function Extract(Item: Pointer): Pointer; reintroduce; function First: Pointer; reintroduce; function GetEnumerator: TListEnumerator; reintroduce; procedure Insert(Index: Integer; Item: Pointer); reintroduce; function Last: Pointer; reintroduce; procedure Move(CurIndex, NewIndex: Integer); reintroduce; function Remove(Item: Pointer): Integer; reintroduce; function RemoveItem(Item: Pointer; Direction: TList.TDirection): Integer; reintroduce; property Items[Index: Integer]: Integer read Get write Put; default; end;
Some parts of the implementation are trivial. The next code fragment shows how
Exchange are disabled:
procedure TInheritedLimitedList.Delete(Index: Integer); begin raise Exception.Create('Not supported'); end; procedure TInheritedLimitedList.Exchange(Index1, Index2: Integer); begin raise Exception.Create('Not supported'); end;
Most of the implementation is equally dull, so I won't show it here. The demo project contains a fully implemented class that you can peruse in peace. Still, I'd like to point out two implementation details.
The first is the
Items property. We had to reintroduce it, as we'd like to work with integers, not pointers. It is also implemented in a way that allows read-only access:
function TInheritedLimitedList.Get(Index: Integer): Integer; begin Result := Integer(inherited Get(Index)); end; procedure TInheritedLimitedList.Put(Index: Integer; const Value: Integer); begin raise Exception.Create('Not supported'); end;
The second interesting detail is the implementation of the
Clear method. It is normally disabled (because calling
Clear would result in an exception). The
Destroy destructor, however, sets an internal flag that allows
Clear to be called from the inherited destructor, as shown in the following code:
destructor TInheritedLimitedList.Destroy; begin FAllowClear := true; inherited; end; procedure TInheritedLimitedList.Clear; begin if FAllowClear then inherited else raise Exception.Create('Not supported'); end;
There are numerous problems with this approach. We had to introduce some weird hacks, and write a bunch of code to disable functions that should not be used. This is partially caused by the bad
TList design (bad from an object-oriented viewpoint), which does not allow us to override virtual methods. But worst of all is the fact that our inheritance based list still doesn't work correctly!
Looking at the following code fragment, everything seems OK. If we run it, we get an exception in the
list := 42 statement:
var list: TInheritedLimitedList; list.Add(1); list.Add(2); list.Add(3); list := 42;
If, however, we pass this list to another method that expects to get a
TList, that method would be able to modify our list! The following code fragment changes the list to contain elements
procedure ChangeList(list: TList); begin list := pointer(42); end; var list: TInheritedLimitedList; list.Add(1); list.Add(2); list.Add(3); ChangeList(list);
This happens because
Put in the original
TList are not
virtual. Because of this, the compiler has no idea that a derived class can override them and just blindly calls the
TList version. Assigning to
ChangeList therefore uses
TList.Put, which doesn't raise an exception.
Raising exceptions to report coding errors is another problem with this approach. When working with strongly typed languages, such as Delphi, we would like such coding problems to be caught by the compiler, not during testing.
Compared to inheritance, implementing a list by using composition is totally trivial. We just have to declare a class that exposes the required functionality and write a few methods that use an internal
FList: TList object to implement this functionality. All our public methods are very simple and only map to methods of the internal object. By declaring them
inline, the compiler will actually create almost identical code to the one we would get if we are use
TList instead of
TCompositedLimitedList in our code. As the implementation is so simple, as you can see from the following code it can be fully included in the book:
type TCompositedLimitedList = class strict private FList: TList; strict protected function Get(Index: Integer): Pointer; inline; function GetCount: Integer; inline; public constructor Create; destructor Destroy; override; function Add(Item: Pointer): Integer; inline; property Count: Integer read GetCount; property Items[Index: Integer]: Pointer read Get; default; end; constructor TCompositedLimitedList.Create; begin inherited Create; FList := TList.Create; end; destructor TCompositedLimitedList.Destroy; begin FList.Free; inherited; end; function TCompositedLimitedList.Add(Item: Pointer): Integer; begin Result := FList.Add(Item); end; function TCompositedLimitedList.Get(Index: Integer): Pointer; begin Result := FList[Index]; end; function TCompositedLimitedList.GetCount: Integer; begin Result := FList.Count; end;
The original Gang of Four book separated patterns into three categories: creational, structural, and behavioral. To these three, another large category was added in recent years, concurrency patterns. Some concurrency patterns were covered in another classic book: Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects, Volume 2, by Douglas C Schmidt, Michael Stal, Hans Rohnert, and Frank Buschmann.
Creational patterns deal with delegation. They are focused on creating new objects and groups of related objects. These patterns will create objects for you, meaning that you don't have to create them directly.
Behavioral patterns are big on consultation they talk about responsibilities between objects. Unlike structural patterns, which only specify a structure, behavioral patterns define communication paths and messages.
Concurrency patterns deal with cooperation. They make a system composed of multiple components, running in parallel, work together. The main concerns of concurrency patterns are resource protection and messaging.
The next few pages will give an overview of the most important design patterns, organized by category and sorted by name.
- Abstract factory pattern: You can use this pattern to create entire families of related objects but without the need to specify their classes. This pattern is described in Chapter 3, Factory Method, Abstract Factory, Prototype, and Builder.
- Builder pattern: This pattern abstracts the construction of a complex object, and allows the same process to create different representations. It is described inChapter 2, Singleton, Dependency Injection, Lazy Initialization, and Object Pool.
- Dependency injection pattern: This is used to send specific instances of depended objects into a class (injecting them), instead of the class creating them directly. This pattern is described inChapter 3, Factory Method, Abstract Factory, Prototype, and Builder.
- Factory method pattern: This pattern defines an interface for creating a single object. Subclasses can then decide which class to instantiate. This is described inChapter 3, Factory Method, Abstract Factory, Prototype, and Builder.
- Lazy initialization pattern: This delays the creation of an object or the calculation of a value until it is actually needed. In the GoF book, it appeared as a virtual proxy. This pattern is described inChapter 3, Factory Method, Abstract Factory, Prototype, and Builder.
- Multiton pattern: This pattern similar to singleton. It allows multiple named instances, while serving as the only way of accessing them. It is not covered in this book.
- Object pool pattern: This pattern recycles objects to avoid the expensive acquisition and creation of resources. A special case, connection pool, is well-known to all database programmers. This pattern is described inChapter 2, Singleton, Dependency Injection, Lazy Initialization, and Object Pool.
- Prototype pattern: This specifies how to create objects based on a template object that is cloned to produce new objects. This pattern is described inChapter 2, Singleton, Dependency Injection, Lazy Initialization, and Object Pool.
- Resource acquisition is initialization pattern (RAII): This pattern ensures that resources are properly released by tying them to the lifespan of an object. In Delphi, we implement this pattern by using an interface as a resource owner. It is not covered in this book.
- Singleton pattern: This pattern ensures that a class has only one instance. It also provides a common point of access to that instance. This pattern is described inChapter 2, Singleton, Dependency Injection, Lazy Initialization, and Object Pool.
- Adapter pattern: This pattern, which is also known as a wrapper or a translator pattern, converts the interface of a class into another interface expected by a client. It is described in Chapter 5, Adapter, Decorator, Facade, and Proxy.
- Bridge pattern: This decouples an abstraction from its implementation, which allows the two to vary independently. This pattern is described inChapter 4, Composite, Flyweight, Marker Interface, and Bridge.
- Composite pattern: This composes from the hierarchies of more basic objects. This pattern is described inChapter 4, Composite, Flyweight, Marker Interface, and Bridge.
- Decorator pattern: This pattern allows an object to take additional responsibilities, in addition to its original interface. Decorators are an alternative to subclassing for an extending functionality. This pattern is described inChapter 5, Adapter, Proxy, Decorator, and Facade.
- Extension object pattern: This pattern allows you to adding of a functionality to a hierarchy without changing that hierarchy. This is not covered in this book.
- Facade pattern: This pattern combines a set of interfaces exposed by subsystems into a simpler interface that is easier to use. This pattern is described inChapter 5, Adapter, Proxy, Decorator, and Facade.
- Flyweight pattern: This pattern uses data-sharing to efficiently support large numbers of similar objects. It is described inChapter 5, Adapter, Proxy, Decorator, and Facade.
- Front controller pattern: This pattern is used when designing web applications and provides a centralized entry point for request handling. It is not covered in this book.
- Marker pattern: This allows us to associate metadata with a class. This pattern is described inChapter 4, Composite, Flyweight, Marker Interface, and Bridge.
- Module pattern: This pattern groups several related elements into one conceptual entity. It is not covered in this book.
- Proxy pattern: It provides a replacement for another object so it can control access to it. This pattern is described inChapter 5, Adapter, Proxy, Decorator, and Facade.
- Twin pattern: This pattern helps simulating multiple inheritance in programming languages that don't support this feature. It is not covered in this book.
- Blackboard pattern: This is an artificial intelligence (AI) pattern for combining different data sources. It is not covered in this book.
- Chain of responsibility pattern: This is an object-oriented version of an
ifladder idiom (
if ... else if ... else if ... else). It works by constructing a chain of processing objects. It is not covered in this book.
- Command pattern: This pattern encapsulates a request as an object. It is especially useful for building user interfaces where it allows for the support of undoable operations. This pattern is described inChapter 6, Nullable Value, Template Method, Command, and State.
- Interpreter pattern: This pattern defines a representation of a language grammar and gives an interpreter for that grammar. It is not covered in this book.
- Iterator pattern: This provides a way to access elements of an aggregate object (list, array, symbol table, tree, and so on) sequentially, without exposing the underlying implementation of that object. This pattern is described inChapter 7, Iterator, Visitor, Observer, and Memento.
- Mediator pattern: This defines an object that handles interaction between other objects. This pattern supports loose coupling by preventing objects from referring to one another explicitly. It is not covered in this book.
- Memento pattern: This specifies how to store and restore an object's internal state without violating encapsulation. This pattern is described inChapter 7, Iterator, Visitor, Observer, and Memento.
- Null object pattern: This removes the reason for using a
nilpointer, by providing a special, default value for a class. This pattern is described inChapter 6, Nullable Value, Template Method, Command, and State.
- Observer pattern: This pattern is also known as a publish/subscribe pattern. It provides another way to prevent tight coupling in a system, by setting up a system where a change of objects results in all of its dependents being notified about the change. This pattern is described inChapter 7, Iterator, Visitor, Observer, and Memento.
- Servant pattern: This pattern defines an object that implements a common functionality for a group of classes. It is not covered in this book.
- Specification pattern: This pattern provides support for business logic that can be recombined by chaining the rules together with boolean operations. It is not covered in this book.
- State pattern:This allows an object to change its behavior when there is a change to its internal state.It is described inChapter 6, Nullable Value, Template Method, Command, and State.
- Strategy pattern: This pattern defines a family of algorithms that can be used interchangeably. It is not covered in this book.
- Template method pattern: This defines a skeleton of on operation and defers some steps to subclasses. This pattern is described inChapter 6, Nullable Value, Template Method, Command, and State.
- Visitor pattern: This pattern specifies an operation that is performed on all elements of an object's internal structure. It is described inChapter 7, Iterator, Visitor, Observer, and Memento.
- Active object pattern: This pattern hides the concurrency by implementing asynchronous method inside an object, which serves as a scheduler for handling requests. It is not covered in this book.
- Binding properties pattern: This pattern combines multiple observers to force synchronization on properties in different objects. It is not covered in this book.
- Blockchain pattern: This provides a decentralized way for storing data in a linked list protected with cryptographic means. It is not covered in this book.
- Compute kernel pattern: This pattern executes the same calculation many times in parallel, differing only on integer input parameters. It is frequently related to GPU calculation. It is not covered in this book.
- Double-checked locking pattern: This reduces the overhead of acquiring a lock in a safe manner. This pattern is described inChapter 8, Locking patterns.
- Event-based asynchronous pattern: This pattern defines a way of executing parallel operations where a caller is notified when a worker finishes the execution. It is not explicitly described in this book, although it is used as a basis for concrete implementations inChapter 9, Thread pool, Messaging, Future and Pipeline.
- Future pattern: This pattern pushes a calculation into a background and replaces it with a promise that a result will be available in the future. It is described inChapter 9, Thread pool, Messaging, Future and Pipeline.
- Guarded suspension pattern: This pattern manages operations that depend on a two-part condition: a precondition that must be satisfied and a lock that must be acquired. It is not covered in this book.
- Join pattern: This pattern provides a way to write distributed and parallel systems, by message passing. It is not covered in this book.
- Lock pattern: This protects shared resources by implementing a locking mechanism. This pattern is described inChapter 8, Locking patterns.
- Lock striping pattern: This pattern optimizes locking, by replacing a single global lock with a set of specialized locks. It is described in Chapter 8, Locking patterns.
- Messaging design pattern (MDP): This is based on the interchange of information between components in the system. This pattern is described inChapter 9, Thread Pool, Messaging, Future,and Pipeline.
- Monitor object pattern: This pattern combines locking with a mechanism for signalling other threads that their condition was met. It is not covered in this book.
- Optimistic initialization pattern: This reduces the cost of locking by replacing it with the small probability of extraneous objects being created and thrown away. This pattern is described inChapter 8,Locking patterns.
- Pipeline pattern: This pattern specifies a way of decoupling thread dependencies by passing small subsets of data from one worker thread to another through a message-passing pipeline. It is described inChapter 9, Thread Pool, Messaging, Future, and Pipeline.
- Reactor pattern: This is a reactor object that provides an asynchronous interface to resources that must be handled synchronously. It is not covered in this book.
- Read-write lock pattern: This allows multiple objects to simultaneously read a shared resource, but forces exclusive access for write operations. This pattern is described inChapter 8,Locking patterns.
- Scheduler pattern: This pattern controls when threads may execute single-threaded code. It is not covered in this book.
- Thread pool pattern: This is a parallel version of an object pool creational pattern that provides a pool of worker threads that execute numerous tasks. It is described inChapter 9, Thread Pool, Messaging, Future, and Pipeline.
- Thread-specific storage pattern: This allows us to use global memory that is local to a thread. In Delphi, we implement this by declaring a variable with the
threadvardirective. It is not covered in this book.
While design patterns are undoubtedly a useful tool, many prominent computer scientists have expressed criticism directed both at the Design Patterns: Elements of Reusable Object-Oriented Software book and at the general way patterns are used in practice.
Over the years, programmers have learned that the patterns in the GoF book really aren't as widely applicable as the authors thought. Lots of them only apply to the object-oriented world. If we try to use them with a functional language, they are largely useless. They can also be greatly simplified when used in aspect-oriented languages.
Delphi, however, is an object-oriented language, so we can reuse most of the Gang of Four observations. Still, it is important to keep in mind how patterns should be used in practice.
As a main rule, you should never use design patterns to architect the software. Design patterns tell you how to write code to solve a specific problem while software design should be done on a higher abstraction level, without referring to implementation details. Rather, you should use them to approach specific problems that you run into while programming.
The second idea that should be present in your mind at all times is that patterns are not the goal: they are just a tool. Design patterns formalize only some aspects of programming, not all of it. You should also never follow a design pattern blindly. Think about what it says, think about how it applies to your problem, and then use it wisely. Programming is a craft, not a paint by numbers book, and patterns are not a silver bullet.
If you look at the code and the pattern stands out to you, it was not implemented correctly. A good pattern will hide in the code, and only a careful observer will be able to say: Oh! I see you used a visitor pattern here. Nice. It is important to understand the concepts behind the design patterns, and not the exact names of the methods and properties that were used in an implementation you found somewhere (even if that was in the Design Patterns book).
Design patterns are a great tool for refactoring and communication. Hey, gals and guys, this part of our system is completely messed up, and we should use a publish/subscribe pattern instead, is a type of a statement that should appear more frequently in our discussions!
Every yin has its yang, and every hero has their dark side, and so the very existence of patterns suggest that there exists the opposite. We could simply call it a mess, but programmers try to find an order to everything, even in chaos, and so they cataloged the mess and described the many kinds of anti-patterns. I will only briefly touch on this topic, as the goal of this book is to teach you about order, not disorder, but there is always something to learn from bad examples.
Design patterns are nicely classified, and most programmers agree on how they should be named and defined. Anti-patterns, on the other hand, are messier. They hide behind different names and they provide mere sketches of behavior, not fully defined templates.
The nastiest of the anti-patterns is sometimes called a big ball of mud. A typical sign of this anti-pattern is that the code is a mess, no matter how you look at it. It is unreadable: the data is global; every class uses every other class, except for the code that no one uses at all; and so on and so on. If you are ever hired to work on such a project, find a better job (just some friendly advice).
Another anti-pattern is the blob. It occurs when the problem was not correctly decomposed. One class is doing most of the work, while others represent just small fragments that don't do anything significant. In Delphi, we can find this anti-pattern in badly organized projects where most of the functionality is implemented in the main form of class. Such a project can usually be saved by applying SOLID design principles, which I'll discuss in the next section.
The golden hammer anti-pattern happens when a programmer uses one technology to solve everything. I've seen projects where every data structure was a
TStringList and others where all data structures were implemented with in-memory datasets. The best way of rescuing such projects is to put them in the hands of programmers with a wide knowledge of programming, data structures, and available tools.
Everyone who programmed in old-school BASIC has an intimate knowledge of a spaghetti code pattern: the code jumps here and there, doesn't follow any logic, and definitely does not use standard flow-control constructs, such as
while. It is usually too difficult to decode and fix such an implementation. A better approach is to write a decent set of unit tests and then rewrite the problematic part from scratch.
The last anti-pattern I want to mention is called copy and pasteprogramming. A typical sign of this anti-pattern is longer sequences of code that are frequently repeated in the source. There are almost no shared methods; everything is copied all over the place. This is a direct violation of the DRY design principle, which will be described in the next section and can be more-or-less simply solved by applying the same principle.
Patterns are not the only way of formalizing metaprogramming concepts. Patterns address specific problems, but sometimes we would like to express ideas that are not problem specific. Such formalizations are called principles. If they are related to program design, we call them, quite obviously, design principles.
Principles provide a view of a problem that is complementary to patterns. They don't give specific instructions on how to solve problems but rather instruct us how to write good code. A good programmer should, therefore, know both design patterns and design principles by heart. An excellent programmer, of course, also knows when to use patterns and principles and when to ignore them, but that's another story. You can only get such a level of knowledge by practicing programming for a long time.
Still, everyone has to start somewhere, and at the beginning, it is advantageous to know well-known and commonly appreciated principles. I'll finish this chapter with a short review of the most important design principles.
The most important principle of OOP is undoubtedly SOLID. It covers five ideas that are a subset of many principles promoted by software engineer and author Robert C Martin (you may know him as Dr. Bob). SOLID is an acronym in which each letter represents one of the following principles:
- Single responsibility principle: This principle states that a class should only have one responsibility. It goes hand-in-hand with software decomposition. If we cannot nicely decompose software implementation into components, it will also be hard to implement classes with only one responsibility. Taking care of the single responsibility principle helps prevent the blob anti-pattern.
- Open/closed principle: This principle states that software entities should be open for extensions but closed for modification. In other words, a module (class) should be extensible (open for extensions) without having to modify its source code (closed for modification). This extensibility is usually achieved either with the careful use of object-oriented principles (inheritance, virtual functions) or delegation (for example, providing a custom comparison function to a sorting method).
- Liskov substitution principle: Introduced in 1987 by Barbara Liskov, this tells us that the program should continue to work correctly if we substitute one object with its sub-type (a derived class, for example). This principle requires that a derived class cannot have more strict requirements than its base class (the preconditions cannot be stronger in a derived class) and that it cannot give weaker guarantees about output data (post-conditions cannot be weaker).
- Interface segregation principle: This principle merely states that multiple interfaces with a specific functionality are better than one general-purpose interface. An interface segregation principle makes the system more decoupled and easier to refactor.
- Dependency inversion principle: The last SOLID principle states that you should depend on abstraction and not on the concrete implementation. When you use a class, you should only depend on the interface that is exposed and not on the specific implementation. This principle is frequently used to refer to programming for an interface, not for an implementation.
We could jokingly say that the SOLID principle doesn't respect its own rules. By the interface segregation principle, we would expect to read about five different principles and not one larger set. Other design principles in this chapter are simpler and cover only one idea.
The don't repeat yourself principle (DRY), states that every piece of knowledge should have a single representation within a system. In other words, you should implement each small part of functionality as one method, and not use copy and paste.
In practice, such detailed decomposition will also cause problems. We will usually use a function from multiple places in the code. If that shared function changes behavior, we have to explicitly check all call sites (all places that use it) to see whether the new functionality is indeed required and desired at that place. Still, this is much better than the mess introduced by copy and paste programming.
The KISS principle came into programming from the US Navy. It states that systems are more reliable when they are kept simple. Although many sources say that KISS stands for keep it simple, stupid, the original author described it as keep it simple stupid. In other words, it is not that the engineer is stupid, but that the implementation should be simple stupid, or as trivial as possible.
This principle goes hand in hand with another idea taken from the extreme programming world—YAGNI. Meaning you ain't gonna need it, this acronym teaches us to only implement parts of code that are actually needed. If you try to foresee what will be needed in the future and write it in advance, you will, in many cases, just lose time, as either you'll be mistaken or the software specification will change.
Both KISS and YAGNI require frequent refactoring when software specification is updated, so it is helpful if you know the refactoring tools in Delphi or use a software add-on, such as MMX Code Explorer.
This chapter provided a brief overview of a topic that will be discussed in the rest of the book: design patterns. We took a look at the broader picture and found that patterns are everywhere and that design patterns are only part of a larger group of programming patterns. We also learned about architectural patterns, which function on a higher level, and idioms, which are very low-level patterns. (We will talk more about architectural patterns in Chapter 11, Other Kinds of Patterns. Delphi idioms are introduced throughout this book, starting with this chapter.)
We then learned about at the history of patterns, and were introduced to Gang of Four and their Design Patterns book. We learned that patterns are not fixed in time, but are evolving, and that many patterns, especially ones dealing with parallel programming, were documented after that book was published.
After this, I gave an overview of design pattern classification, where we saw how patterns can be split into four big groups, and we learned what the most important patterns in each group are. This will help you research patterns that are not covered in this book.
The chapter ended with a short section on design principles, which represent more generic ideas than patterns. Design principles represent a foundation of a programmer's knowledge, and it is recommended that you know them, even before you start studying design patterns.
In the next chapter, we'll start discovering design patterns. I'll start with the first group (creational patterns) and give detailed examples of four design patterns: singleton, dependency injection, lazy initialization, and object pool.