4

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

2
  • I think that stackoverflow.com/questions/681522/…" might have some bearing on what you're trying to do Commented Mar 30, 2009 at 9:18
  • Thanks for the link, it was useful reading. I don't think it applies directly but thinking about restructuring in a similar way is throwing up ideas for me, though nothing I've been able to pin down yet Commented Apr 2, 2009 at 0:39

3 Answers 3

3

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.

Sign up to request clarification or add additional context in comments.

4 Comments

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?
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.
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?
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.
3

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;

2 Comments

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.
Another way of doing this is using "absolute" var MyThing : TMyThing absolute Sender; begin if Sender is TMyThing then....
1

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.

2 Comments

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
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.