What are good uses for class helpers?
Asked Answered
P

10

37

Delphi (and probably a lot of other languages) has class helpers. These provide a way to add extra methods to an existing class. Without making a subclass.

So, what are good uses for class helpers?

Personalize answered 31/10, 2008 at 13:7 Comment(0)
E
34

I'm using them:

  • To insert enumerators into VCL classes that don't implement them.
  • To enhance VCL classes.
  • To add methods to the TStrings class so I can use the same methods in my derived lists and in TStringList.

    TGpStringListHelper = class helper for TStringList
    public
      function  Last: string;
      function  Contains(const s: string): boolean;
      function  FetchObject(const s: string): TObject;
      procedure Sort;
      procedure Remove(const s: string);
    end; { TGpStringListHelper }
    
  • To simplify access to record fields and remove casting.

Eisenhart answered 31/10, 2008 at 13:30 Comment(0)
P
14

At first I was kind of sceptic about class helpers. But then I read an interesting blog entry and now I'm convinced that they are indeed useful.

For example, if you want extra functionality for an existing instance class and for some reason you are not able to change the existing source. You can create a class helper to add this functionality.

Example:

type
  TStringsHelper = class helper for TStrings
  public
    function IsEmpty: Boolean;
  end;

function TStringsHelper.IsEmpty: Boolean;
begin
  Result := Count = 0;
end;

Every time, we now use an instance of (a subclass of) TStrings, and TStringsHelper is within the scope. We have access to the method IsEmpty.

Example:

procedure TForm1.Button1Click(Sender: TObject);
begin
  if Memo1.Lines.IsEmpty then
    Button1.Caption := 'Empty'
  else
    Button1.Caption := 'Filled';
end;

Notes:

  • Class helpers can be stored in a separate unit, so you can add your own nifty class helpers. Be sure to give these units a easy to remember name like ClassesHelpers for helpers for the Classes unit.
  • There are also record helpers.
  • If there are multiple class helpers within scope, expect some problems, only one helper can be used.
Personalize answered 31/10, 2008 at 13:7 Comment(4)
Have you read the comment: "The biggest problem with class helpers, from the p.o.v of using them in your own applications, is the fact that only ONE class helper for a given class may be in scope at any time." ... "That is, if you have two helpers in scope, only ONE will be recognised by the compiler. You won't get any warnings or even hints about any other helpers that may be hidden."Ouzel
CLASS helpers can inherit from other CLASS helpers, so you can - in effect - have multiple CLASS helpers active at the same time (but they must be aware of each other, and you thus can only have a continous "string" of CLASS helpers active, ie. if you have Helper1, and Helper2 inherits from Helper1, and Helper3 inherits from Helper2, then you can't only have Helper3 active (without some conditional compilation directives), but must have all three helpers in scope - you can have only Helper1, only Helper1+Helper2 or all three active simultaneously - no Helper1+Helper3 only).Terribly
"Class helpers can inherit from one another"- this invalidating the entire virtue of class helpers.Inwrought
Modified the blog link to use archive.org page.Calloway
C
6

This sounds very much like extension methods in C#3 (and VB9). The best use I've seen for them is the extensions to IEnumerable<T> (and IQueryable<T>) which lets LINQ work against arbitrary sequences:

var query = someOriginalSequence.Where(person => person.Age > 18)
                                .OrderBy(person => person.Name)
                                .Select(person => person.Job);

(or whatever, of course). All of this is doable because extension methods allow you to effectively chain together calls to static methods which take the same type as they return.

Cabanatuan answered 31/10, 2008 at 13:18 Comment(4)
As a side note, Delphi has had class helpers since 2000, and we hold the patent on the notion. Nick Hodges Delphi Product Manager Embarcadero TechnologiesPhenformin
@Nick: Which version of Delphi are you talking about, that had class helpers since 2000? I only remember reading about them when they were used in Delphi 2007 to make a non-breaking release. That was 6 years after the year you claim that Delphi had them.Wyant
Class helpers were added to Delphi in Delphi 8 (the first version of Delphi for .NET).Could
@NickHodges Did you also patent the inability for more than one helper to be active at once?Kadner
P
4

They're very useful for plug-ins. For example, let's say your project defines a certain data structure and it's saved to disc in a certain way. But then some other program does something very similar, but the data file's different. But you don't want to bloat your EXE with a bunch of import code for a feature that a lot of your users won't need to use. You can use a plugin framework and put importers into a plugin that would work like this:

type
   TCompetitionToMyClass = class helper for TMyClass
   public
      constructor Convert(base: TCompetition);
   end;

And then define the converter. One caveat: a class helper is not a class friend. This technique will only work if it's possible to completely setup a new TMyClass object through its public methods and properties. But if you can, it works really well.

Parbuckle answered 31/10, 2008 at 20:37 Comment(0)
O
4

I would not recommend to use them, since I read this comment:

"The biggest problem with class helpers, from the p.o.v of using them in your own applications, is the fact that only ONE class helper for a given class may be in scope at any time." ... "That is, if you have two helpers in scope, only ONE will be recognised by the compiler. You won't get any warnings or even hints about any other helpers that may be hidden."

http://davidglassborow.blogspot.com/2006/05/class-helpers-good-or-bad.html

Ouzel answered 4/7, 2009 at 5:57 Comment(3)
I have read that comment. But that's no reason not to use them. Just use them carefully like any other tool.Personalize
@GamecatisToonKrijthe not even hints, this means developers now have to scan all sources of third party libraries (no only once but for every update too) to detect "conflict helpers" ...Ouzel
This is the correct answer. Helpers are not Delphi's version of C# extension methods. Helpers are not a general programming solution, and should not be used in any new code. If you try to declare a helper where one already existed on a class (e.g. TGuidHelper), then you break existing code. I would LOVE it if Delphi supported Extension methods; it would be an EXTRAORDINARILY useful language feature. But helpers are not that feature.Inwrought
P
3

The first time I remember experiencing what you're calling "class helpers" was while learning Objective C. Cocoa (Apple's Objective C framework) uses what are called "Categories."

A category allows you to extend an existing class by adding you own methods without subclassing. In fact Cocoa encourages you to avoid subclassing when possible. Often it makes sense to subclass, but often it can be avoided using categories.

A good example of the use of a category in Cocoa is what's called "Key Value Code (KVC)" and "Key Value Observing (KVO)."

This system is implemented using two categories (NSKeyValueCoding and NSKeyValueObserving). These categories define and implement methods that can be added to any class you want. For example Cocoa adds "conformance" to KVC/KVO by using these categories to add methods to NSArray such as:

- (id)valueForKey:(NSString *)key

NSArray class does not have either a declaration nor an implementation of this method. However, through use of the category. You can call that method on any NSArray class. You are not required to subclass NSArray to gain KVC/KVO conformance.

NSArray *myArray = [NSArray array]; // Make a new empty array
id myValue = [myArray valueForKey:@"name"]; // Call a method defined in the category

Using this technique makes it easy to add KVC/KVO support to your own classes. Java interfaces allow you to add method declarations, but categories allow you to also add the actual implementations to existing classes.

Pascal answered 31/10, 2008 at 14:10 Comment(0)
F
3

As GameCat shows, TStrings is a good candidate to avoid some typing:

type
  TMyObject = class
  public
    procedure DoSomething;
  end;

  TMyObjectStringsHelper = class helper for TStrings
  private
    function GetMyObject(const Name: string): TMyObject;
    procedure SetMyObject(const Name: string; const Value: TMyObject);
  public
    property MyObject[const Name: string]: TMyObject read GetMyObject write SetMyObject; default;
  end;

function TMyObjectStringsHelper.GetMyObject(const Name: string): TMyObject;
var
  idx: Integer;
begin
  idx := IndexOf(Name);
  if idx < 0 then
    result := nil
  else
    result := Objects[idx] as TMyObject;
end;

procedure TMyObjectStringsHelper.SetMyObject(const Name: string; const Value:
    TMyObject);
var
  idx: Integer;
begin
  idx := IndexOf(Name);
  if idx < 0 then
    AddObject(Name, Value)
  else
    Objects[idx] := Value;
end;

var
  lst: TStrings;
begin
  ...
  lst['MyName'] := TMyObject.Create; 
  ...
  lst['MyName'].DoSomething;
  ...
end;

Did you ever need to access multi line strings in the registry?

type
  TRegistryHelper = class helper for TRegistry
  public
    function ReadStrings(const ValueName: string): TStringDynArray;
  end;

function TRegistryHelper.ReadStrings(const ValueName: string): TStringDynArray;
var
  DataType: DWord;
  DataSize: DWord;
  Buf: PChar;
  P: PChar;
  Len: Integer;
  I: Integer;
begin
  result := nil;
  if RegQueryValueEx(CurrentKey, PChar(ValueName), nil, @DataType, nil, @DataSize) = ERROR_SUCCESS then begin
    if DataType = REG_MULTI_SZ then begin
      GetMem(Buf, DataSize + 2);
      try
        if RegQueryValueEx(CurrentKey, PChar(ValueName), nil, @DataType, PByte(Buf), @DataSize) = ERROR_SUCCESS then begin
          for I := 0 to 1 do begin
            if Buf[DataSize - 2] <> #0 then begin
              Buf[DataSize] := #0;
              Inc(DataSize);
            end;
          end;

          Len := 0;
          for I := 0 to DataSize - 1 do
            if Buf[I] = #0 then
              Inc(Len);
          Dec(Len);
          if Len > 0 then begin
            SetLength(result, Len);
            P := Buf;
            for I := 0 to Len - 1 do begin
              result[I] := StrPas(P);
              Inc(P, Length(P) + 1);
            end;
          end;
        end;
      finally
        FreeMem(Buf, DataSize);
      end;
    end;
  end;
end;
Farland answered 31/10, 2008 at 14:27 Comment(2)
Wow, overriding 'default'! Incredible. And possibly a great source of weird errors :)Eisenhart
Agreed, that is a little bit off. But I use class helpers mostly in tight scope, so this has not been an issue - up to now...Farland
F
1

Other languages have properly designed class helpers.

Delphi has class helpers that were introduced solely to help the Borland engineers with a compatibility problem between Delphi and Delphi.net.

They were never intended to be used in "user" code and have not been improved since. They can be helpful if developing frameworks (for private use within the framework, as with the original .NET compatibility solution); it is dangerously misguided to equate Delphi class helpers with those in other languages or to draw on examples from those other languages in an effort to identify use cases for those in Delphi.

To this day, the current Delphi documentation has this to say about class and record helpers:

they should not be viewed as a design tool to be used when developing new code

ref: https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Class_and_Record_Helpers_(Delphi)

So the answer to the question "what are the good uses for class helpers" in Delphi specifically is quite simple:

There is only one safe use: For context-specific extensions of utility and interest only in the single codebase that implements and consumes the helper (detailed example here: https://www.deltics.co.nz/blog/posts/683).

The example is a framework for restful API where extensions to a class of interest only to client-side code are provided by "Client Helper" extensions, explicitly imported from client-specific units rather than (over)loading client-concerns into an original class with both server and client context.

Other than that: Do not use them at all (either implementing your own or consuming those provided by others) unless you are prepared to deal with the consequences:

Primarily: Only one helper can be in scope at any time

Secondarily: There is no way to qualify helper referenced

Because of the primary problem:

  1. Adding a unit to (or even just changing the order of) the units in a uses clause may inadvertently "hide" a helper needed in your code (you may not even know where from)

  2. A helper added to a unit already in your uses list could hide some other helper previously "imported" and used from another

And thanks to the secondary problem, if you are unable to re-order the uses list to make a desired helper "visible" or you need 2 unrelated helpers (unaware of each other and so unable to "extent" one another), then there is no way to use it!

Worth emphasising here is that the ability of Delphi class helpers to break other people's code is an almost uniquely bad characteristic. Many language features in many languages can be abused to break your own code; not many enable you to break someone else's!

More details in various posts here: https://www.deltics.co.nz/blog/?s=class+helpers

Particularly this one: https://www.deltics.co.nz/blog/posts/273/

Faunia answered 20/12, 2022 at 3:48 Comment(0)
H
0

I've seen them used for making available class methods consistent across classes: Adding Open/Close and Show/Hide to all classes of a given "type" rather than only Active and Visible properties.

Hersh answered 27/1, 2009 at 11:32 Comment(0)
I
0

If Dephi supported extension methods, one use i want is:

TGuidHelper = class
public
   class function IsEmpty(this Value: TGUID): Boolean;
end;

class function TGuidHelper(this Value: TGUID): Boolean;
begin
   Result := (Value = TGuid.Empty);
end;

So i can call if customerGuid.IsEmpty then ....

Another good example is to be able to read values from an XML document (or JSON if you're into that sort of thing) with the IDataRecord paradigm (which i love):

orderGuid := xmlDocument.GetGuid('/Order/OrderID');

Which is much better than:

var
   node: IXMLDOMNode;

   node := xmlDocument.selectSingleNode('/Order/OrderID');
   if Assigned(node) then
      orderID := StrToGuid(node.Text) //throw convert error on empty or invalid
   else
      orderID := TGuid.Empty; // "DBNull" becomes the null guid
Inwrought answered 3/11, 2021 at 15:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.