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 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 System.SysUtils
).
Note
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 TSparseArrayHelper
).
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.
Note
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 ObjectLifecycle
.
For simplicity's sake, we'll be using two objects: obj1
and 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 finally
section.
Note
Is it better to use obj1.Free
or 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 obj2
.
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.
For the same reason, the following simplified version also doesn't work:
try obj1 := TObject.Create; // some code finally FreeAndNil(obj1); end;
If an 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.