Sei sulla pagina 1di 47

Chapter 12 Bidirectional

links, Callbacks
and linking classes

Main concepts

– Bidirectional links between objects and the problem of circular referencing.


– Flexible bidirectional linking with the SenderArgument Pattern.
– The importance of inheritance when linking between hierarchies.
– The use of callbacks and access methods.
– A typical application of callbacks.
– An association class.

Chapter contents

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

Requesting additional information . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

The Sender parameter and Callbacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 1
Bidirectional linking: the problem of a circular reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

Identifying one object to another . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

Example 12.1 Bidirectional linking and the Sender parameter . . . . . . . . . . . . . . . . . . . . . . . . 10


Ex 12.1 step 1 Disclosing an object’s identity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Ex 12.1 step 2 Circular referencing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
Ex 12.1 step 3 Downcasting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

Example 12.2 Callback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14


Ex 12.2 step 1 Providing a data access method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
Ex 12.2 step 2 Using the callback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

Pattern 12.1 The SenderArgument Pattern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

Link through the root . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

Example 12.3 Linking through superclasses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19


Ex 12.3 step 1 Linking to subtypes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Ex 12.3 step 2 Defining the new recipient subtypes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21

Example 12.4 Inheriting a link . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22


Ex 12.4 step 1 User interface subclasses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

Example 12.5 Providing additional data to a role . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25


Ex 12.5 step 1 Adapting TCategory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Ex 12.5 step 2 Modifying TEmployee . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Ex 12.5 step 3 The base form for the user interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Ex 12.5 step 4 The new user interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

Callback methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

Example 12.6 Two way communication between classes in the same unit . . . . . . . . . . . . . . 34

Example 12.7 A linking class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

Chapter 12, Page 2 Object orientation with Delphi (all rights reserved)
Chapter Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Problem 12.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Problem 12.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Problem 12.3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Problem 12.4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46

Introduction

Nearly all the communication between objects covered so far has been unidirectional, from
an initiator to a recipient. This leaves the recipient with no possibility of using its own
initiative to solicit any information it may need from the initiator, a problem we address in
this chapter.

Requesting additional information

The Role–Player pattern in chapter 11 requires that the different roles all implement the
same interface that is set by the abstract base class. For example, we can plug in any other
employment category that we want to in example 11.4 provided that it has the method
GetPackage with a single ANotch parameter (example 11.4, step 3, lines 8–9).

Figure 1 Adding a category for hourly paid employees

But what happens if our new category represents hourly paid employees (figure 1)? Hourly
paid employees clock in and out and work a variable number of hours each month. So we
need both their notch and the number of hours they have worked in order to calculate their
earnings. TCategory’s existing interface, the GetPackage method, transfers only the notch
and not the number of hours. We could change the GetPackage method signature to include
Hours. But the TCategory hierarchy must have pure subtyping for the program to work,

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 3
and so if we make any changes we must make them in the (abstract) base class TCategory,
and all its descendants must then implement this change. TTrainee, TWeekly and TMonthly
are not interested in the Hours and so redesigning the whole hierarchy to accept an
additional Hours parameter for the sake of only one subclass is clumsy and reduces the
overall cohesion.
It seems then that the user interface can’t (elegantly) tell THourly what the number of
hours is, so we must find a way for THourly to request this information. This is a move
away from a push communication strategy to a pull strategy where the recipient object can
take responsibility for its information needs by requesting the data it requires.

Asking the user

A very simple solution is for THourly to display an InputBox asking the user to enter the
number of hours (figure 2).

Figure 2 Class THourly can


request the user to enter the
number of hours worked

function THourly.GetPackage(ANotch: integer): integer;


var Hours: integer;
begin
Hours := StrToInt (InputBox ('Hourly employees',
'Please enter the number of hours worked', ''));
Result := Hours * (40 + ANotch*15);
end; // end function THourly.GetPackage

This simplicity is appealing, but it has a number of problems. It combines different tiers,
since a business logic class (THourly) now interacts directly with the user interface. If we
ever need to change this program to work with a database, we’re stuck. Also, it may be
preferable to enter the hours directly from the user interface (figure 3).

Chapter 12, Page 4 Object orientation with Delphi (all rights reserved)
Figure 3 The user interface revised to add hourly employees

Reading the value directly from the user interface

As an alternative to the InputBox, THourly could access the SpinEdit on the user interface
directly for the value of the number of hours (figure 4):

function THourly.GetPackage(ANotch: integer): integer;


begin
Result := frmPayment.sedHours.Value * (40 + ANotch*15);
end; // end function THourly.GetPackage

This works, but it also has problems since it increases the overall level of coupling
significantly and breaks the ‘neighbours only’ principle proposed by the Law of Demeter. In
figure 1, TfrmPayment and the TContract hierarchy have no knowledge of each other’s
existence. (One reason for using the Strategy pattern was to reduce the coupling.) Figure 4
has a hard-coded link to a specific input component, sedHours, on a specific form,
frmPayment. Should we in the future need to connect to a database, or simply change the
user interface or use a different input component, THourly will also have to change.

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 5
Figure 4 Link, between THourly and TfrmPayment, contravening the Law of Demeter

The Sender parameter and Callbacks

We can establish a communication channel between THourly and TfrmPayment that keeps
to the ‘neighbours only’ rule by setting up a series of bidirectional links. TfrmPayment will
identify itself to TEmployee. TEmployee in turn will identify itself to TCategory. (This
makes the linkage dynamically configurable rather than hard coded, and simplifies reuse.)
When THourly requires additional data, it will request this data from TEmployee.
TEmployee will delegate this request to TfrmPayment and then supply the resulting data to
THourly.
A sequence diagram illustrates how a CategoryObj (of type THourly) issues a callback to
Employee when Employee sends it a GetPackage message (figure 5). Employee in turn
issues a callback to frmCallBackDemo which supplies the values for the hours worked.
Employee sends this value on to the THourly object which then supplies Employee with the
value for the package. Employee sends this value to frmCallBackDemo. (We have changed
the form’s name for reasons that will become clear.)

Chapter 12, Page 6 Object orientation with Delphi (all rights reserved)
Figure 5 A sequence of messages illustrating a CallBack message

A revised class diagram shows the bidirectional linkages between TfrmBase and
TEmployee, and between TEmployee and TCategory that make it possible for THourly to
access a value from TfrmCallBackDemo via TEmployee (figure 6).

Figure 6 M odifications to allow callbacks

When we compare figure 6 with the previous diagrams (chapter 11, figures 12, 19 (excluding
the TContract and TTax hierarchies) and chapter 12, figure 1), we see a number of changes.
First, the user interface class is now derived from an abstract class TfrmBase. (We have
changed the interface class’s name to TfrmCallBackDemo for this version.) Through the
abstract class TfrmBase we lay a communication framework: we declare a link to a
TEmployee and an abstract method GetHours. If needed in future, we can derive several
different user interface objects from TFrmBase that all use the communication structure it
declares, so reducing the coupling between TEmployee and a specific user interface. The

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 7
GetHours method provides a means for a TEmployee to read the values entered into the
SpinEdit on TfrmCallBackDemo (figures 3 & 5). (This will become clearer in a little while.)
Previously there was a unidirectional link from the user interface object to TEmployee.
There is now a return link, and TEmployee has a new, private variable, FCaller, as the
reference for the return navigation to the user interface. TEmployee gains a GetHours access
method (which delegates the operation to the user interface’s GetHours), and THourly will
use this as a callback method.
TCategory also changes. It now carries a reference back to TEmployee, and this is the
path THourly uses for the callback.
Because of space restrictions, we don’t show the full method signatures in figure 6, but
there are also change to allow the calling object to identify itself and so to establish
bidirectional links.
There are several aspects to linking (associating) different classes and we’ll look at these
first before writing the code for this version of the program in example 12.5.

Bidirectional linking: the problem of a circular


reference

When one class refers to another class, it must refer to the other unit in its uses clause. So
when we have classes in different units referring to each other, each unit must list the other
unit in its global uses clause. Doing this, however, sets up a circular reference that the
Delphi compiler rejects. To see the problem, assume that we have the units Unit1 and Unit2.

1 unit Unit1;

2 interface

3 uses Unit2;

4 type
5 TBase1 = class(TObject)
6 protected
7 Base2: TBase2;
8 public
9 procedure Method1;
10 // etc ...
11 end; // end TBase1 = class(TObject)

12 // etc ...

Chapter 12, Page 8 Object orientation with Delphi (all rights reserved)
Unit1 declares class TBase1 (line 5 above) and carries a reference to an object of class TBase2
(line 7). Because TBase2 is defined in Unit2, Unit1 must include Unit2 in its global uses
clause (line 3). This is fine, and is the standard way to set up a unidirectional link.
To set up the reverse link, we try to do the same in declaring Unit2.

1 unit Unit2;

2 interface

3 uses Unit1;

4 type
5 TBase2 = class(TObject)
6 protected
7 Base1: TBase1;
8 public
9 procedure Method2;
10 // etc ...
11 end; // end TBase1 = class(TObject)

Here we want to declare TBase2 (line 5) with a reference to a TBase1 (line 7) and so we
include Unit1 in the uses clause (line 3). This is where the trouble begins! Unit1 already uses
Unit2 and now Unit2 wants to use Unit1. We have a circular reference and the compiler
flags an error.
The uses clause in the interface section is global while the uses clause in the
implementation section is private, so we can break this circular reference by moving the
uses Unit1 clause from Unit2’s interface section to its implementation section. This has a
consequence, though. We can no longer have a reference to a TBase1 as part of TBase2
because, once the uses Unit1 clause is moved into the implementation section, TBase1 (line 7
above) becomes an unknown identifier. So we must declare Base1 to be of type TObject (line
6 below), and then, whenever we use the Base1 reference, we must cast it to a TBase2 (line 15
below).

1 unit Unit2;

2 interface

3 type
4 TBase2 = class(TObject)
5 protected
6 Base1: TObject;
7 public
8 procedure Method2;
9 // etc ...
10 end; // end TBase1 = class(TObject)

11 implementation

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 9
12 uses Unit1;

13 procedure Method2;
14 begin
15 ... (Base1 as TBase1).Method1 ...
16 end;

When TBase1 wants to use Method2 in TBase2 it uses the call Base2.Method2.
When TBase2 wants to use Method1 in TBase1 it must do the necessary typecasting
(downcasting) and so uses either of the calls TBase1(Base1).Method2 or
(Base1 as TBase1).Method2 (line 15 above). We’ll see this principle applied in a little
while, as soon as we have briefly discussed a related issue, that of one object identifying
itself to another object.

Identifying one object to another

To set up the link for a callback, the calling object (the initiator) must identify itself to the
called object (the recipient). For Delphi programmers this is not a new concept. With every
event, the object initiating the event identifies itself to the event handler through the
(Sender: TObject) parameter. We will once again take a tip from Delphi’s RAD code
generator and extend the Sender concept to classes we ourselves write.
With events, Sender is always transferred as a TObject to allow any type of object to act
as the initiator, and so the recipient uses Sender by typecasting it as necessary. We will do
the same in our use of Sender.
The approach of using a Sender for identification has been formulated as the
SenderArgument pattern, which we state after the following example.

Example 12.1 Bidirectional linking and the Sender


parameter

To introduce the coding for the callback in the Player–Role pattern, we’ll start with a simple
example where a visual object, frmBaseInitiator, initiates communication with a non-visual
object, RecBase, which acts as the recipient of the communication (figure 7).

Figure 7 The two way link

The visual object that initiates the link is in figures 8 & 9.

Chapter 12, Page 10 Object orientation with Delphi (all rights reserved)
Figure 8 Link initiator object

Figure 9 Objects comprising


the user interface / link initiator

Figure 10 shows the response by the non-visual object, a TBaseRecipient, to confirm the
communication by showing the identity of the initiator. (Check the messages in figure 10
against the classes in figure 7.)

Figure 10 Recipient confirming


the link

Ex 12.1 step 1 Disclosing an object’s identity

The code for the visual class TfrmBaseInitiator (figure 8) that initiates the link is as follows:

1 unit InitiatorU;

2 interface

3 uses
4 Windows, Messages, SysUtils, Variants, Classes, Graphics,
5 Controls, Forms, Dialogs, StdCtrls,
6 RecipientU;

7 type
8 TfrmBaseInitiator = class(TForm)

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 11
9 radCallBase: TRadioButton;
10 procedure FormCreate(Sender: TObject);
11 procedure FormDestroy(Sender: TObject);
12 procedure radCallBaseClick(Sender: TObject);
13 protected
14 RecBase: TBaseRecipient; // reference to recipient
15 end; // end TfrmBaseInitiator = class(TForm)

16 var
17 frmBaseInitiator: TfrmBaseInitiator;

18 implementation

19 {$R *.dfm}

20 procedure TfrmBaseInitiator.FormCreate(Sender: TObject);


21 begin
22 RecBase := TBaseRecipient.Create;
23 end; // end procedure TfrmBaseInitiator.FormCreate

24 procedure TfrmBaseInitiator.FormDestroy(Sender: TObject);


25 begin
26 RecBase.Free;
27 end; // end procedure TfrmBaseInitiator.FormDestroy

28 procedure TfrmBaseInitiator.radCallBaseClick(Sender: TObject);


29 begin
30 RecBase.MakeContact(Self); // identify self to recipient
31 end; // end procedure TfrmBaseInitiator.radCallBaseClick

32 end. // end unit InitiatorU

TfrmBaseInitiator communicates with an object RecBase of type TBaseRecipient, and so it


declares this reference as part of its type declaration (lines 13–14). RecBase has protected
visibility so any descendants of TfrmBaseInitiator can also use this link. TBaseRecipient is
defined in unit RecipientU, which is therefore included in the uses class (line 6). As part of
its own creation and destruction TfrmBaseInitiator creates and frees RecBase (lines 20–27).
The return link from RecBase is established in radCallBase’s event handler (lines 28–31)
through RecBase’s MakeContact method (figure 7). This requires a parameter identifying
the link initiator, and frmBaseInitiator identifies itself through the Self keyword (line 30).

Ex 12.1 step 2 Circular referencing

We must now define the second class in figure 7. Type in the code below as it stands, and,
when you compile it, notice the error message caused by line 3.

Chapter 12, Page 12 Object orientation with Delphi (all rights reserved)
1 unit RecipientU;

2 interface

3 uses InitiatorU; // error - circular reference

4 type
5 TBaseRecipient = class(TObject)
6 public
7 procedure MakeContact (Sender: TfrmBaseInitiator); virtual;
8 end; // end TBaseRecipient = class(TObject)

9 implementation

10 uses Dialogs; // for ShowMessage below

11 { TBaseRecipient }

12 procedure TBaseRecipient.MakeContact(Sender: TObject);


13 var
14 MsgStr: string;
15 begin
16 MsgStr := 'Hello ' + Sender.ClassName;
17 MsgStr := MsgStr + #13#10 + 'I am ' + ClassName;
18 ShowMessage (MsgStr);
19 end; // end procedure TBaseRecipient.MakeContact

20 end. // end unit RecipientU

This is the ‘obvious’ way to code TBaseRecipient. We declare method MakeContact to accept
a Sender of type TfrmBaseInitiator (line 7), and so we include InitiatorU in the global uses
clause (line 3). We then use the Sender parameter in line 16.
Unit InitiatorU already uses unit RecipientU globally (step 1, line 3) and now unit
RecipientU attempts to use InitiatorU unit globally (step 2, line 3). So, as expected, this
causes a circular reference error.

Ex 12.1 step 3 Downcasting

To solve the problem that arose in step 2, we need an alternative approach which does not
require unit InitiatorU to appear in unit RecipientU’s global uses clause. Delphi’s event
handler structure gives us a clue. Sender is the first parameter in every event and it is
always of type TObject. (We considered this at length in chapter 5.) If we declare Sender of
type TObject (line 6 below), we can remove the global uses clause. However, now when we
use Sender we must cast it as a TfrmBaseInitiator (line 15 below) and so InitiatorU must
appear in the local (ie private) uses clause (line 9).
Modify the code of step 2 appropriately and run the program.

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 13
1 unit RecipientU;

2 interface

3 type
4 TBaseRecipient = class(TObject)
5 public
6 procedure MakeContact (Sender: TObject); virtual;
7 end; // end TBaseRecipient = class(TObject)

8 implementation

9 uses Dialogs, InitiatorU;

10 { TBaseRecipient }

11 procedure TBaseRecipient.MakeContact(Sender: TObject);


12 var
13 MsgStr: string;
14 begin
15 MsgStr := 'Hello ' + (Sender as TfrmBaseInitiator).ClassName;
16 MsgStr := MsgStr + #13#10 + 'I am ' + ClassName;
17 ShowMessage (MsgStr);
18 end; // end procedure TBaseRecipient.MakeContact

19 end. // end unit RecipientU

In line 6 we make the MakeContact method virtual so that any subclasses of TBaseRecipient
can override it if necessary, though we don’t do that here.
In the MakeContact method we twice use the ClassName method that is defined in
TObject. The first time it gives the initiator’s class name (line 15) and the second time the
recipient’s class name (line 16).

Summary of example 12.1: In this example we establish a bidirectional link between two
classes in different units through the Sender parameter: the initiator calls a method of the
recipient and uses the Self identifier to identify itself to the recipient. To prevent circular
referencing, the recipient declares the Sender parameter as type TObject and then
downcasts it appropriately whenever it uses Sender.

Example 12.2 Callback

We now know how the recipient class can refer to the initiator class. If the initiator class has
any public methods, data or properties, the recipient can access these through the Sender
parameter. To illustrate this, we’ll give the initiator class a public GetCaption data access
method which the recipient will call in order to display the initiating form’s Caption (figure
11, and compare the messages to figure 10).

Chapter 12, Page 14 Object orientation with Delphi (all rights reserved)
Figure 11 Recipient using the
value of the Sender parameter
to initiate a callback

Ex 12.2 step 1 Providing a data access method

A callback, such as we are creating in this example, can use any public data access methods
available in the caller (initiator), so here we create a typical data access method called
GetCaption (lines 15–16, 34–37 below).

1 unit InitiatorU;

2 interface
3 uses
4 { uses clause as example 12.1 step 1 }
7 type
8 TfrmBaseInitiator = class(TForm)
9 { RAD declarations as example 12.1 step 1 }
13 protected
14 RecBase: TBaseRecipient; // reference to recipient
15 public
16 function GetCaption: string; // access method (for callback)
17 end; // end TfrmBaseInitiator = class(TForm)

18 var
19 frmBaseInitiator: TfrmBaseInitiator;

20 implementation

21 {$R *.dfm}

22 { procs FormCreate, FormDestroy & radCallBaseClick as ex 12.1 step 1


23 }

34 function TfrmBaseInitiator.GetCaption: string;


35 begin
36 Result := Caption;
37 end; // end function TfrmBaseInitiator.GetCaption

38 end. // end unit InitiatorU

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 15
Ex 12.2 step 2 Using the callback

Using the callback is like any other method call (lines 17–18 below).

1 unit RecipientU;
2 interface
3 { interface section as example 12.1 step 2 }

8 implementation

9 uses Dialogs, InitiatorU;

10 { TBaseRecipient }

11 procedure TBaseRecipient.MakeContact(Sender: TObject);


12 var
13 MsgStr: string;
14 begin
15 MsgStr := 'Hello ' + (Sender as TfrmBaseInitiator).ClassName;
16 MsgStr := MsgStr + #13#10 + 'I am ' + ClassName;
17 MsgStr := MsgStr + #13#10 + 'Your caption is: ' +
18 (Sender as TfrmBaseInitiator).GetCaption;
19 ShowMessage (MsgStr);
20 end; // end procedure TBaseRecipient.MakeContact

21 end. // end unit RecipientU

In terms of programming style, some programmers may prefer to avoid repeated


typecasting by declaring an appropriate local variable:

procedure TBaseRecipient.MakeContact(Sender: TObject);


var
MsgStr: string;
Caller: TfrmBaseInitiator;
begin
Caller := Sender as TfrmBaseInitiator;
MsgStr := 'Hello ' + Caller.ClassName;
MsgStr := MsgStr + #13#10 + 'I am ' + ClassName;
MsgStr := MsgStr + #13#10 + 'Your caption is: ' + Caller.GetCaption;
ShowMessage (MsgStr);
end; // end procedure TBaseRecipient.MakeContact

Summary of example 12.2: In this example we have extended example 12.1 to demonstrate a
simple callback. The initiator identifies itself through the Sender parameter, and the
recipient uses Sender to call one of the initiator’s public methods.

Using the Sender parameter is sufficiently important a technique that it has been identified
as the SenderArgument pattern, which we now present.

Chapter 12, Page 16 Object orientation with Delphi (all rights reserved)
Pattern 12.1 The SenderArgument Pattern

We can state the SenderArgument Pattern as follows:

On occasion it is necessary for an initiating object to identify itself to a recipient object. The
recipient can use this identification to verify the identity of the initiator for access control, to
supply the initiator with specific data or services, or to request further data from the
initiator. To promote flexibility and modifiability, the initiator’s identity should be estab-
lished at runtime at the start of each communication sequence and not be hard coded at
compile time.

Therefore,

the initiator should pass a reference to itself as a Sender Argument in the method call to
the recipient who then uses this reference for the return communication, typically on a
short-term basis.
The Sender parameter is generally passed as a TObject type. This prevents a circular
reference loop when the classes are in different units and enhances flexibility since any
object can then act as initiator. The recipient must downcast the Sender to an appropriate
type when issuing a callback.
Delphi makes extensive use of the SenderArgument in its event handling mechanism.

Link through the root

We now come to a third factor in establishing bidirectional communication between objects:


when several classes in a hierarchy need to communicate with the same class outside the
hierarchy, the communication link should be set up in a common parent of the classes and
not individually by each object. This reduces the amount of coding needed and the overall
coupling in the system.
To illustrate this, consider the case where any subclass in one hierarchy needs to
communicate with any subclass in another hierarchy so that any single pairing is possible
between the subclasses (figure 12).

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 17
Figure 12 Full linking between two sets of subclasses

To establish this communication structure, Subclass1a needs an explicit reference to both


Subclass2a and Subclass2b, and the same pattern holds for each of the other three
subclasses. (To keep it less cluttered, we assume that the base classes here are abstract.) With
two subclasses in each hierarchy we therefore need eight data fields altogether to hold the
references, and as we add further subclasses the number of links escalates dramatically.
However, if we link just the two base classes together, we can still link any class in the
one hierarchy with any class in the other hierarchy, but now we need only two references
(figure 13): each subclass inherits the reference to the other hierarchy from its superclass,
and can reach any class in the other hierarchy through polymorphism. In future, if any
further subclasses are added to either hierarchy, these subclasses will automatically be able
to use these links and benefit from the inheritance and the polymorphism without the need
for any further references. Linking through the base classes thus reduces the coupling in the
system, facilitating future maintenance and enhancement, and reduces the amount of
coding needed.

Figure 13 Full linking through the superclasses

This concept of linking through the root is relevant to the whole idea of bidirectional linking
and callbacks, and so we’ll explore it briefly by extending the previous example.

Chapter 12, Page 18 Object orientation with Delphi (all rights reserved)
Example 12.3 Linking through superclasses

Linking through superclasses has two aspects. The initiator can link to the recipient or any
of its subtypes (figure 14), as we explore in this example. The initiator’s subclasses inherit
the link to the recipient, and so the initiator’s subclasses can also use the link, as we explore
in the next example.

Figure 14 Adding subtypes to


the recipient

Ex 12.3 step 1 Linking to subtypes

To illustrate the process of linking to subtypes, we’ll first change the initiating user interface
class to call the subtypes (figure 15).

Figure 15 Calling recipient


subtypes

Figure 16 User interface objects

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 19
1 unit InitiatorU;
2 interface
3 uses
4 { uses clause as example 12.1 step 1 }

7 type
8 TfrmBaseInitiator = class(TForm)
9 { standard RAD declarations }
17 protected
18 RecBase: TBaseRecipient; // ref to base of recipient hierarchy
19 RecSub1: TSubRecipient1; // ref to 1st recipient subclass
20 RecSub2: TSubRecipient2; // ref to 2nd recipient subclass
21 public
22 function GetCaption: string;
23 end; // end TfrmBaseInitiator = class(TForm)

24 var
25 frmBaseInitiator: TfrmBaseInitiator;

26 implementation

27 {$R *.dfm}

28 procedure TfrmBaseInitiator.FormCreate(Sender: TObject);


29 begin
30 RecBase := TBaseRecipient.Create;
31 RecSub1 := TSubRecipient1.Create;
32 RecSub2 := TSubRecipient2.Create;
33 end; // end procedure TfrmBaseInitiator.FormCreate

34 procedure TfrmBaseInitiator.FormDestroy(Sender: TObject);


35 begin
36 RecBase.Free;
37 RecSub1.Free;
38 RecSub2.Free;
39 end; // end procedure TfrmBaseInitiator.FormDestroy

40 function TfrmBaseInitiator.GetCaption: string;


41 begin
42 Result := Caption;
43 end; // end function TfrmBaseInitiator.GetCaption

44 procedure TfrmBaseInitiator.radCallBaseClick(Sender: TObject);


45 begin
46 RecBase.MakeContact(Self); // identify self to recipient
47 end; // end procedure TfrmBaseInitiator.radCallBaseClick

48 procedure TfrmBaseInitiator.radCallSub1Click(Sender: TObject);


49 begin
50 RecSub1.MakeContact(Self);
51 end; // end procedure TfrmBaseInitiator.radCallSub1Click

52 procedure TfrmBaseInitiator.radCallSub2Click(Sender: TObject);


53 begin
54 RecSub2.MakeContact(Self);
55 end; // end procedure TfrmBaseInitiator.radCallSub2Click

Chapter 12, Page 20 Object orientation with Delphi (all rights reserved)
56 end. // end unit InitiatorU

First we declare objects of the recipient subtypes (figure 14) as protected data fields (lines
19–20) and create and free them (lines 31–32, 37–38). (In other applications a different
strategy for creating and destroying the recipient classes may be more appropriate, a
principle discussed in chapter 11.) We then add event handlers for the additional
RadioButtons to call the subtypes (lines 48–55).

Ex 12.3 step 2 Defining the new recipient subtypes

The recipient subtypes will identify themselves on the second line of the ShowMessage.
Clicking the RadioButton shown in figure 15 will lead to the display in figure 17; compare
this to figure 11.

Figure 17 Response by
recipient subclass

Because of the subtyping, the only code we need to add to RecipientU is an ‘empty’
declaration of the two subtypes (lines 8–11 below, and see figure 14).

1 unit RecipientU;

2 interface

3 type
4 TBaseRecipient = class(TObject) // base of recipient hierarchy
5 public
6 procedure MakeContact (Sender: TObject); virtual;
7 end; // end TBaseRecipient = class(TObject)

8 TSubRecipient1 = class(TBaseRecipient) // recipient subclass 1


9 end; // end TSubRecipient1 = class(TBaseRecipient)

10 TSubRecipient2 = class(TBaseRecipient) // recipient subclass 2


11 end; // end TSubRecipient2 = class(TBaseRecipient)

12 implementation

13 { TBaseRecipient }

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 21
14 procedure TBaseRecipient.MakeContact(Sender: TObject); // unchanged
15 var
16 MsgStr: string;
17 begin
18 MsgStr := 'Hello ' + (Sender as TfrmBaseInitiator).ClassName;
19 MsgStr := MsgStr + #13#10 + 'I am ' + ClassName;
20 MsgStr := MsgStr + #13#10 + 'Your caption is: ' +
21 (Sender as TfrmBaseInitiator).GetCaption;
22 ShowMessage (MsgStr);
23 end; // end procedure TBaseRecipient.MakeContact

24 end. // end unit RecipientU

TSubRecipient1 and TSubRecipient2 inherit the MakeContact method from TBaseRecipient


(lines 8, 10). So when the initiating class calls either of these (step 1, lines 50 or 54), they run
the inherited method but, because of dynamic linking, identify themselves in line 19. If
either of these classes had overrides for the MakeContact method, these override methods
would have been called instead of the inherited method.
Notice that the entire communication structure for this hierarchy is created in the base
class, TBaseRecipient, and so the subclasses automatically participate in the communication
structure without requiring any additional code. Generally, the subclasses add additional
application code and so are not empty classes as in this example. We’ll see this clearly in
example 12.5.
The interplay of inheritance and polymorphism is typical of an object oriented approach,
so take some time to make sure you understand how this set of recipient classes works.
As a side comment about coding, neither of the subclasses need the end keyword, and
lines 8–11 can be replaced as follows:

TSubRecipient1 = class(TBaseRecipient); // recipient subclass 1


TSubRecipient2 = class(TBaseRecipient); // recipient subclass 2

Summary of example 12.3: Example 12.3 demonstrates how reuse through inheritance means
that a link to a class provides linking to all its subclasses too. The superclass of the
recipient implements all the link handling mechanisms, which the subclasses inherit
automatically without having to implement any of the link handling themselves.

Example 12.4 Inheriting a link

To demonstrate that subclasses can use a link defined in a superclass (figure 18), we’ll create
two subclasses of the user interface using Visual Form Inheritance (VFI). (This allows us to
take advantage of the RAD creation and destruction of the forms.)

Chapter 12, Page 22 Object orientation with Delphi (all rights reserved)
Figure 18 Subclassing the link
initiator too

This will give us three user interface objects, the Initiator Base superclass and its two
Initiator Sub subclasses, that can initiate the link through to any of the recipient classes
(figure 19).

Figure 19 The user interface windows of the initiator hierarchy

Ex 12.4 step 1 User interface subclasses

Derive a VFI form from the existing user interface through the menu sequence File | New |
Other | <Project Name> | frmBaseInitiator. This form inherits its attributes and behaviour
from frmBaseInitiator and so we need code only for any differences we need. Change the
derived form’s name to frmSubInitiator1, its Caption to Initiator Sub 1, and its position so
that it does not overlap its parent (figure 19). Because of the inheritance, Delphi
automatically adds InitiatorU to the uses list (line 5 below) and we add nothing to this
form’s RAD generated code.

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 23
1 unit InitiatorSub1U;

2 interface

3 uses
4 Windows, Messages, SysUtils, Variants, Classes, Graphics,
5 Controls, Forms, Dialogs, StdCtrls, InitiatorU;

6 type
7 TfrmSubInitiator1 = class(TfrmBaseInitiator)
8 private
9 { Private declarations }
10 public
11 { Public declarations }
12 end;

13 var
14 frmSubInitiator1: TfrmSubInitiator1;

15 implementation

16 {$R *.dfm}

17 end.

If we wanted to, we could delete lines 8–11, or even 8–12, in which case we would have a
single line class definition:

TfrmSubInitiator1 = class(TfrmBaseInitiator);

Similarly, use VFI to create frmSubInitiator2. Change its name to frmSubInitiator2, its
Caption to Initiator Sub 2, and position it so that it does not overlap either of the other forms
(figure 19).
Finally, Show these two inherited forms in the main form’s OnShow event handler:

procedure TfrmBaseInitiator.FormShow(Sender: TObject);


begin
frmSubInitiator1.Show;
frmSubInitiator2.Show;
end; // end procedure TfrmBaseInitiator.FormShow

Run and test this new version of the program. Confirm that all the possible combinations of
initiator and recipient work correctly. For example, figure 19 shows initiator subclass1
calling recipient subclass2 as requested in figure 16. Figure 20 shows the correct initiator
(lines 1 & 3 of the ShowMessage) received by the correct recipient (line 2).

Chapter 12, Page 24 Object orientation with Delphi (all rights reserved)
Figure 20 The recipient
subclass2 responds to the
initiator subclass1

Summary of example 12.4: This example illustrates that, as a result of inheritance, once a link
exists between two classes, subclasses of both the initiator and the recipient classes can
participate in the linking without the need for any additional coding.

Example 12.5 Providing additional data to a role

Now that we have established this background, we can return to the problem at the start of
this chapter. We will add these principles to our existing Role–Player pattern program from
chapter 11. This will allow the transfer of the additional Hours data that THourly needs by
using a callback.
A callback is a general purpose strategy and is not limited just to specific data, as used in
this case for Hours. Once a callback is established, it can access any data access method or
property in the Caller. (Because of space considerations, we don’t illustrate this here.)
The callback structure is shown in figure 6, repeated here as figure 21 for convenience.

Figure 21 The callback structure between THourly and


TfrmCallbackDemo

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 25
Ex 12.5 step 1 Adapting TCategory

We’ll start by adapting TCategory to accept a calling object’s identity and then to use that
identity in issuing a callback, as shown by the links between TEmployee and TCategory in
figure 21.

1 unit CategoryU;

2 interface

3 type
4 TCategory = class(TObject)
5 public
6 function GetPackage (Sender: TObject; ANotch: integer): integer;
7 virtual; abstract;
8 function GetRole: string;
9 end; // end TCategory = class(TObject)

10 TTrainee = class(TCategory)
11 public
12 function GetPackage (Sender:TObject; ANotch:integer): integer;
13 override;
14 end; // end TTrainee = class(TCategory)

15 // TWeekly & TMonthly similarly add Sender to their parameter lists

25 THourly = class(TCategory)
26 public
27 function GetPackage (Sender:TObject; ANotch:integer): integer;
28 override;
29 end; // end THourly = class(TCategory)

30 implementation

31 uses EmployeeU; // for the callback

32 { TCategory’s GetRole definition as before.


33 TTrainee’s, TWeekly’s & TMonthly’s GetPackage methods as before,
34 except Sender: TObject is now included in their parameter lists }

57 { THourly }

58 function THourly.GetPackage(Sender:TObject;ANotch:integer): integer;


59 begin
60 Result := (Sender as TEmployee).GetHours * (40 + ANotch*15);
61 end; // end function THourly.GetPackage

62 end. // end CategoryU

We need to provide a reverse link to the calling object (a TEmployee) so that THourly’s
GetPackage method can request a value for the number of hours worked (line 60 above).

Chapter 12, Page 26 Object orientation with Delphi (all rights reserved)
This means that the calling object must identify itself, and so we modify the TCategory
hierarchy to accommodate a Sender: TObject parameter for the GetPackage method (line 6).
(Since we are changing the method signature in the base class, all its descendants also
change to have the same method signature, eg lines 12 & 27.) Even though we know that
Sender will be an object of type TEmployee, we cannot give it this type at this stage because
that would cause a circular reference between TEmployee and TCategory (as discussed
above). To avoid this circular reference we declare Sender as type TObject as events do. We
then need to use EmployeeU only in the implementation section (line 31).
We solicit the required data in line 60 by typecasting Sender as a TEmployee and
invoking its GetHours access method for the callback. So our next step is to modify
TEmployee to provide the required value for the Sender parameter and to implement the
callback method, as well as to communicate with the user interface.

Ex 12.5 step 2 Modifying TEmployee

We will modify TEmployee to communicate with both the role class TCategory and with the
user interface TfrmBase (figure 21). So we add a Sender: TObject parameter to the
GetPackage method’s parameter list to allow the client (ie the user interface) to identify
itself (line 18), a data field to store the client’s identity temporarily (line 11) and a GetHours
data access method for THourly to callback the hours entered by the user (lines 19, 41–44).

1 unit EmployeeU;

2 interface

3 uses
4 CategoryU;

5 type
6 TEmployee = class(TObject) // the Player
7 private
8 { Object references & data as before }
11 FCaller: TObject; // Caller = callback target
12 public
13 { property definitions as before }
18 function GetPackage (Sender: TObject): integer; // added Sender
19 function GetHours: integer; // Callback method for TCategory
20 end; // end TEmployee = class(TObject)

21 implementation

22 uses SysUtils, BaseFormU; // not CallBackDemoU; for the callback

23 { TEmployee }

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 27
24 procedure TEmployee.AssignRole(ACategory: integer);
25 begin
26 FreeAndNil (CategoryObj); // free previous role
27 // Create the new associated role object
28 case ACategory of
29 0: CategoryObj := TTrainee.Create;
30 1: CategoryObj := TWeekly.Create;
31 2: CategoryObj := TMonthly.Create;
32 3: CategoryObj := THourly.Create; // additional role
33 else CategoryObj := nil; // range / error control
34 end;
35 end; // end procedure TEmployee.AssignRole

36 destructor TEmployee.Destroy;
37 begin
38 CategoryObj.Free; // propagate destruction
39 inherited;
40 end; // end destructor TEmployee.Destroy

41 function TEmployee.GetHours: integer;


42 begin
43 Result := (FCaller as TfrmBase).GetHours; // delegate to user i/f
44 end; // end function TEmployee.GetHours

45 function TEmployee.GetPackage (Sender: TObject): integer;


46 begin
47 FCaller := Sender; // remember FCaller to set up the callback

48 if assigned (CategoryObj) then // simple error checking


49 Result := CategoryObj.GetPackage(Self, Notch) // delegation
50 else
51 Result := 0;
52 end; // end procedure TEmployee.Calculate

53 function TEmployee.GetRole: string;


54 begin
55 if assigned (CategoryObj) then // simple error checking
56 Result := CategoryObj.GetRole // polymorphic delegation
57 else
58 Result := 'Unassigned';
59 end; // end function TEmployee.GetRole

60 end. // end EmployeeU

We’ll start by looking at the TEmployee – TCategory communication. When a TEmployee


invokes a TCategory object, it must identify itself as required by the modified GetPackage
method signature (example 12.4, step 1, lines 6–7). Thus in the GetPackage method call in
line 49 above we introduce Self as the first parameter in the list. Self identifies the current
object. THourly can then use the caller’s identity to callback TEmployee’s GetHours method
(example 12.4, step 1, line 60). We declare the GetHours callback method in line 19 above
and define it in lines 41–44. As we see in line 43, GetHours delegates this call. To allow the
delegation of the GetHours callback, TEmployee must have stored a reference to the object

Chapter 12, Page 28 Object orientation with Delphi (all rights reserved)
which invoked it. Thus we add to TEmployee the protected data field FCaller of type
TObject (line 11 above). To allow the caller to identify itself, we introduce a Sender: TObject
as the first parameter in the GetPackage method (lines 18 & 45 above), and modify it to store
the Caller’s identity before invoking the TCategory role (line 47 above).
Thus, when THourly calls TEmployee’s GetHours method, TEmployee uses the stored
value of FCaller to delegate GetHours to its caller, which in this case is the user interface
(line 43, and refer to figure 21).
In the next step, we set up the user interface to use this revised version of TEmployee and
to implement a callback method to supply the value of Hours.

Ex 12.5 step 3 The base form for the user interface

We could simply adapt the existing user interface, TfrmPayment, to identify itself to
TEmployee and to provide the callback method. But this would introduce very tight
coupling since TEmployee would have to typecast its callback to the type of the user
interface, TfrmPayment. This would mean that only frmPayment or one of its descendants
could invoke TEmployee. We would like to provide for future enhancement of this program
and build in the possibility for other user interface objects to be able to use TEmployee.
To make the link between the user interface and TEmployee more flexible requires some
OO ingenuity. In providing the background to this example, we mentioned that it is much
more flexible to provide links through a hierarchy’s base class than directly between objects.
This allows all subclasses to use the base class’s links. So what we’ll do here is create a
hierarchy of user interface objects. The base of this hierarchy, which we’ll call TfrmBase, will
provide the linking through to TEmployee. Then any user interface that needs to use
TEmployee can do so easily provided it is a descendant of TfrmBase. At the moment we
have only a single user interface object, but we are building in the possibility for future
variations and development (figure 22, and figure 21).

Figure 22 Building a user


interface hierarchy to reduce
coupling

Our present user interface class, TfrmPayment, is descended directly from TForm, so we
can’t use it as it stands. Instead, we will first derive a new user interface class, TfrmBase,

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 29
from TForm. Then we’ll create a second form, TfrmCallBackDemo, which we will derive
from TfrmBase by using VFI (visual form inheritance).
Making this change is simple, just slightly messy, so we’ll go through it step by step.
First, to create the base form, add a new form to the project (File | New | Form) and change
its Name property to frmBase.

1 unit BaseFormU;

2 interface

3 uses
4 Windows, Messages, SysUtils, Variants, Classes, Graphics,
5 Controls, Forms, Dialogs,
6 EmployeeU; // for forward link to TEmployee

7 type
8 TfrmBase = class(TForm)
9 procedure FormCreate(Sender: TObject);
10 procedure FormDestroy(Sender: TObject);
11 protected
12 Employee: TEmployee; // link available to descendants
13 public
14 function GetHours: integer; virtual; abstract; //Callback method
15 end; // TfrmBase = class(TForm)

16 var
17 frmBase: TfrmBase;

18 implementation

19 {$R *.dfm}

20 procedure TfrmBase.FormCreate(Sender: TObject);


21 begin
22 Employee := TEmployee.Create;
23 end; // end procedure TfrmBase.FormCreate

24 procedure TfrmBase.FormDestroy(Sender: TObject);


25 begin
26 Employee.Free;
27 end; // end procedure TfrmBase.FormDestroy

28 end. // end unit BaseFormU

It’s a very simple class. We added a protected data member Employee of type TEmployee
(lines 11–12) so that any descendant of TfrmBase can link through to a TEmployee. We
consequently add EmployeeU to the global uses clause (line 6). We also declare an abstract,
public callback (access) method, GetHours, which every descendant must implement (line
14). In this application, the initiator creates and frees the recipient and we do this in the
form’s OnCreate and OnDestroy event handlers (lines 9–10, 20–27).

Chapter 12, Page 30 Object orientation with Delphi (all rights reserved)
Ex 12.5 step 4 The new user interface

Using VFI, inherit a new form from TfrmBase (File | New | Other | (project name) |
frmBase) and call it frmCallBackDemo. We could now just build frmCallBackDemo from
scratch the way we built frmPayment in chapter 11. Alternatively, we can copy all the
existing components from frmPayment and paste them onto frmCallBackDemo.
To make the copy, click on frmPayment to make it the active form. Now select all the
components on it through Edit | Select All. All the components should now be selected. Use
<Ctrl+C> or Edit | Copy to copy everything. Now click on frmCallBackDemo to make it the
active form and use <Ctrl+V> or Edit | Paste to paste the components onto the new form.
Create the skeletons for the three event handlers through the Object Inspector and then
copy the event handler bodies across from the old unit, BenefitsU, to the new form’s unit.
There is an extra Item, Hourly, in rgpCategory, a new GroupBox and SpinEdit for the
Hours, and on OnClick event handler for rgpCategory, so modify the new user interface
appropriately (figure 23, figure 24 and the code below).

Figure 23 The revised user interface

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 31
Figure 24 The user interface
objects

1 unit CallBackDemoU;

2 interface

3 uses
4 Windows, Messages, SysUtils, Variants, Classes, Graphics,
5 Controls, Forms, Dialogs, BaseFormU, StdCtrls, ExtCtrls, Spin;

6 type
7 TfrmCallBackDemo = class(TfrmBase) // VFI for callback links
8 { standard RAD declarations }
22 private
23 procedure UpdateDisplay; // new local helper method
24 public
25 function GetHours: integer; override; // virtual in ancestor
26 end; // end TfrmCallBackDemo = class(TfrmBase)

27 var
28 frmCallBackDemo: TfrmCallBackDemo;

29 implementation
30
31 {$R *.dfm}

32 procedure TfrmCallBackDemo.btnNameClick(Sender: TObject);


33 begin
34 Employee.Name := edtName.Text;
35 lblName.Caption := 'Name: ' + Employee.Name;
36 end; // end procedure TfrmCallBackDemo.btnNameClick

37 procedure TfrmCallBackDemo.rgpNotchClick(Sender: TObject);


38 begin
39 Employee.Notch := rgpNotch.ItemIndex;
40 UpdateDisplay;
41 end; // end procedure TfrmCallBackDemo.rgpNotchClick

42 procedure TfrmCallBackDemo.rgpCategoryClick(Sender: TObject);

Chapter 12, Page 32 Object orientation with Delphi (all rights reserved)
43 begin
44 if rgpCategory.ItemIndex = 3 then // set default values
45 begin
46 if sedHours.Value = 0 then
47 sedHours.Value := 160;
48 end
49 else
50 sedHours.Value := 0;
51 Employee.AssignRole(rgpCategory.ItemIndex);
52 UpdateDisplay;
53 end; // end procedure TfrmEmployee.rgpCategoryClick

54 function TfrmCallBackDemo.GetHours: integer;


55 begin
56 Result := sedHours.Value;
57 end; // end function TfrmCallBackDemo.GetHours

58 procedure TfrmCallBackDemo.UpdateDisplay;
59 begin
60 lblCategory.Caption := 'Category: ' + Employee.GetRole;
61 lblNotch.Caption := 'Notch: ' + IntToStr (Employee.Notch);
62 lblPackage.Caption:= 'Package: R' +
63 IntToStr(Employee.GetPackage(Self));
64 end; // end procedure TfrmCallBackDemo.UpdateDisplay

65 end. // end unit CallBackDemoU

The OnClick event handlers for both rgpNotch (lines 37–41) and rgpCategory (lines 42–53)
call the helper method UpdateDisplay (lines 58–64). In lines 62–63, frmCallBackDemo
identifies itself to the Employee object through the Self parameter.
The GetHours function (lines 54–57) provides an access method for other objects to read
the value of Hours, and is an implementation of the abstract method declared in TfrmBase.
TfrmCallBackDemo does not declare a reference to a TEmployee since it inherits the
reference from TfrmBase.
Make the new user interface, TfrmCallBackDemo, the project’s main form by selecting it
on the Form tab of the Options screen (Project | Options). Now remove TfrmPayment from
the project through Project | Remove from Project.
We have just substituted the new user interface, which is derived from TfrmBasic and
which supports the callbacks, for the previous one, so we are now set to run and test this
enhanced version of our Role–Player program.

This example uses relatively sophisticated OO techniques. It brings together a number of


different concepts, particularly if you are new to OO programming, and it might be useful to
work through it more than once to make sure that you understand the different
implications.

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 33
Callback methods

A callback method in object A is essentially a data access method (ie a Get... method) that
supplies data to object B at object B’s request. B’s request is a consequence of an operation
object A requested of object B.
Callback methods represent a pull communication strategy, where the recipient actively
requests the data it requires, in contract to parameter passing, where the initiator pushes a
fixed set of data to a passive recipient.
Parameter pushing by the initiator supplies data to all the roles (in the example here)
irrespective of whether or not they require it all. (In this example, TTrainee ignores the
ANotch parameter.) Parameter passing is fast, widely used, and easy to understand.
Pulling data through a callback allows data to be transferred selectively at the discretion
of the recipient, and allows different recipients to request different data. It requires extra
coding to establish the Sender and the access methods for the callbacks, although some of
the callback methods may already exist as general data access methods. Using callbacks is
slower than parameter passing and can be more difficult to understand. Once the initiator’s
identity has been established, the recipient can execute a callback on any of the initiator’s
access methods.
Choosing between parameter passing and issuing callbacks is a trade off between these
various factors.

Example 12.6 Two way communication between


classes in the same unit

If the base objects that are communicating with each other are in the same unit we can
overcome the problem of the circular reference through the use of forward declarations. For
example, in example 12.4, we can merge RecipientU into InitiatorU and create a single unit,
InitRecU, as follows:

1 unit InitRecU;

2 interface

3 uses
4 Windows, Messages, SysUtils, Variants, Classes, Graphics,
5 Controls, Forms, Dialogs, StdCtrls;

6 type

7 // forward declarations

Chapter 12, Page 34 Object orientation with Delphi (all rights reserved)
8 TBaseRecipient = class; // base of recipient hierarchy
9 TSubRecipient1 = class; // subclass of recipient hierarchy
10 TSubRecipient2 = class; // subclass of recipient hierarchy

11 // full declarations
12 TfrmBaseInitiator = class(TForm)
13 {Standard RAD declarations }

22 protected
23 RecBase: TBaseRecipient; // ref to base of recipient hierarchy
24 RecSub1: TSubRecipient1; // ref to 1st recipient subclass
25 RecSub2: TSubRecipient2; // ref to 2nd recipient subclass
26 public
27 function GetCaption: string;
28 end; // end TfrmBaseInitiator = class(TForm)
29
30 TBaseRecipient = class(TObject) // base of recipient hierarchy
31 public
32 procedure MakeContact (Sender: TfrmBaseInitiator); virtual;
33 end; // end TBaseRecipient = class(TObject)

34 TSubRecipient1 = class(TBaseRecipient); // recipient subclass 1


35 TSubRecipient2 = class(TBaseRecipient); // recipient subclass 2

36 var
37 frmBaseInitiator: TfrmBaseInitiator;

38 implementation

39 uses InitiatorSub1U, InitiatorSub2U;

40 {$R *.dfm}

41 { TfrmBaseInitiator }

42 { procedures FormCreate, FormDestroy, GetCaption,


43 radCallBaseClick, radCallSub1Click and radCallSub2Click
44 as in example 12.3 step 1 lines 28-55 }

71 procedure TfrmBaseInitiator.FormShow(Sender: TObject);


72 begin
73 frmSubInitiator1.Show;
74 frmSubInitiator2.Show;
75 end; // end procedure TfrmBaseInitiator.FormShow

76 { TBaseRecipient }

77 procedure TBaseRecipient.MakeContact(Sender: TfrmBaseInitiator);


78 var
79 MsgStr: string;
80 begin
81 MsgStr := 'Hello ' + Sender.ClassName;
82 MsgStr := MsgStr + #13#10 + 'I am ' + ClassName;
83 MsgStr := MsgStr +#13#10+ 'Your caption is: ' + Sender.GetCaption;
84 ShowMessage (MsgStr);
85 end; // end procedure TBaseRecipient.MakeContact

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 35
86 end. // end unit InitiatorU

Lines 8–10 are forward declarations of the three recipient classes – they inform the compiler
that these three names refer to classes, but give no indication of the class structure. When
the compiler then encounters the references in lines 23–25, it knows at least that these are
references to classes and so does not give an ‘unknown identifier’ error message. Lines
30–35 then declare these classes fully and can refer to Sender as type TfrmBaseInitiator in
line 32, since this type has already been declared.
Notice the difference between forward declarations (lines 9–10) and declarations of
classes that do not extend or specialise their superclass in any way (lines 34–35). When using
forward declarations, the full declaration must appear later in the same unit as the forward
declaration.
The implementation section of this unit effectively combines the implementation sections
of the original two units. However, in TBaseRecipient’s MakeContact method it is now no
longer necessary to typecast Sender since it is already of type TfrmBaseInitiator. This is
possible because the classes are defined in the same unit and the use of forward
declarations overcomes the issue of circular referencing.
This technique of defining communicating classes in the same unit is an interesting one.
Generally, however, it is preferable to define unrelated classes in different units to reduce
system coupling and to allow the classes to be reused separately. An exception is when
either of the communicating classes can initiate the communication, since it is then simpler
to have both in the same unit. However, it is not necessary to have their descendants in the
same unit. In the example above, TBaseRecipient’s descendants are in the same unit, but
TfrmBaseInitiator’s descendants are in separate units.

Example 12.7 A linking class

A callback embodies a bidirectional 1:1 association between two classes. These principles
naturally apply to bidirectional associations in general, and so are similar to those we
explored in chapter 9. Sometimes, though, it is not sufficient simply to link two classes.
Sometimes data is needed about the link itself. For example, we may have a situation where
a gym member may freely participate in any of the gym’s activities (figure 25). This requires
an n:n link, which is a bit messy, but follows the principles we covered in chapter 9 and in
this chapter.

Chapter 12, Page 36 Object orientation with Delphi (all rights reserved)
Figure 25 An n:n association

But what happens where the gym member must pay an additional fee to participate in the
activity? The system must now contain data about whether or not the fee has been paid.
How do we store this data?
A member may be involved in a variety of activities (1..n or 1..*). Similarly, many
different members can participate in the same activity. If we look at the semantics of the
situation, we see that whether or not a particular member has paid the fee for a particular
activity is not about either the member or the activity in isolation. It is actually data about
the relationship between that member and that activity, and our model should reflect this. So
we introduce an additional enrolment class (figure 26), where TEnrolment performs the
function of a linking class or an association class. So now, instead of simply using a reference
to link two classes, we create an entirely new class to provide the linking and to maintain
data about the link itself. This is therefore different to an n:n link between two classes.

Figure 26 Using an association class

From this diagram we see that:


– TEnrolment, the association class, carries a reference to one TMember and to one TActivity
plus the data about the fee status, and so TEnrolment carries information about a single
TMember–TActivity relationship.
– TMember can carry references to many TEnrolments, each of which carries a reference to a
single TActivity. Thus any TMember can be linked to many TActivities and each link has
its own separate TEnrolment.
– TActivity can carry references to many TEnrolments, each of which carries a reference to a
single TMember. Thus any TActivity can be linked to many TMembers and each link has
its own sesparate TEnrolment.
So, including an association class in our domain model allows us a set of 1:n relationships in
both directions, each with its own link data.

We can also draw an object diagram. To keep the example simple, we’ll have the ‘*’
multiplicity take on the value ‘2’, and we’ll have three members and two activities, with two
of the members enrolled in one activity each, and the other in two activities (figure 27).

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 37
Figure 27 An object diagram illustrating association objects

In other words, in figure 25 TMember sees many TActivities. In figures 26 & 27, TMember
sees many TEnrolments each of which is directly mapped to a TActivity. Thus the linking
provided in figures 25 & 27 is the same, but the details of each association are concentrated
in a TEnrolment object rather than being shared between TMember and TActivity.
We need to know whether each member has paid the fees for each activity. As we
mentioned, this is data that relates separately to each :TMember – :TActivity association.
Since there is a separate :TEnrolment object for each association, and each association is
specified separately in a :TEnrolment object, we use the TEnrolment class to store the fees
paid status separately for each association.
TMember and TActivity correspond closely with our understanding of the physical
application and we can see the need for them intuitively. However, we realised that trying
to give either class the responsibility for carrying the FeeStatus would be cumbersome. So
we fabricated a class that does not necessarily have a close correspondence to a physical
object but that enhances the implementation and improves cohesion. In general, an
association class carries the references to the associated classes and any data about the
association. The association itself occurs through this association class.

We’ll create an association class with bidirectional 1:1 linking. Extending this to the 1:n or
n:n case can be done by adding TList components.

1 unit AssocU;

2 interface

3 uses ActivityU, MemberU;

4 type
5 TAssociation = class(TObject)
6 private
7 FFeeStatus: Boolean;
8 FTheActivity: TActivity;
9 FTheMember: TMember;
10 public
11 property TheMember: TMember read FTheMember; // (read-only) link
12 property TheActivity: TActivity read FTheActivity; // 2nd link
13 property FeeStatus: Boolean read FFeeStatus; // data

Chapter 12, Page 38 Object orientation with Delphi (all rights reserved)
14 constructor Create (AMember: TMember;
15 AnActivity: TActivity; AStatus: Boolean);
16 end; // end TAssociation = class(TObject)

17 implementation

18 { TAssociation }

19 constructor TAssociation.Create(AMember: TMember;


20 AnActivity: TActivity; AStatus: Boolean);
21 begin
22 inherited Create;
23 FTheMember := AMember; // outgoing link
24 TheMember.Link := Self; // return link
25 FTheActivity := AnActivity; // outgoing link
26 TheActivity.Link := Self; // return link
27 FFeeStatus := AStatus; // link data
28 end; // end constructor TAssociation.Create

29 end. // end unit AssocU

The TAssociation class provides links to the domain objects (members and activities). These
domain objects require return links to the association class, and we have to take care with
them to prevent circular linking. First we give the TMember class.

1 unit MemberU;

2 interface

3 type
4 TMember = class(TObject)
5 private
6 FMember: string;
7 FLink: TObject;
8 public
9 property Member: string read FMember; // data
10 property Link: TObject read FLink write FLink;//association class
11 constructor Create (AMember: string);
12 procedure LinkTo(AnActivity: TObject; AStatus: Boolean);
13 end; // end TMember = class(TObject)

14 implementation

15 uses SysUtils, ActivityU, AssocU;

16 { TMember }

17 constructor TMember.Create(AMember: string);


18 begin
19 FMember := AMember;
20 end; // end constructor TMember.Create

21 procedure TMember.LinkTo(AnActivity: TObject; AStatus: Boolean);


22 begin

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 39
23 FreeAndNil (FLink);
24 TAssociation.Create(Self, (AnActivity as TActivity), AStatus);
25 end; // end procedure TMember.LinkTo

26 end. // unit MemberU

The TActivity class is similar.

1 unit ActivityU;

2 interface

3 type
4 TActivity = class(TObject)
5 private
6 FActivity: string;
7 FLink: TObject;
8 public
9 property Activity: string read FActivity; // data
10 property Link: TObject read FLink write FLink; // association
11 constructor Create (AnActivity: string);
12 procedure LinkTo(AMember: TObject; AStatus: Boolean);
13 end; // end TActivity = class(TObject)

14 implementation

15 uses SysUtils, MemberU, AssocU;

16 { TActivity }

17 constructor TActivity.Create(AnActivity: string);


18 begin
19 FActivity := AnActivity;
20 end; // end constructor TActivity.Create

21 procedure TActivity.LinkTo(AMember: TObject; AStatus: Boolean);


22 begin
23 FreeAndNil (FLink);
24 TAssociation.Create((AMember as TMember), Self, AStatus);
25 end; // end procedure TActivity.LinkTo

26 end. // end unit ActivityU

We can use a visual class to demonstrate the association (figure 28).

Chapter 12, Page 40 Object orientation with Delphi (all rights reserved)
Figure 28 Demonstrating an association class

Figure 29 Objects on the user


interface

1 unit AssocClassDemoU;

2 interface

3 uses
4 Windows, Messages, SysUtils, Variants, Classes, Graphics,
5 Controls, Forms, Dialogs, StdCtrls, ExtCtrls;

6 type
7 TfrmAssocClass = class(TForm)
8 { standard RAD declarations }
23 private
24 procedure FailMessage;
25 procedure ClearMessage;
26 end; // end TfrmAssocClass = class(TForm)

27 var

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 41
28 frmAssocClass: TfrmAssocClass;

29 implementation

30 uses ActivityU, AssocU, MemberU;

31 var
32 ThisMember: TMember;
33 ThisActivity: TActivity;

34 {$R *.dfm}
35
36 procedure TfrmAssocClass.btnRelationshipClick(Sender: TObject);
37 begin
38 try
39 FreeAndNil (ThisMember); // or ThisMember.Free;
40 ThisMember := TMember.Create(edtMember.Text);
41 FreeAndNil (ThisActivity); // or ThisActivity.Free;
42 ThisActivity := TActivity.Create(edtActivity.Text);
43 // Create the association
44 ThisMember.LinkTo (ThisActivity, chkFees.Checked);
45 //ThisActivity.LinkTo (ThisMember, chkFees.Checked);//alternative
46 ClearMessage;
47 except
48 FailMessage;
49 end;
50 end; // end procedure TfrmAssocClass.btnRelationshipClick

51 procedure TfrmAssocClass.rgpNavigateClick(Sender: TObject);


52 begin
53 try
54 case rgpNavigate.ItemIndex of
55 0:
56 begin // navigate link from member
57 lblSource.Caption := 'Source: ' + ThisMember.Member;
58 lblDestination.Caption := 'Destination: ' +
59 (ThisMember.Link as TAssociation).TheActivity.Activity;
60 if (ThisMember.Link as TAssociation).FeeStatus then
61 lblStatus.Caption := 'Status: Fees paid'
62 else
63 lblStatus.Caption := 'Status: Fees not paid';
64 end;
65 1:
66 begin // navigate link from activity
67 lblSource.Caption := 'Source: ' + ThisActivity.Activity;
68 lblDestination.Caption := 'Destination: ' +
69 (ThisActivity.Link as TAssociation).TheMember.Member;
70 if (ThisActivity.Link as TAssociation).FeeStatus then
71 lblStatus.Caption := 'Status: Fees paid'
72 else
73 lblStatus.Caption := 'Status: Fees not paid';
74 end;
75 else
76 raise Exception.Create ('Error');
77 end; // end case rgpNavigate.ItemIndex of
78 except
79 FailMessage;

Chapter 12, Page 42 Object orientation with Delphi (all rights reserved)
80 end;
81 end; // end procedure TfrmAssocClass.rgpNavigateClick

82 procedure TfrmAssocClass.FailMessage;
83 begin
84 lblSource.Caption := 'Operation failed:';
85 lblDestination.Caption := '- invalid link';
86 lblStatus.Caption := '';
87 end; // end procedure TfrmAssocClass.FailMessage

88 procedure TfrmAssocClass.ClearMessage;
89 begin
90 lblSource.Caption := '';
91 lblDestination.Caption := '';
92 lblStatus.Caption := '';
93 end; // end procedure TfrmAssocClass.ClearMessage

94 end. // end unit AssocClassDemoU

This driver class is a bit contrived, but nevertheless demonstrates the principles of an
association class.

Chapter Summary

– Bidirectional links between objects and the problem of circular referencing.


– Flexible bidirectional linking with the SenderArgument Pattern.
– The importance of inheritance when linking between hierarchies.
– The use of callbacks and access methods.
– A typical application of callbacks.
– An association class.

Objects as interacting entities: Reusing communication links through inheritance, bidirectional


linking, callbacks, an association class.

References

As reported by Van Camp, the SenderArgument pattern was formulated by Noble.

Van Camp, D. 2002. SenderArgument. PatternDigest. Available at:


http://patterndigest.com/patterns/SenderArgument.html. Viewed 14/5/2003.

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 43
Problems

Problem 12.1 Study Chapter 12

Identify the appropriate example(s) or section(s) of the chapter to illustrate each comment
made in the summary at the end of chapter 12.

Problem 12.2 Updating displays via callbacks

In this problem we have an entry form for data about exchange rates (figure 30) and
separate forms for viewing one of the available values (figurer 31). The user enters the
appropriate rates for the Euro, Pound and Dollar (figure 30) and then clicks the Notify
button. This sends a Notify message from the entry form to each viewing form.

Figure 30 Entering the current


exchange rates

When a viewing form receives a Notify message, it Shows itself, keeps a record of the
Sender of the message, and then sets its background colour to yellow to notify the user that
a Notify message has been received. If the user wants a particular updated value, he or she
Clicks the Update button on the relevant viewing form. This viewing form now sends an
update request to the entry form. This enters the identity of the requesting viewing form
into a ListBox and then supplies the required value. The viewing form updates its display
with this new value and resets its background colour to the standard default value.
The Notify and Update methods are not dependent on each other. Thus the entry form
may send repeated Notify messages to the viewing forms irrespective of whether it has

Chapter 12, Page 44 Object orientation with Delphi (all rights reserved)
received any update requests or not. Similarly, a viewing form may issue an Update request
irrespective of whether it has received any Notify messages since its last Update request. In
response to an Update request, the viewing form returns the value currently on display.

Figure 31 Viewing the exchange rates

From figures 30 and 31, we can see that the viewing form has at least once issued a Notify
message to each viewing form. The first viewing form to request an update was the Pounds
form, since TfrmPounds is the first entry in the ListBox (figure 30). The Euros viewing form
then requested an Update, while the Dollars form has not yet requested any update. Thus,
the Dollars form is still yellow, it is not displaying any value, and its name does not appear
in the ListBox.

This projects consists of five units, each declaring a class. The class declarations are as
follows. The entry form (figure 30):

unit SetRateU;

type
TfrmSetRate = class(TForm)
gpbEuro: TGroupBox;
edtEuro: TEdit;
gpbSterling: TGroupBox;
edtSterling: TEdit;
gpbDollar: TGroupBox;
edtDollar: TEdit;
btnNotify: TButton;
gpbCallers: TGroupBox;
lstCallers: TListBox;
procedure btnNotifyClick(Sender: TObject);
protected
{… and helper methods that may be required …}
public
function UpdateDollar(Sender: TObject): string;
function UpdateEuro(Sender: TObject): string;
function UpdatePound(Sender: TObject): string;

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 45
end; // end TfrmCaller = class(TForm)

The template for the viewing forms:

unit ViewRateU;

type
TfrmExchangeRate = class(TForm)
gpbExchangeRate: TGroupBox;
lblExchangeRate: TLabel;
btnUpdate: TButton;
procedure btnUpdateClick(Sender: TObject);
protected
FCaller: TfrmSetRate; // Callback target used by descendants
public
procedure Notify (Sender: TObject);
end; // end TfrmExchangeRate = class(TForm)

The Euro viewing form:

unit ViewEuroU;

type
TfrmEuros = class(TfrmExchangeRate)
procedure btnUpdateClick(Sender: TObject);
end; // end TfrmEuros = class(TfrmExchangeRate)

The Pound and Dollar viewing forms are similar to the Euro viewing form.
Write this program.

Problem 12.3 Additional callback for the Role-Player


pattern

Modify the Role–Player callback example (example 12.5) to accommodate a change in the
Trainee category. All trainees who received study loans from the company have 5% of their
Basic deducted as repayment of their study loans.

Problem 12.4 Implementing an association class

Answer the following questions about the units in Example 12.7:


a) What is the significance of lines 23 & 25 in unit AssocU?
b) What is the significance of lines 24 & 26 in unit AssocU?
c) What is the significance of line 27 in unit AssocU?

Chapter 12, Page 46 Object orientation with Delphi (all rights reserved)
d) What is the significance of lines 11–13 in unit AssocU?
e) How, in MemberU and ActivityU, do we prevent circular linking?
f) Under what conditions are the link between the Member and the Activity established?
g) Lines 58–60 and 68–70 in AssocClassDemoU have several levels of delegation. Explain
what is happening here.
h) What principle do lines 58–60 and 68–70 contravene? Give whatever modified code is
necessary in order to rewrite these lines in accordance with this principle. (Hint: What is
necessary to be able to rewrite lines 66–74 as:

66 begin // navigate link from activity


67 with ThisActivity do
68 begin
69 lblSource.Caption := 'Source: ' + Activity;
70 lblDestination.Caption := 'Destination: ' + Member;
71 if FeeStatus then
72 lblStatus.Caption := 'Status: Fees paid'
73 else
74 lblStatus.Caption := 'Status: Fees not paid';
75 end;
76 end;

and similarly for lines 56–64? A slightly simpler alternative is to replace Member in line 70
above with GetMember, and FeeStatus in line 71 with GetFeeStatus.)

Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 47

Potrebbero piacerti anche