Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
links, Callbacks
and linking classes
Main concepts
Chapter contents
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 1
Bidirectional linking: the problem of a circular reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Callback methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
Example 12.6 Two way communication between classes in the same unit . . . . . . . . . . . . . . 34
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.
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).
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.
A very simple solution is for THourly to display an InputBox asking the user to enter the
number of hours (figure 2).
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
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):
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
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).
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.
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.
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.
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).
Chapter 12, Page 10 Object orientation with Delphi (all rights reserved)
Figure 8 Link initiator object
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.)
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}
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
4 type
5 TBaseRecipient = class(TObject)
6 public
7 procedure MakeContact (Sender: TfrmBaseInitiator); virtual;
8 end; // end TBaseRecipient = class(TObject)
9 implementation
11 { TBaseRecipient }
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.
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
10 { TBaseRecipient }
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.
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
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}
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
10 { TBaseRecipient }
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
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.
Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 17
Figure 12 Full linking between two sets of subclasses
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.
To illustrate the process of linking to subtypes, we’ll first change the initiating user interface
class to call the subtypes (figure 15).
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}
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).
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)
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
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.
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).
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:
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.
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.
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)
25 THourly = class(TCategory)
26 public
27 function GetPackage (Sender:TObject; ANotch:integer): integer;
28 override;
29 end; // end THourly = class(TCategory)
30 implementation
57 { THourly }
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.
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
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
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.
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).
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}
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).
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}
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
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
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.
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.
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)
36 var
37 frmBaseInitiator: TfrmBaseInitiator;
38 implementation
40 {$R *.dfm}
41 { TfrmBaseInitiator }
76 { TBaseRecipient }
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.
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.
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
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 }
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
16 { TMember }
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
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
16 { TActivity }
Chapter 12, Page 40 Object orientation with Delphi (all rights reserved)
Figure 28 Demonstrating an association class
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
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
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
This driver class is a bit contrived, but nevertheless demonstrates the principles of an
association class.
Chapter Summary
References
Bidirectional links, Callbacks and linking classes (22 Jan 2007) Chapter 12, Page 43
Problems
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.
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.
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.
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)
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)
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.
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.
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:
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