Honesty is overrated in accordance with ... (I do not remember the song). I think that many of us overestimate inheritance and often solve problems with inheritance too quickly and not with composition or delegation.
I really doubt the desire to add the SaveToFile method to each class that you want to save to a file.
In my opinion, classes should be unaware of responsibilities that are not the reason for their existence. Perseverance is one such responsibility, typing another. The print class must be responsible for printing. Of course, you donโt want the print class to be a network of hornets of if statements to deal with every perceived class you want to print. Thus, you define the base class of the printer and extend it using the PeoplePrinter, LocationPrinter and WhateverPrinter descendants. Each of them can deal with the entire hierarchy of classes.
If you are thinking about a decorator now, well, well noticed.
The idea is that you are not creating descendants for the existing hierarchy, but you are creating classes and possibly class hierarchies for specific responsibilities. If you want to save an instance of an existing class, instead of calling SomeClass.SaveToText , you must create an instance of TSaver and pass it an instance of the class to save.
A very naive implementation might look like this.
type TSaver = class(TObject) procedure SaveToText; virtual; abstract; end; TBaseHierarchySaver = class(TSaver) private FBase: TBaseClass; public constructor Create(aBase: TBaseClass); procedure SaveToText; override; class procedure Save(aBase: TBaseClass); end; constructor TBaseHierarchySaver.Create(aBase: TBaseClass); begin FBase := aBase; end; class procedure TBaseHierarchySaver.Save(aBase: TBaseClass); var Me: TSaver; begin Me := TBaseHierarchySaver.Create(aBase); Me.SaveToText; end; procedure TBaseHierarchySaver.SaveToText; var Str: TStrings; begin Str := TStringList.Create; try Str.Add(Format('%s (%d)', [FBase.Name, FBase.ID])); if FBase.InheritsFrom(TDerivedClass) then begin Str.Add(Format('%d', [TDerivedClass(FBase).Age])); Str.Add(Format('%s', [TDerivedClass(FBase).Address])); end; finally Str.SaveToFile('SomeFileName'); Str.Free; end; end;
I donโt really like it. It's fragile. We can do better.
There are many ways to make the above code more flexible and / or to provide polymorphic execution. For example, TSaver may have a dictionary of anonymous methods associated with TBaseClass classes. TSaver.SaveToText can then receive the TBaseClass argument and be implemented to execute each anonymous method for the class of the instance passed to it, if it inherits from the class associated with this anonymous method.
type TBaseClassClass = class of TBaseClass; TAddInfoProc = reference to procedure(aBase: TBaseClass; aStr: TStrings); TSaver = class(TObject) class var FAddInfoClasses: TDictionary<TBaseClassClass, TAddInfoProc>; public class procedure RegisterAddInfoProc(aBase: TBaseClassClass; aAddInfo: TAddInfoProc); class procedure SaveToText(aBase: TBaseClass); end; TSaver.RegisterAddInfoProc(TBaseClass, procedure(aBase: TBaseClass; aStr: TStrings) begin aStr.Add(Format('%s (%d)', [aBase.Name, aBase.ID])); end ); TSaver.RegisterAddInfoProc(TDerivedClass, procedure(aBase: TBaseClass; aStr: TStrings) begin aStr.Add(Format('%d', [TDerivedClass(FBase).Age])); aStr.Add(Format('%s', [TDerivedClass(FBase).Address])); end );
This frees you from inheritance hierarchies, but if you want to perform polymorphic execution, it can be changed to a dictionary linking specific TBaseClass descendants to the corresponding AddInfo descendant hierarchy, where each AddInfo descendant adds its own information:
type TAddInfo = class(TObject) public procedure AddInfo(aBase: TBaseClass; aStr: TStrings); virtual; end; TDerivedAddInfo = class(TAddInfo) public procedure AddInfo(aBase: TBaseClass; aStr: TStrings); override; end; procedure TAddInfo.AddInfo(aBase: TBaseClass; aStr: TStrings); begin aStr.Add(Format('%s (%d)', [aBase.Name, aBase.ID])); end; procedure TDerivedAddInfo.AddInfo(aBase: TBaseClass; aStr: TStrings); var Derived: TDerivedClass absolute aBase; begin inherited; if not aBase.InheritsFrom(TDerivedClass) then Exit; aStr.Add(Format('%d', [Derived.Age])); aStr.Add(Format('%s', [Derived.Address])); end; type TBaseClassClass = class of TBaseClass; TAddInfoClass = class of TAddInfo; TSaver = class(TObject) class var FAddInfoClasses: TDictionary<TBaseClassClass, TAddInfoClass>; public class procedure RegisterAddInfoClass(aBase: TBaseClassClass; aAddInfo: TAddInfoClass); class procedure SaveToText(aBase: TBaseClass); end;
Which, by the way, is very similar to the helper class method proposed elsewhere, but without limiting the presence of only one class helper at any given time. Thus, you can have TSaver, TPrinter, TMailer and everything else that you would like to do with TBaseClass, which is not its main responsibility.
Oh, by the way, the above use of the absolute is one of the few use cases for the absolute that I can handle. This is a convenient short arm for hard casting, which becomes safe thanks to an early exit restriction, which in itself is also one of the few use cases for early exits that I can handle :-)