Book Image

Hands-On Design Patterns with Delphi

By : Primož Gabrijelčič
Book Image

Hands-On Design Patterns with Delphi

By: Primož Gabrijelčič

Overview of this book

Design patterns have proven to be the go-to solution for many common programming scenarios. This book focuses on design patterns applied to the Delphi language. The book will provide you with insights into the language and its capabilities of a runtime library. You'll start by exploring a variety of design patterns and understanding them through real-world examples. This will entail a short explanation of the concept of design patterns and the original set of the 'Gang of Four' patterns, which will help you in structuring your designs efficiently. Next, you'll cover the most important 'anti-patterns' (essentially bad software development practices) to aid you in steering clear of problems during programming. You'll then learn about the eight most important patterns for each creational, structural, and behavioral type. After this, you'll be introduced to the concept of 'concurrency' patterns, which are design patterns specifically related to multithreading and parallel computation. These will enable you to develop and improve an interface between items and harmonize shared memories within threads. Toward the concluding chapters, you'll explore design patterns specific to program design and other categories of patterns that do not fall under the 'design' umbrella. By the end of this book, you'll be able to address common design problems encountered while developing applications and feel confident while building scalable projects.
Table of Contents (18 chapters)
Title Page
Copyright and Credits
About Packt
Contributors
Preface
Index

Patterns are useful


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: 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 (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).

 

Delphi idioms – Creating and destroying an object

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.