Tuesday, March 1, 2011

How to force a Descendant class to either use directly the Ancestor method or have a new implementation not calling inherited?

We have the TAncestor class which has a virtual method GetFile.
We will have some TDescendant = class(TAncestor) which may override GetFile.
We want to insure that in such a case those overriden methods do not call inherited in their implementation.
But if they don't implement GetFile and just use the one from TAncestor, it's fine.
Is there a (simple) way to do this?

to make it clearer:
- yes, doc clearly says 'do not use inherited in your descendant class'
- I have no control of what others will code when overriding and don't rely on them reading the doc
- I cannot restrict the execution to the exact class TAncestor as it is legit for descendant to use it if they don't provide their own implementation
- I cannot make it abstract because it needs to have a default implementation
- I want to enforce this with a way of detecting in the base class that the code is called through a descendant overridden implementation
- Looking at the stack seems overkill but is my 1st idea so far

From stackoverflow
  • When you implement the ancestor, override the method, but don't call inherited inside the method.

    procedure TDescentdent.GetFile;
    begin
      //do not call inherited
      //Do something new
    end;
    
  • Could make TAncestor.GetFile abtract so it has to be overriden but provide a helper method for people who don't want to implement it themselves?

    Also, do you not have control over who is overriding this method? e.g. is it used by people external to your team?

    procedure TDescentdent.GetFile;
    begin
      FileUtils.GetFile    
    end;
    

    Edit: Steve is of course right if you have control over the descendant code

    Steve : I assumed he had control!
    Jamie : Due to his reputation I assumed he would have known that :)
    Gamecat : Eh reputation is not directly related to knowledge. You also get reputation for asking questions ;-).
    François : Thank you Jamie ;-)
  • I do not think you really want to do this, but in any case it doesn't sound like you're using inheritance properly.

    If you really need to do something like this, maybe you could use the strategy pattern instead? Extract GetFile into its own class hierarchy, with an abstract class (say, TAbstractFileUtils) that defines the contract, and concrete subclasses that implement it (including your default TDefaultFileUtils). The constructor could take an instance of TAbstractFileUtils. This means the caller (or a factory method) is responsible for providing the correct implementation.

    This won't prevent others from subclassing TDefaultFileUtils and calling the inherited GetFile, but that is just not how inheritance works anyway.

  • If the descendants are under your control, just override the method and not use the inherited keyword. Else, that's not much that can be done - it's up to the people overriding the GetFile method to use it's inherited method or not. Except, maybe, the Jamie's idea.

  • No. There is no way to force code outside your control to not call something that is otherwise perfectly accessible. The best you can do is strongly discourage the practice in the documentation for the class.

    What are the consequences if a descendant calls the inherited method? If it means the program stops working, then so be it. The programmer who writes the descendant class will test the code, notice that it doesn't work, and then consult the documentation for the method to ensure that he's using it correctly (at which time he'll learn he isn't).

    You could take another approach. Instead of making the function virtual, and having descendants override it, provide a protected method-pointer property.

    type
      TGetFileImpl = procedure of object;
    
      TAncestor = class
      private
        FGetFile: TGetFileImpl;
      protected
        property GetFileImpl: TGetFileImpl write FGetFile write FGetFile;
      public
        procedure GetFile; // not virtual.
      end;
    
      TDescendant = class(TAncestor)
      private
        procedure SpecializedGetFile;
      public
        constructor Create;
      end;
    
    procedure TAncestor.GetFile;
    begin
      if Assigned(GetFileImpl) then
        GetFileImpl
      else begin
        // Do default implementation instead
      end;
    end;
    
    constructor TDescendant.Create;
    begin
      GetFileImpl := SpecializedGetFile;
    end;
    

    The base class provides a method pointer that descendants can assign to indicate they want their own special handling. If the descendant provides a value for that property, then the base class's GetFile method will use it. Otherwise, it will use the standard implementation. Define TGetFileImpl to match whatever the signature of GetFile will be.

    Fabricio Araujo : Hmm. Delegation. He could ever transforming this on a event.... Cool, mr Kennedy.
    Gamecat : Also my idea, but I would have given the TGetFileImpl as an argument of the constructor (defaults to nil) and if nil uses the default. In that case it is not possible to access it any other way.
    François : Your alternative approach is probably the solution, as otherwise it's kinda misusing inheritance, which calls for design change or hack... But it's not true that you cannot"force code outside your control to not call ..." inherited. It is possible, see my solution for that.
  • Well, having to clarify the question gave me a solution:

    type
      TForm1 = class(TForm)
        btnGoodDescendant: TButton;
        btnBadDescendant: TButton;
        btnSimpleDescendant: TButton;
        procedure btnGoodDescendantClick(Sender: TObject);
        procedure btnBadDescendantClick(Sender: TObject);
        procedure btnSimpleDescendantClick(Sender: TObject);
      private
        { Private declarations }
      public
        { Public declarations }
      end;
    
      TAncestor = class
      public
        procedure GetFile; virtual;
      end;
    
      TBadDescendant = class(TAncestor)
      public
        procedure GetFile; override;
      end;
    
      TGoodDescendant = class(TAncestor)
      public
        procedure GetFile; override;
      end;
    
      TDescendant = class(TAncestor)
      public
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    {$R *.dfm}
    
    procedure TAncestor.GetFile;
    type
      TGetFileImpl = procedure of object;
    var
      BaseGetFile, GetFileImpl: TGetFileImpl;
      ClassAncestor: TClass;
    begin
      // detecting call through inherited...
      GetFileImpl := GetFile; // method actually called
      ClassAncestor := ClassType;
      while (ClassAncestor <> nil) and (ClassAncestor <> TAncestor) do
        ClassAncestor := ClassAncestor.ClassParent;
      if ClassAncestor = nil then
        raise Exception.Create('no ancestor???');
      BaseGetFile := TAncestor(@ClassAncestor).GetFile; // TAncestor code
      // if we are here, we should be directly using TAncestor code, not
      // not calling inherited from a derived class
      // thus the actual code should be exactly TAncestor code.
      if TMethod(GetFileImpl).Code <> TMethod(BaseGetFile).Code then
        raise Exception.Create('You must not call inherited!');
    
      // this is the Ancestor work code here
      ShowMessage('Ancestor code for GetFile');
    end;
    
    { TBadDescendant }
    
    procedure TBadDescendant.GetFile;
    begin
      inherited;
      ShowMessage('TBadDescendant code for GetFile');
    end;
    
    { TGoodDescendant }
    
    procedure TGoodDescendant.GetFile;
    begin
      ShowMessage('TGoodDescendant code for GetFile');
    end;
    
    procedure TForm1.btnGoodDescendantClick(Sender: TObject);
    begin
      with TGoodDescendant.Create do
        GetFile;
    end;
    
    procedure TForm1.btnBadDescendantClick(Sender: TObject);
    begin
      with TBadDescendant.Create do
        GetFile;
    end;
    
    procedure TForm1.btnSimpleDescendantClick(Sender: TObject);
    begin
      with TDescendant.Create do
        GetFile;
    end;
    
    Rob Kennedy : I don't really think this meets your requirement that the solution be "simple."
    François : Not as simple as I'd like, but ~10 lines of codes and no additional classes or design changes isn't bad for what I want. I agree that your solution was probably the best suggestion though ;-) ...at least from a design point of view.
  • Of course, no solution is perfect, for example:

    procedure TBadDescendant.GetFile;
    var
      AncestorImpl : procedure(This : TObject);
      ThisClass : TClass;
    begin
      AncestorImpl := @TAncestor.GetFile;
      ThisClass := ClassType;
      PPointer(Self)^ := TAncestor;
      AncestorImpl(Self);
      PPointer(Self)^ := ThisClass;
      ShowMessage('TBadDescendant code for GetFile');
    end;
    

    This will work if GetFile does not call other virtual methods and you don't care about concurrency.

0 comments:

Post a Comment