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

Gang of Four started it all


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 former rule corresponds to the dependency inversion principle (the D part of the SOLID principle, which I'll cover in more detail later in this chapter).

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.

Don't inherit – compose!

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 Insert, Move, 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 Items property.

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 Delete and 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[1] := 42 statement:

var
  list: TInheritedLimitedList;

list.Add(1);
list.Add(2);
list.Add(3);
list[1] := 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 1, 42, and 3:

procedure ChangeList(list: TList);
begin
  list[1] := pointer(42);
end;

var
 list: TInheritedLimitedList;

list.Add(1);
list.Add(2);
list.Add(3);
ChangeList(list);

This happens because Get and 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 list[1] in ChangeList therefore uses TList.Put, which doesn't raise an exception.

Note

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;

By using composition instead of inheritance, we get all the benefits fast code, small and testable implementation, and error checking in the compiler.