Class Helper for generic class?
Asked Answered
T

3

11

I'm using Delphi 2009. Is it possible to write a class helper for a generic class, i.e. for TQueue . The obvious

TQueueHelper <T> = class helper of TQueue <T>
  ...
end;

does not work, nor does

TQueueHelper = class helper of TQueue
  ...
end;
Tyrannous answered 21/10, 2009 at 11:27 Comment(0)
M
13

As documented in the Delphi help, class helpers are not designed for general purpose use and they are incorrectly perceived as having a number of limitations or even bugs as a result.

nevertheless there is a perception - incorrect and dangerous in my view - that these are a legitimate tool in the general purpose "toolkit". I have blogged about why this is wrong and subsequently about how you can go some way to mitigate the dangers by following a socially responsible coding pattern (although even this isn't bullet proof).

You can achieve much the effect of a class helper without any of these bugs or limitations or (most importantly) risks by using a hard cast to a "pseudo" class derived from the class you are trying to extend. i.e instead of:

TFooHelper = class helper for TFoo
  procedure MyHelperMethod;
end;

use

TFooHelper = class(TFoo)
  procedure MyHelperMethod;
end;

Just like with a "formal" helper, you never instantiate this TFooHelper class, you use it solely to mutate the TFoo class, except in this case you have to be explicit. In your code when you need to use some instance of a TFoo using your "helper" methods you then have to hard cast:

   TFooHelper(someFoo).MyHelperMethod;

Downsides:

  1. you have to stick to the same rules that apply to helpers - no member data etc (not really a downside at all, except that the compiler won't "remind you").

  2. you have to explicitly cast to use your helper

  3. If using a helper to expose protected members you have to declare the helper in the same unit that you use it (unless you expose a public method which exposes the required protected members)

Advantages:

  1. Absolutely NO risk that your helper will break if you start using some other code that "helps" the same base class

  2. The explicit typecasting makes it clear in your "consumer" code that you are working with the class in a way that is not directly supported by the class itself, rather than fudging and hiding that fact behind some syntactic sugar.

It's not as "clean" as a class helper, but in this case the "cleaner" approach is actually just sweeping the mess under the rug and if someone disturbs the rug you end up with a bigger mess than you started with.

Maisiemaison answered 21/10, 2009 at 18:33 Comment(2)
+1 Interesting. Perhaps I was too enthusiastic about them. It seemed very elegant to put your own helper functions into the VCL stuff. The cast makes it less elegant and readable unfortunately.Tyrannous
To me "readable" is as much - if not more - about being able to clearly see what is going on as it is about reducing the amount of typing. When I see a hard cast being applied using some class "TxxxHelper" then I know that a bit of monkey business is afoot. Monkey business that I absolutely may need to be aware of if not now then in the future when I've perhaps forgotten that "MyMethod" is not part of the class that it appears to be and can't figure out why some other code that tries to use it won't compile (i.e. I don't have the right helper in scope or is being "obscured" by another).Maisiemaison
M
15

I currently still use Delphi 2009 so I thought I'd add a few other ways to extend a generic class. These should work equally well in newer versions of Delphi. Let's see what it would look like to add a ToArray method to a List class.

Interceptor Classes

Interceptor classes are classes that are given the same name as the class they inherit from:

TList<T> = class(Generics.Collections.TList<T>)
public
  type
    TDynArray = array of T;
  function ToArray: TDynArray;
end;

function TList<T>.ToArray: TDynArray;
var
  I: Integer;
begin
  SetLength(Result, self.Count);
  for I := 0 to Self.Count - 1 do
  begin
    Result[I] := Self[I];
  end;
end;

Notice you need to use the fully qualified name, Generics.Collections.TList<T> as the ancestor. Otherwise you'll get E2086 Type '%s' is not completely defined.

The advantage of this technique is that your extensions are mostly transparent. You can use instances of the new TList anywhere the original was used.

There are two disadvantages to this technique:

  • It can cause confusion for other developers if they aren't aware that you've redefined a familiar class.
  • It can't be used on a sealed class.

The confusion can be mitigated by careful unit naming and avoiding use of the "original" class in the same place as your interceptor class. Sealed classes aren't much of a problem in the rtl/vcl classes supplied by Embarcadero. I only found two sealed classed in the entire source tree: TGCHandleList(only used in the now defunct Delphi.NET) and TCharacter. You may run into issues with third party libraries though.

The Decorator Pattern

The decorator pattern lets you extend a class dynamically by wrapping it with another class that inherits its public interface:

TArrayDecorator<T> = class abstract(TList<T>)
public
  type
    TDynArray = array of T;
  function ToArray: TDynArray; virtual; abstract;
end;

TArrayList<T> = class(TArrayDecorator<T>)
private
  FList: TList<T>;
public
  constructor Create(List: TList<T>);
  function ToArray: TListDecorator<T>.TDynArray; override;
end;

function TMyList<T>.ToArray: TListDecorator<T>.TDynArray;
var
  I: Integer;
begin
  SetLength(Result, self.Count);
  for I := 0 to Self.Count - 1 do
  begin
    Result[I] := FList[I];
  end;
end;

Once again there are advantages and disadvantages.

Advantages

  • You can defer introducing the new functionally until its actually needed. Need to dump a list to an array? Construct a new TArrayList passing any TList or a descendant as a parameter in the constructor. When you're done just discard the TArrayList.
  • You can create additional decorators that add more functionality and combine decorators in different ways. You can even use it to simulate multiple inheritance, though interfaces are still easier.

Disadvantages

  • It's a little more complex to understand.
  • Applying multiple decorators to an object can result in verbose constructor chains.
  • As with interceptors you can't extend a sealed class.

Side Note

So it seems that if you want to make a class nearly impossible to extend make it a sealed generic class. Then class helpers can't touch it and it can't be inherited from. About the only option left is wrapping it.

Methane answered 15/9, 2011 at 20:55 Comment(0)
M
13

As documented in the Delphi help, class helpers are not designed for general purpose use and they are incorrectly perceived as having a number of limitations or even bugs as a result.

nevertheless there is a perception - incorrect and dangerous in my view - that these are a legitimate tool in the general purpose "toolkit". I have blogged about why this is wrong and subsequently about how you can go some way to mitigate the dangers by following a socially responsible coding pattern (although even this isn't bullet proof).

You can achieve much the effect of a class helper without any of these bugs or limitations or (most importantly) risks by using a hard cast to a "pseudo" class derived from the class you are trying to extend. i.e instead of:

TFooHelper = class helper for TFoo
  procedure MyHelperMethod;
end;

use

TFooHelper = class(TFoo)
  procedure MyHelperMethod;
end;

Just like with a "formal" helper, you never instantiate this TFooHelper class, you use it solely to mutate the TFoo class, except in this case you have to be explicit. In your code when you need to use some instance of a TFoo using your "helper" methods you then have to hard cast:

   TFooHelper(someFoo).MyHelperMethod;

Downsides:

  1. you have to stick to the same rules that apply to helpers - no member data etc (not really a downside at all, except that the compiler won't "remind you").

  2. you have to explicitly cast to use your helper

  3. If using a helper to expose protected members you have to declare the helper in the same unit that you use it (unless you expose a public method which exposes the required protected members)

Advantages:

  1. Absolutely NO risk that your helper will break if you start using some other code that "helps" the same base class

  2. The explicit typecasting makes it clear in your "consumer" code that you are working with the class in a way that is not directly supported by the class itself, rather than fudging and hiding that fact behind some syntactic sugar.

It's not as "clean" as a class helper, but in this case the "cleaner" approach is actually just sweeping the mess under the rug and if someone disturbs the rug you end up with a bigger mess than you started with.

Maisiemaison answered 21/10, 2009 at 18:33 Comment(2)
+1 Interesting. Perhaps I was too enthusiastic about them. It seemed very elegant to put your own helper functions into the VCL stuff. The cast makes it less elegant and readable unfortunately.Tyrannous
To me "readable" is as much - if not more - about being able to clearly see what is going on as it is about reducing the amount of typing. When I see a hard cast being applied using some class "TxxxHelper" then I know that a bit of monkey business is afoot. Monkey business that I absolutely may need to be aware of if not now then in the future when I've perhaps forgotten that "MyMethod" is not part of the class that it appears to be and can't figure out why some other code that tries to use it won't compile (i.e. I don't have the right helper in scope or is being "obscured" by another).Maisiemaison
B
8

As near as I can tell, there's no way to put a class helper on a generic class and have it compile. You ought to report that to QC as a bug.

Begat answered 21/10, 2009 at 11:35 Comment(2)
Yeah, I'd kinda like to know that myself... ;)Begat
It wasn't me, but I imagine it was because if it's by design then it isn't a bug. Just as the class helper implementation was originally "incomplete" in comparison with the Delphi.NET equivalent. Some considered that a bug, but CodeGear's official response was "by design" - the fact that class helpers don't support things they aren't intended to be used for isn't "a bug". Asking for them to do more than they are currently designed for is an enhancement request. :)Maisiemaison

© 2022 - 2024 — McMap. All rights reserved.