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.