views:

555

answers:

7

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

+1  A: 

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;
Steve
+1  A: 

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

Jamie
I assumed he had control!
Steve
Due to his reputation I assumed he would have known that :)
Jamie
Eh reputation is not directly related to knowledge. You also get reputation for asking questions ;-).
Gamecat
Thank you Jamie ;-)
François
A: 

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.

Gabe Moothart
+1  A: 

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.

Fabricio Araujo
+4  A: 

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.

Rob Kennedy
Hmm. Delegation. He could ever transforming this on a event.... Cool, mr Kennedy.
Fabricio Araujo
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.
Gamecat
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.
François
A: 

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;
François
I don't really think this meets your requirement that the solution be "simple."
Rob Kennedy
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.
François
A: 

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.

Kcats