views:

246

answers:

2

Consider an interface like

IMyInterface = interface
  procedure DoSomethingRelevant;
  procedure Load (Stream : TStream);
  procedure Save (Stream : TStream);
end;

and several classes that implement the interface:

TImplementingClass1 = class (TInterfacedObject, IMyInterface)
  ...
end;
TImplementingClass2 = class (TInterfacedObject, IMyInterface)
  ...
end;
...

I have a class that has a list of IMyInterface implementors:

TMainClass = class
strict private
  FItems : TList <IMyInterface>;
public
  procedure LoadFromFile (const FileName : String);
  procedure SaveToFile (const FileName : String);
end;

Now to the question: how can I load the main class and especially the item list in an object-oriented manner? Before I can call the virtual Load method for the items, I have to create them and thus have to know their type. In my current implementation I store the number of items and then for each item

  • a type identifier (IMyInterface gets an additional GetID function)
  • call the Save method of the item

But that means that during loading I have to do something like

ID := Reader.ReadInteger;
case ID of
  itClass1 : Item := TImplementingClass1.Create;
  itClass2 : Item := TImplementingClass2.Create;
  ...
end;
Item.Load (Stream);

But that doesn't seem to be very object-oriented since I have to fiddle with existing code every time I add a new implementor. Is there a better way to handle this situation?

+4  A: 

One solution would be to implement a factory where all classes register themselve with a unique ID.

TCustomClassFactory = class(TObject)
public      
  procedure Register(AClass: TClass; ID: Integer);
  function Create(const ID: Integer): IMyInterface;
end;

TProductionClassFactory = class(TCustomClassFactory)
public
  constructor Create; override;
end;

TTestcase1ClassFactory = class(TCustomClassFactory);
public
  constructor Create; override;
end;

var
  //***** Set to TProductionClassFactory for you production code,
  //      TTestcaseXFactory for testcases or pass a factory to your loader object.
  GlobalClassFactory: TCustomClassFactory;

implementation

constructor TProductionClassFactory.Create;
begin
  inherited Create;
  Register(TMyImplementingClass1, 1);
  Register(TMyImplementingClass2, 2);
end;

constructor TTestcase1ClassFactory.Create;
begin
  inherited Create;
  Register(TMyImplementingClass1, 1);
  Register(TDoesNotImplementIMyInterface, 2);
  Register(TDuplicateID, 1);
  Register(TGap, 4);
  ...
end;

Advantages

  • You can remove the conditional logic from your current load method.
  • One place to check for duplicate or missing ID's.
Lieven
+1 Thanks for the answer! From where would the Register method be called? From the initialization section? That would just move the code that I have to change. Your second advantage is true though. Don't get me wrong: nice solution, but depending on where to call Register, still not perfect.
Smasher
I would let the factory unit know all Implementation units and register each implementation class in constructor of the factory. When and where you construct the factory would then be up to you. This would make it easier to swap out the factory with another test factory for you testcases (you do have testcased don't you ;). The disadvantage of such a solution is that it abstracts everything up to a point where it becomes difficult to know how the Loader and the implementors are glued together.
Lieven
+2  A: 

You need a class registry, where you store every class reference together with their unique ID. The classes register themselves in the initialization section of their unit.

TImplementingClass1 = class (TInterfacedObject, IMyInterface)
  ...
end;
TImplementingClass2 = class (TInterfacedObject, IMyInterface)
  ...
end;

TMainClass = class
public
  procedure LoadFromFile (const FileName : String);
  procedure SaveToFile (const FileName : String);
end;

Edit: moved the class registry into a separate class:

TMyInterfaceContainer = class 
strict private
class var
  FItems : TList <IMyInterface>;
  FIDs: TList<Integer>;
public
  class procedure RegisterClass(TClass, Integer);
  class function GetMyInterface(ID: Integer): IMyInterface;
end;

procedure TMainClass.LoadFromFile (const FileName : String);
  ...
  ID := Reader.ReadInteger;
  // case ID of
  //   itClass1 : Item := TImplementingClass1.Create;
  //   itClass2 : Item := TImplementingClass2.Create;
  //   ...
  // end;
  Item := TMyInterfaceContainer.GetMyInterface(ID);
  Item.Load (Stream);
  ...

initialization
  TMyInterfaceContainer.RegisterClass(TImplementingClass1, itClass1);
  TMyInterfaceContainer.RegisterClass(TImplementingClass2, itClass2);

This should point you into the direction, for a very good introduction into these methods read the famous Martin Fowler article, esp. the section about Interface Injection

Ozan
"The classes register themselves in the initialization section of their unit" ... that's a bad idea IMHO. Why should the implementing classes know about the existence of TMainClass?
Smasher
@Smasher: This doesn't follow, the design in the answer is just unfortunate. The class registry should reside outside the TMainClass, maybe where the interfaces are declared. You can add as many levels of indirection as you wish. However letting the implementation classes register themselves with a global registry does make sense, as everything works just by adding and removing the units of implementation classes.
mghie
Exactly. Thank you mghie, for explaining my point, the essence of which is the **runtime** binding of TTImplementingClassX and TMainClass.
Ozan
+1 although the initialization section should read TMyInterfaceContainer.RegisterClass(...
skamradt
thanks for noticing, I corrected that.
Ozan