views:

367

answers:

3

When designing libraries, I often end up resorting to the following pattern, which I don't like as it results in lots of type-casting.

The basic pattern is:
The code using the library hands an object to the library, the library then hands the object back to the calling code. The calling code is forced to cast the object, as the library hands back a generic type. (Stripped-down code example below)

The library defines the following objects and function:

TThing = Class
End;

TThingProcessor = Class
Public
    Function  CreateThing : TThing; Virtual; Abstract;
    Procedure ProcessThing (Thing : TThing); Virtual; Abstract;
End;

Procedure DoEverything (Processor : TThingProcessor);

The calling code then uses the library by overriding the objects and calling DoEverything, as follows -

TMyThing = Class(TThing)
Public
    X : Integer;
End;

TMyThingProcessor = Class(TThingProcessor)
Public
    XSum : Integer;

    Function  CreateThing : TThing; Override;
    Procedure ProcessThing (Thing : TThing); Override;
End;

Function TMyThingProcessor.CreateThing : TThing;
Begin
    Result := TMyThing.Create;
End;

Procedure TMyThingProcessor.ProcessThing (Thing : TThing);
Begin
    XSum := XSum + (Thing As TMyThing).X;
    //Here is the problem, the caller is forced to cast to do anything
End;

The processor class is also a TThing factory. The library guarantees that it will only pass TThings to the corresponding TThingProcessor that created them, so it works, but isn't type-safe. While the code above is a bit stupid in that it doesn't really do anything, it shows why ProcessThing can't simply be shifted to TThing and be polymorphic - the XSum variable needs to be updated.

How can I restructure the code so the cast is unnecessary? I need to keep the library code separate but be able to accept any type.

Edit: Changed the hard-cast to an as-cast due to suggestion so it will at least throw exception instead of crash in the case of mismatched types

+3  A: 

You should restructure your code. If you are doing a lot of creation of 'TThing's, then it should have an ancestor class with irs process method declared as virtual, then you simply work with the ancestor. Generally it will always be possible to define a common 'base class' when you are doing similar calls to very differnt classes. If you cant create a class structure like this, use an interface to define your processing requirements and attatch this interface to your TThing.

In the last resort where you cannot avoid a cast, use the following for ease of reading and debugging...

procedure Something( ASender : TObject );
var
  MyThing : TMyThing;
begin
  MyThing := ASender as TMyThing;
  MyTHing.DoSomething;
  MyThing.DoSomethingElse;
end;
Brian Frost
I typically need all the TThings to access some common data that's stored in TThingProcessor (in this case XSum), so I can't put the process function within TThing.More generally, any addition to TThing (like attaching an interface) makes the library less generic - something I wish to avoid.
David
Another way of doing this is using "absolute"var MyThing : TMyThing absolute Sender; begin if Sender is TMyThing then....
Gerry
+1  A: 

If TMyThingProcessor will only accept TMyThing objects and basic TThing objects won't work, then don't try to do this polymorphically. Declare ProcessThing with a reintroduce; directive instead of an override; directive.

Additionally, if you have it, the Generics features of Delphi 2009 help a lot in cutting down on spurious typecasts in certain situations.

Mason Wheeler
If TMyThingProcessor.ProcessThing was declared reintroduce then DoEverything would always call the base TThingProcessor.ProcessThing, which isn't what I want. I don't have D2009 unfortunately, but I'm open to solutions that use it, as I'm more interested in the code structure than language specifics
David
Oh, I see what you mean. In that case, you're stuck casting. You're trying to mix polymorphism (literally, "many-forms-ism") with something that only works with a *single* form, and they don't fit together with out you doing a bit of work manually.
Mason Wheeler
+3  A: 

Are you using Delphi 2009? This is a great use for generics. Change your declarations to:

TThingProcessor<T: TThing> = Class
Public
    Function  CreateThing : T; Virtual; Abstract;
    Procedure ProcessThing (Thing : T); Virtual; Abstract;
End;


TMyThingProcessor = Class(TThingProcessor<TMyThing>)
Public
    XSum : Integer;

    Function  CreateThing : TMyThing; Override;
    Procedure ProcessThing (Thing : TMyThing); Override;
End;

No more casting.

Craig Stuntz
I don't have D2009 to try it, but I like the look of this. Does this mean the library would need to be distributed as source code (can't create a DLL, for example)? Also I'm assuming DoEverything needs to be generic too - would the compiler create a new version of it for every type?
David
You don't have to distribute source code. DCUs will certainly work. Packages might (haven't tried). Plain DLLs won't (can't share even non-generic objects with a plain DLL). I can't say about DoEverything without seeing the implementation.
Craig Stuntz
I was thinking DoEverything would need to be generic just from its declaration because if it was declared as accepting TThingProcessor<TThing>, then it would not accept classes derived from TThingProcessor<TMyThing>. This thinking comes from C++ templates, do Delphi generics work differently?
David
Your first sentence is correct; there is no covariance or contravariance. Delphi generics do work differently from C++ templates; they are closer to C#/.NET generics.
Craig Stuntz