Friday 16 February 2007

De-coupling inheritance dependencies in Delphi

I had been having problems with testing a Delphi unit that fired events off and had many subclasses, when I realised that a different approach would solve the problem. The new approach solved a long-term problem that I had with my design – too much coupling between classes because of the way Delphi implements inheritance.

The long-term problem is one that applies in many places in my system design. It could be regarded merely as a theoretical issue, but a more elegant design sorts it out in many places.

In the requirements for the system, the following structure comes up repeatedly:

Several object types need to share a common interface, but each has its own behaviour. My standard way of defining these requirements is to define a category (see note below) which defines the interface. In addition I define the individual object types that have the different behaviours. The category is defined as the intersection of the individual object types. My original method of implementing this structure was to follow the suggestions in the definition of a category, to define a superclass corresponding to the category, and descendant classes corresponding to the individual behaviours. This enables common behaviour to be isolated out into the superclass.

Example
The requirements for part of my system involve the definition of structure nodes and part of the requirements for structure node is:

Thus structure node is existence dependent on the three categorised node types.

This was originally implemented in Delphi as five units, one for each specialisation, one for the abstract parent, and one for the interface of the datatype, as follows:
**********************************************************************
unit Node;
interface
uses Classes, Constants, Operations;
type
IJSPNode = Interface
function AddChild(aJSPTYpe: TNodeType = jspSEQ): IJSPNode;
....
procedure AddOperation(const NewOperation: string);
procedure Assign(Source: IJSPNode);
procedure SetChild(i: integer; aNode: IJSPNode);
property ChildCount: integer read GetChildCount;
property Condition: string read GetCondition write SetCondition;
....
end;
implementation
end.
*****************************************************************

unit NodeImpl;
interface
uses
Classes, Constants, Node, Operations;
function NewJSPNode(ATNodeType: TNodeType): IJSPNode;
type
TJSPNode = class (TInterfacedPersistent, IJSPNode, Iinterface)
strict private
....
strict protected
....
public
function AddChild(aJSPTYpe: TNodeType = jspLEA): IJSPNode; virtual; abstract;
procedure AddOperation(const aOperation: string); virtual; final;
procedure Assign(Source: IJSPNode); reintroduce;
....
end;
implementation
uses
Math, NodeAdmImpl, .... NodeItrImpl, .... NodeSeqImpl,StrUtils, Sysutils;
function NewJSPNode(ATNodeType: TNodeType): IJSPNode;
begin
case ATNodeType of
jspADM: Result := NewPositAdmitNode;
jspITR: Result := NewIterationNode;
jspSEQ: Result := NewSequenceNode;
....
end;
end;
....
end.
***************************************************************************

unit NodeItrImpl;
interface
uses
Classes, Constants, Node, NodeImpl;
function NewIterationNode: IJSPNode;
implementation
type
TIterationNode = class(TJSPNode)
public
class function SetJSPType: TNodeType; override; final;
function AddChild(aJSPTYpe: TNodeType = jspLEA): IJSPNode; override; final;
function AddChildBefore(AChild: IJSPNode; aJSPType: TNodeType = jspLEA): IJSPNode; override; final;
end;
function NewIterationNode: IJSPNode;
begin
Result := TIterationNode.Create;
end;
.....
end.
.....
*************************************************************************

There are a number of characteristics of this code, some related to my style of coding, but some forced on me by the constraints of Delphi.

The declaration of the interface is fine, but, in order to provide a class from which I can inherit I need to expose the interface of the implementing class, so that other units can see it.

The implementation of the class implementing the interface needs to know about all its descendants, which will provide maintenance problems as the system evolves.

There is a massive case statement within the factory method which needs to know about all the subclasses. This is again fragile under change.

The visibility of the elements needed purely by the demands of the implementing language presents an opportunity for developer's of the system subsequently to misuse the implementation parts from misunderstanding or a desire to shortcut some of the “clutter”.

The solution to these problems was to realise that the inversion of control did not need to be implemented by inheritance, but rather could be implemented by delegation. The categorised items do not become specialisations, but rather classes in their own right, with an internal variable of type category interface. Any methods that are implemented in the category class are delegated from the “specialisation” class to the internal variable. Any methods that would be overriden in the first solution are implemented directly in the categorised item class. This leads to a unit for the interface which is unchanged from that above, but the class definitions become:
*****************************************************************************
unit NodeImpl;
interface
uses
Classes, Node;
function NewStructuredNode: IJSPNode;
implementation
type
TJSPNode = class (TInterfacedPersistent, IJSPNode, Iinterface)
strict private
....
strict protected
....
public
....
function AddChild(aJSPTYpe: TNodeType = jspLEA): IJSPNode; /* Do nothing */
procedure AddOperation(const aOperation: string);
procedure Assign(Source: IJSPNode); reintroduce;
....
end;
uses
Math, StrUtils, Sysutils, Constants, Operations;
function NewStructureNode: IJSPNode;
begin
Result := TJSPNode.Create;
end;
....
end.
*************************************************************************

unit NodeItrImpl;
interface
uses
Classes, Node;
function NewIterationNode: IJSPNode;
implementation
uses
Constants, NodeImpl;
type
TIterationNode = class(TInterfacedPersistent, IJSPNode, Iinterface)
strict private
aStructuredNode: IJSPNode;
public
class function SetJSPType: TNodeType;
function AddChild(aJSPTYpe: TNodeType = jspLEA): IJSPNode;
function AddChildBefore(AChild: IJSPNode; aJSPType: TNodeType = jspLEA): IJSPNode;
end;
function NewIterationNode: IJSPNode;
begin
Result := TIterationNode.Create;
end;
constructor Create(....);
begin
aStructuredNode := NewStructureType;
....
end;
function AddChildBefor(....)
begin
Result := aStructuredNode.AddChildBefore(....);
....
end;
.....
end.
.....
*************************************************************************
This approach does not quite get rid of all the problems of coupling. It is still necessary for something external to this part of the system to know what type of structured node is being demanded, but this is natural in this system, so not eliminating this lingering remnant of coupling is not significant.

Naming Rationale
I prefer this name to generalisation as the latter suggests that the system is to be implemented in a particular way. This construct arises in statements of requirements whereas generalisation arises in specifications. This term was used in the KISS method by Kristen[1] and was defined there as:
Category
A category takes care of the reuse of action types by different object types, weak object types or gerunds. For a category we always identify two or more parents. For transformation to the implementation environment, the category can incorporate the generic attribute types of its parents, so the attribute types have to be specified only once. In an object-orientated programming language the category can be implemented as an abstract superclass. The category allows us to model polymorphism.
[1] Object Orientation The KISS Method from Information Architecture to Information System.
ISBN 0-201-42299-9
Gerald Kristen 1994
Addison-Wesley Publishers Ltd.

No comments:

Post a Comment