Contents
Introduction
How Delphi Helps You Define
Patterns
Delphi Examples of Design
Patterns
Pattern: Singleton
Pattern: Adapter
Pattern: Template
Method
Pattern: Builder
Pattern: Abstract
Factory
Pattern: Factory
Method
Appendix: Key Elements of
Delphi Class Definitions
Bibliography
Download
Paper and Source Code Examples
Introduction
Design patterns are frequently recurring
structures and relationships in object-oriented design. Getting to know them can
help you design better, more reusable code and also help you learn to design
more complex systems.
Much of the ground-breaking work on design
patterns was presented in the book Design Patterns: Elements of Reusable
Object-Oriented Software by Gamma, Helm, Johnson and Vlissides. You might
also have heard of the authors referred to as "the Gang of Four". If
you haven't read this book before and you're designing objects, it's an
excellent primer to help structure your design. To get the most out of these
examples, I recommend reading the book as well.
This paper takes some sample patterns from Design
Patterns and discusses their implementation in Delphi.
Another good source of pattern concepts is the
book Object Models: Strategies, Patterns and Applications by Peter Coad.
Coad's examples are more business oriented and he emphasises learning strategies
to identify patterns in your own work.
How Delphi Helps You Define Patterns
Delphi implements a fully object-oriented
language with many practical refinements that simplify development.
A summary of Delphi class concepts can be
found in the following appendix.
The most important class attributes from a
pattern perspective are the basic inheritance of classes; virtual and abstract
methods; and use of protected and public scope. These give you the tools to
create patterns that can be reused and extended, and let you isolate varying
functionality from base attributes that are unchanging.
Delphi is a great example of an extensible
application, through its component architecture, IDE interfaces and tool
interfaces. These interfaces define many virtual and abstract constructors and
operations.
Delphi Examples of Design Patterns
I should note from the outset, there may be
alternative or better ways to implement these patterns and I welcome your
suggestions on ways to improve the design. The following patterns from the book Design
Patterns are discussed and illustrated in Delphi to give you a starting
point for implementing your own Delphi patterns.
Pattern Name |
Definition |
Singleton |
"Ensure a class has only one
instance, and provide a global point of access to it." |
Adapter |
"Convert the interface of a
class into another interface clients expect. Adapter lets classes work
together that couldn't otherwise because of incompatible
interfaces." |
Template Method |
"Define the skeleton of an
algorithm in an operation, deferring some steps to subclasses. Template
Method lets subclasses redefine certain steps of an algorithm without
changing the algorithm's structure." |
Builder |
"Separate the construction of a
complex object from its representation so that the same construction
process can create different representations." |
Abstract Factory |
"Provide an interface for
creating families of related or dependant objects without specifying
their concrete classes." |
Factory Method |
"Define an interface for
creating an object, but let subclasses decide which class to
instantiate. Factory method lets a class defer instantiation to
subclasses." |
Note: These definitions are taken from Design
Patterns.
Pattern: Singleton
Definition
"Ensure a class has only one instance, and
provide a global point of access to it."
This is one of the easiest patterns to
implement.
Applications in Delphi
There are several examples of this sort of class
in the Delphi VCL, such as TApplication, TScreen or TClipboard. The pattern is
useful whenever you want a single global object in your application. Other uses
might include a global exception handler, application security, or a single
point of interface to another application.
Implementation Example
To implement a class of this type, override the
constructor and destructor of the class to refer to a global (interface)
variable of the class.
Abort the constructor if the variable is
assigned, otherwise create the instance and assign the variable.
In the destructor, clear the variable if it
refers to the instance being destroyed.
Note: To make the creation and destruction of
the single instance automatic, include its creation in the initialization
section of the unit. To destroy the instance, include its destruction in an
ExitProc (Delphi 1) or in the finalization section of the unit (Delphi 2).
The following Delphi 1 example illustrates two
singleton classes, one derived from TComponent and another derived from TObject.
unit Singletn;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs;
type
TCSingleton = class(TComponent)
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
TOSingleton = class(TObject)
public
constructor Create;
destructor Destroy; override;
end;
var
Global_CSingleton: TCSingleton;
Global_OSingleton: TOSingleton;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Design Patterns', [TCSingleton]);
end;
{ TCSingleton }
constructor TCSingleton.Create(AOwner: TComponent);
begin
if Global_CSingleton <> nil then
{NB could show a message or raise a different exception here}
Abort
else begin
inherited Create(AOwner);
Global_CSingleton := Self;
end;
end;
destructor TCSingleton.Destroy;
begin
if Global_CSingleton = Self then
Global_CSingleton := nil;
inherited Destroy;
end;
{ TOSingleton }
constructor TOSingleton.Create;
begin
if Global_OSingleton <> nil then
{NB could show a message or raise a different exception here}
Abort
else
Global_OSingleton := Self;
end;
destructor TOSingleton.Destroy;
begin
if Global_OSingleton = Self then
Global_OSingleton := nil;
inherited Destroy;
end;
procedure FreeGlobalObjects; far;
begin
if Global_CSingleton <> nil then
Global_CSingleton.Free;
if Global_OSingleton <> nil then
Global_OSingleton.Free;
end;
begin
AddExitProc(FreeGlobalObjects);
end.
Pattern: Adapter
Definition
"Convert the interface of a class into
another interface clients expect. Adapter lets classes work together that
couldn't otherwise because of incompatible interfaces."
Applications in Delphi
A typical example of this is the wrapper Delphi
generates when you import a VBX or OCX. Delphi generates a new class which
translates the interface of the external control into a Pascal compatible
interface. Another typical case is when you want to build a single interface to
old and new systems.
Note Delphi does not allow class adaption
through multiple inheritance in the way described in Design Patterns.
Instead, the adapter needs to refer to a specific instance of the old class.
Implementation Example
The following example is a simple (read only)
case of a new customer class, an adapter class and an old customer class. The
adapter illustrates handling the year 2000 problem, translating an old customer
record containing two digit years into a new date format. The client using this
wrapper only knows about the new customer class. Translation between classes is
handled by the use of virtual access methods for the properties. The old
customer class and adapter class are hidden in the implementation of the unit.
unit Adapter;
interface
uses SysUtils, Classes;
type
{ The new class }
TNewCustomer = class
private
FCustomerID: Longint;
FFirstName: string;
FLastName: string;
FDOB: TDateTime;
protected
function GetCustomerID: Longint; virtual;
function GetFirstName: string; virtual;
function GetLastName: string; virtual;
function GetDOB: TDateTime; virtual;
public
constructor Create(CustID: Longint); virtual;
property CustomerID: Longint read GetCustomerID;
property FirstName: string read GetFirstName;
property LastName: string read GetLastName;
property DOB: TDateTime read GetDOB;
end;
{ An interface method }
{ Lets us hide details of TOldCustomer from the client }
function GetCustomer(CustomerID: Longint): TNewCustomer;
implementation
const
Last_OldCustomer_At_Year_2000 = 15722;
Last_OldCustomer_In_Database = 30000;
{ The new class }
constructor TNewCustomer.Create(CustID: Longint);
begin
FCustomerID := CustID;
FFirstName := 'A';
FLastName := 'New_Customer';
FDOB := Now;
end;
function TNewCustomer.GetCustomerID: Longint;
begin
Result := FCustomerID;
end;
function TNewCustomer.GetFirstName: string;
begin
Result := FFirstName;
end;
function TNewCustomer.GetLastName: string;
begin
Result := FLastName;
end;
function TNewCustomer.GetDOB: TDateTime;
begin
Result := FDOB;
end;
type
{ The old class }
TOldDOB = record
Day: 0..31;
Month: 1..12;
Year: 0..99;
end;
TOldCustomer = class
FCustomerID: Integer;
FName: string;
FDOB: TOldDOB;
public
constructor Create(CustID: Integer);
property CustomerID: Integer read FCustomerID;
property Name: string read FName;
property DOB: TOldDOB read FDOB;
end;
constructor TOldCustomer.Create(CustID: Integer);
begin
FCustomerID := CustomerID;
FName := 'An Old_Customer';
with FDOB do begin
Day := 1;
Month := 1;
Year := 1;
end;
end;
type
{ The Adapter class }
TAdaptedCustomer = class(TNewCustomer)
private
FOldCustomer: TOldCustomer;
protected
function GetCustomerID: Longint; override;
function GetFirstName: string; override;
function GetLastName: string; override;
function GetDOB: TDateTime; override;
public
constructor Create(CustID: Longint); override;
destructor Destroy; override;
end;
{ The Adapter class }
constructor TAdaptedCustomer.Create(CustID: Longint);
begin
inherited Create(CustID);
FOldCustomer := TOldCustomer.Create(CustID);
end;
destructor TAdaptedCustomer.Destroy;
begin
FOldCustomer.Free;
inherited Destroy;
end;
function TAdaptedCustomer.GetCustomerID: Longint;
begin
Result := FOldCustomer.CustomerID;
end;
function TAdaptedCustomer.GetFirstName: string;
var
SpacePos: integer;
begin
SpacePos := Pos(' ', FOldCustomer.Name);
if SpacePos = 0 then
Result := ''
else
Result := Copy(FOldCustomer.Name,1,SpacePos-1);
end;
function TAdaptedCustomer.GetLastName: string;
var
SpacePos: integer;
begin
SpacePos := Pos(' ', FOldCustomer.Name);
if SpacePos = 0 then
Result := FOldCustomer.Name
else
Result := Copy(FOldCustomer.Name,SpacePos+1,255);
end;
function TAdaptedCustomer.GetDOB: TDateTime;
var
FullYear: Word;
begin
if CustomerID > Last_OldCustomer_At_Year_2000 then
FullYear := 2000 + FOldCustomer.DOB.Year
else
FullYear := 1900 + FOldCustomer.DOB.Year;
Result := EncodeDate(FullYear, FOldCustomer.DOB.Month, FOldCustomer.DOB.Day);
end;
function GetCustomer(CustomerID: Longint): TNewCustomer;
begin
if CustomerID > Last_OldCustomer_In_Database then
Result := TNewCustomer.Create(CustomerID)
else
Result := TAdaptedCustomer.Create(CustomerID) as TNewCustomer;
end;
end.
Pattern: Template Method
Definition
"Define the skeleton of an algorithm in an
operation, deferring some steps to subclasses. Template Method lets subclasses
redefine certain steps of an algorithm without changing the algorithm's
structure."
This pattern is essentially an extension of
abstract methods to more complex algorithms.
Applications in Delphi
Abstraction is implemented in Delphi by abstract
virtual methods. Abstract methods differ from virtual methods by the base class
not providing any implementation. The descendant class is completely responsible
for implementing an abstract method. Calling an abstract method that has not
been overridden will result in a runtime error.
A typical example of abstraction is the
TGraphic class.
TGraphic is an abstract class used to
implement TBitmap, TIcon and TMetafile. Other developers have frequently used
TGraphic as the basis for other graphics objects such as PCX, GIF, JPG
representations. TGraphic defines abstract methods such as Draw, LoadFromFile
and SaveToFile which are then overridden in the concrete classes. Other objects
that use TGraphic, such as a TCanvas only know about the abstract Draw method,
yet are used with the concrete class at runtime.
Many classes that use complex algorithms are
likely to benefit from abstraction using the template method approach. Typical
examples include data compression, encryption and advanced graphics processing.
Implementation Example
To implement template methods you need an
abstract class and concrete classes for each alternate implementation. Define a
public interface to an algorithm in an abstract base class. In that public
method, implement the steps of the algorithm in calls to protected abstract
methods of the class. In concrete classes derived from the base class, override
each step of the algorithm with a concrete implementation specific to that
class.
This example shows some very simple alogrithm
steps, but illustrates the principle of deferring implementation to a subclass.
unit Tpl_meth;
interface
type
TAbstractTemplateClass = class(TObject)
protected
function Algorithm_StepA: Integer; virtual; abstract;
function Algorithm_StepB: Integer; virtual; abstract;
function Algorithm_StepC: Integer; virtual; abstract;
public
function Algorithm: Integer;
end;
TConcreteClassA = class(TAbstractTemplateClass)
protected
function Algorithm_StepA: Integer; override;
function Algorithm_StepB: Integer; override;
function Algorithm_StepC: Integer; override;
end;
TConcreteClassB = class(TAbstractTemplateClass)
protected
function Algorithm_StepA: Integer; override;
function Algorithm_StepB: Integer; override;
function Algorithm_StepC: Integer; override;
end;
Pattern: Builder
Definition
"Separate the construction of a complex
object from its representation so that the same construction process can create
different representations."
A Builder seems similar in concept to the
Abstract Factory. The difference as I see it is the Builder refers to single
complex objects of different concrete classes but containing multiple parts,
whereas the abstract factory lets you create whole families of concrete classes.
For example, a builder might construct a house, cottage or office. You might
employ a different builder for a brick house or a timber house, though you would
give them both similar instructions about the size and shape of the house. On
the other hand the factory generates parts and not the whole. It might produce a
range of windows for buildings, or it might produce a quite different range of
windows for cars.
Applications in Delphi
The functionality used in Delphi's VCL to create
forms and components is similar in concept to the builder. Delphi creates forms
using a common interface, through Application.CreateForm and through the TForm
class constructor. TForm implements a common constructor using the resource
information (DFM file) to instantiate the components owned by the form. Many
descendant classes reuse this same construction process to create different
representations. Delphi also makes developer extensions easy. TForm's OnCreate
event also adds a hook into the builder process to make the functionality easy
to extend.
Implementation Example
The following example includes a class
TAbstractFormBuilder and two concrete classes TRedFormBuilder and
TBlueFormBuilder. For ease of development some common functionality of the
concrete classes has been moved into the shared TAbstractFormBuilder class.
type
TAbstractFormBuilder = class
private
FForm: TForm;
procedure BuilderFormClose(Sender: TObject; var Action: TCloseAction);
protected
function GetForm: TForm; virtual;
public
procedure CreateForm(AOwner: TComponent); virtual;
procedure CreateSpeedButton; virtual; abstract;
procedure CreateEdit; virtual; abstract;
procedure CreateLabel; virtual; abstract;
property Form: TForm read GetForm;
end;
type
TRedFormBuilder = class(TAbstractFormBuilder)
private
FNextLeft, FNextTop: Integer;
public
procedure CreateForm(AOwner: TComponent); override;
procedure CreateSpeedButton; override;
procedure CreateEdit; override;
procedure CreateLabel; override;
end;
type
TBlueFormBuilder = class(TAbstractFormBuilder)
private
FNextLeft, FNextTop: Integer;
public
procedure CreateForm(AOwner: TComponent); override;
procedure CreateSpeedButton; override;
procedure CreateEdit; override;
procedure CreateLabel; override;
end;
At runtime the client application instructs one
of the concrete classes to create parts using the public part creation
procedures. The concrete builder instance is passed to the folliwing procedure:
procedure TForm1.Create3ComponentFormUsingBuilder(ABuilder: TAbstractFormBuilder);
var
NewForm: TForm;
begin
with ABuilder do begin
CreateForm(Application);
CreateEdit;
CreateSpeedButton;
CreateLabel;
NewForm := Form;
if NewForm <> nil then NewForm.Show;
end;
end;
Pattern: Abstract Factory
Definition
"Provide an interface for creating families
of related or dependant objects without specifying their concrete classes."
The Factory Method pattern below is commonly
used in this pattern.
Applications in Delphi
This pattern is ideal where you want to isolate
your application from the implementation of the concrete classes. For example if
you wanted to overlay Delphi's VCL with a common VCL layer for both 16 and 32
bit applications, you might start with the abstract factory as a base.
Implementation Example
The following example uses an abstract factory
and two concrete factory classes to implement different styles of user interface
components. TOAbstractFactory is a singleton class, since we usually want one
factory to be used for the whole application.
TOAbstractFactory = class(TObject)
public
constructor Create;
destructor Destroy; override;
{ abstract widget constructors }
function CreateSpeedButton(AOwner: TComponent): TSpeedButton; virtual; abstract;
function CreateEdit(AOwner: TComponent): TEdit; virtual; abstract;
function CreateLabel(AOwner: TComponent): TLabel; virtual; abstract;
end;
TORedFactory and TOBlueFactory override the
abstract interface to support different widget styles.
TORedFactory = class(TOAbstractFactory)
public
{ concrete widget constructors }
function CreateSpeedButton(AOwner: TComponent): TSpeedButton; override;
function CreateEdit(AOwner: TComponent): TEdit; override;
function CreateLabel(AOwner: TComponent): TLabel; override;
end;
TOBlueFactory = class(TOAbstractFactory)
public
{ concrete widget constructors }
function CreateSpeedButton(AOwner: TComponent): TSpeedButton; override;
function CreateEdit(AOwner: TComponent): TEdit; override;
function CreateLabel(AOwner: TComponent): TLabel; override;
end;
At runtime, our client application instantiates
the abstract factory with a concrete class and then uses the abstract interface.
Parts of the client application that use the factory don't need to know which
concrete class is actually in use.
Pattern: Factory Method
Definition
"Define an interface for creating an object,
but let subclasses decide which class to instantiate. Factory method lets a
class defer instantiation to subclasses."
The Abstact Factory pattern can be viewed as a
collection of Factory Methods.
Applications in Delphi
This pattern is useful when you want to
encapsulate the construction of a class and isolate knowledge of the concrete
class from the client application through an abstract interface.
One example of this might arise if you had an
object oriented business application potentially interfacing to multiple target
DBMS. The client application only wants to know about the business classes, not
about their implementation-specific storage and retrieval.
Implementation Example
In the Abstract Factory example, each of the
virtual widget constructor functions is a Factory Method. In their
implementation we define a specific widget class to return.
TRedSpeedButton = class(TSpeedButton)
public
constructor Create(AOwner: TComponent); override;
end;
constructor TRedSpeedButton.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Font.Color := clRed;
end;
function TORedFactory.CreateSpeedButton(AOwner: TComponent): TSpeedButton;
begin
Result := TRedSpeedButton.Create(AOwner);
end;
Appendix: Key Elements of Delphi Class
Definitions
Unit Structure
Delphi units (.PAS files) allow declaration of
interface and implementation sections. The interface defines the part that is
visible to other units using that unit. The keyword uses can be added to
a unit's interface or implementation section to list the other units that your
unit uses. This indicates to the compiler that your unit refers to parts of the
used unit's interface. Parts of a unit declared in the implementation section
are all private to that unit, i.e. never visible to any other unit. Types,
functions and procedures declared in the interface of a unit must have a
corresponding implementation, or be declared as external (e.g. a call to a
function in a DLL).
Class Interfaces
Classes are defined as types in Delphi and may
contain fields of standard data types or other objects, methods declared as
functions or procedures, and properties. The type declaration of a class defines
its interface and the scope of access to fields, methods and properties of the
class. Class interfaces are usually defined in the interface of a unit to make
them accessible to other modules using that unit. However they don't need to be.
Sometimes a type declaration of a class may be used only within the
implementation part of a unit.
Properties
Properties are a specialised interface to a field
of a defined type, allowing access control through read and write methods.
Properties are not virtual, you can replace a property with another property of
the same name, but the parent class doesn't know about the new property. It is
however possible to make the access methods of a property virtual.
Inheritance
Delphi's inheritance model is based on a single
hierarchy. Every class inherits from TObject and can have only one parent.
A descendant class inherits all of the
interface and functionality of its parent class, subject to the scope described
below.
Multiple inheritance from more than one parent
is not allowed directly. It can be implemented by using a container class to
create instances one or more other classes and selectively expose parts of the
contained classes.
Private, Protected, Public and Published
Scope
Scope refers to the visibility of methods and
data defined in the interface of a class, i.e. what parts of the class are
accessible to the rest of the application or to descendant classes.
The default scope is public, for instance the
component instances you add to a form at design time. Public says "come and
get me"; it makes the data or method visible to everything at runtime.
Published parts of a class are a specialized
form of Public scope. They indicate special behaviour for classes derived from
TPersistent. A persistent class can save and restore its published properties to
persistent storage using Delphi's standard streaming methods. Published
properties also interact with Delphi Object Inspector in the IDE. A class must
descend from TPersistent in order to use Published. There's also not much point
in publishing methods, since you can't store them, although Delphi's compiler
doesn't stop you. Published also lets another application access details of the
class through Delphi's runtime type information. This would be rarely used,
except in Delphi's design time interaction with its VCL.
Encapsulation or information hiding is
essential to object orientation, so Protected and Private scope let you narrow
the access to parts of a class.
Protected parts are visible only to descendant
classes, or to other classes defined in the same unit.
Private parts are visible only to the defining
class, or to other classes defined in the same unit.
It's important to note that once something is
given public or published scope, it cannot be hidden in descendant classes.
Static, Virtual and Dynamic Methods; Override
and Inherited
Methods declared as virtual or dynamic let you
change their behaviour using override in a descendant class. You're unlikely to
see a virtual method in the private part of a class, since it could only be
overridden in the same unit, although Delphi's compiler doesn't stop you from
doing this.
Override indicates that your new method
replaces the method of the same name from the parent class. The override must be
declared with the same name and parameters as the original method.
When a method is overridden, a call to the
parent class's method actually executes the override method in the real class of
the object.
Static methods on the other hand have no
virtual or override declaration. You can replace a method of a class in a
descendant class by redeclaring another method, however this is not object
oriented. If you reference your descendant class as the parent type and try to
call the replaced method, the static method of the parent class is executed. So
in most cases, it's a bad idea to replace a static method.
Virtual and dynamic methods can be used
interchangeably. They differ only in their treatment by the compiler and runtime
library. Delphi's help explains that dynamic methods have their implementation
resolved at compile time and run slightly faster, whereas virtual methods are
resolved at runtime, resulting in slightly slower access but a smaller compiled
program. Virtual is usually the preferred declaration. Delphi's help suggests
using dynamic when you have a base class with many descendants that may not
override the method.
The inherited directive lets you refer back to
a property or method as it was declared in the parent class. This is most often
used in the implementation of an override method, to call the inherited method
of the parent class and then supplement its behaviour.
Abstract Methods
Abstract is used in base classes to declare a
method in the interface and defer its implementation to a descendant class. I.e.
it defines an interface, but not the underlying operation. Abstract must be used
with the virtual or dynamic directive. Abstract methods are never implemented in
the base class and must be implemented in descendant classes to be used. A
runtime error occurs if you try to execute an abstract method that is not
overridden. Calling inherited within the override implementation of an abstract
method will also result in a runtime error, since there is no inherited
behaviour.
Messages
Delphi's handling of Windows messages is a
special case of virtual methods. Message handlers are implemented in classes
that descend from TControl. I.e classes that have a handle and can receive
messages. Message handlers are always virtual and can be declared in the private
part of a class interface, yet still allow the inherited method to be called.
Inherited in a message handler just uses the keyword inherited, there is no need
to supply the name of the method to call.
Events
Events are also an important characteristic of
Delphi, since they let you delegate extensible behaviour to instances of a
class. Events are properties that refer to a method of another object. Events
are not inherited in Delphi 1; Delphi 2 extends this behaviour to let you use
inherited in an event. . Inherited in an event handler just uses the keyword
inherited, there is no need to supply the name of the method to call.
Events are particularly important to component
developers, since they provide a hook for the user of the component to modify
its behaviour in a way that may not be foreseen at the time the component is
written.
Constructors and Destructors
The constructor and destructor are two special
types of methods. The constructor initializes a class instance (allocates memory
initialized to 0) and returns a reference (pointer) to the object. The
destructor deallocates memory used by the object (but not the memory of other
objects created by the object).
Classes descended from TObject have a static
constructor, Create, and a virtual destructor Destroy.
TComponent introduces a new public property,
the Owner of the component and this must be initialized in the constructor.
TComponent's constructor is declared virtual, i.e. it can be overridden in
descendant classes.
It is essential when you override a virtual
constructor or destructor in a TComponent descendant to include a call to the
inherited method.
Bibliography
- Design Patterns: Elements of Reusable
Object-Oriented Software, Erich Gamma et al., Addison-Wesley, Massachusetts,
1995.
- Object Models: Strategies, Patterns and
Applications, Peter Coad et al., Prentice-Hall, New Jersey, 1995.
Copyright © 1996, Objective Software
Technology Pty Limited. This paper may be freely distributed in its entirety,
providing the source is acknowledged.
Portions quoted from Design Patterns are
Copyright © 1995, Addison Wesley Publishing Company. |