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.
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 for..in construct.
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 False and Value contains the value).
If we have a variable of the data: TSparseArray type, 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 (for and 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: HasValue and Value.
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 for/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 ...