Search code examples
c#interfaceclass-hierarchyinterface-implementation

Alternatives to interface reimplementation


I'm reading the excerpt below from Joe Albahari's excellent "C# 9 in a Nutshell" and am trying to understand what's being described here in bold. Is anyone able to explain the alternative approach in a way that I can understand better? This seems somewhat backward to me for some reason.

Alternatives to interface reimplementation Even with explicit member implementation, interface reimplementation is problematic for a couple of reasons:

The subclass has no way to call the base class method.

The base class author might not anticipate that a method be reimplemented and might not allow for the potential consequences.

Reimplementation can be a good last resort when subclassing hasn’t been anticipated. A better option, however, is to design a base class such that reimplementation will never be required. There are two ways to achieve this:

When implicitly implementing a member, mark it virtual if appropriate.

When explicitly implementing a member, use the following pattern if you anticipate that subclasses might need to override any logic:

public class TextBox : IUndoable
{
  void IUndoable.Undo()         => Undo();    // Calls method below
  protected virtual void Undo() => Console.WriteLine ("TextBox.Undo");
}

public class RichTextBox : TextBox
{
  protected override void Undo() => Console.WriteLine("RichTextBox.Undo");
}

If you don’t anticipate any subclassing, you can mark the class as sealed to preempt interface reimplementation.


Solution

  • The issue described, is that when a base class implements an interface, that base class may expect certain behaviour to occur.

    The example of the textbox undo. Suppose that the base textbox does something important (or perhaps the actual work) for undoing. Also for the sake of example, assumen that when writing the base class, the author did not think of inheriting classes (richtextbox here)

    He could have written it as

    public class TextBox : IUndoable
    {
      void IUndoable.Undo() => ....... undo logic here
    }
    

    Any inheriting class would not be able to call Undo directly (they would have to cast its base to IUndoable ) The problem now is, when other code, use the IUndo implementation (for example a menu item), the important undo in the base class would not be called, unless the inheriting class does this explicitely

    Still the same with:

    public class TextBox : IUndoable
    {
      public void Undo() => ...... undo logic here
    }
    

    The inheriting (RichTextBox) class, could call its base when explicitly implementing Undo, but it wouldn't be sure. If RichTextBox makes its own Undo and hides the base Undo, there is no direct impulse to call the base Undo (Although more so than the first option)

    In the end, what the base class author wants, is that when external code calls the IUndoable.Undo method, the required code is always called. (Side note: In an abstract class that could be handled differently, but this is a directly usable class, that may or may not be inherited)

    Making the implicit implementation virtual helps a bit:

    public class TextBox : IUndoable
    {
      public virtual void Undo() => ...... undo logic here
    }
    

    As soon as an inheriting class overrides the method, the default snippet also calls the base class, but that is assuming the snippet is used, and base.Undo still is called, leaving the option to the inheritor.

    That gives the last example as you included it:

    public class TextBox : IUndoable
    {
      void IUndoable.Undo()         => Undo();    // Calls method below
      protected virtual void Undo() => Console.WriteLine ("TextBox.Undo");
    }
    

    This gives a more secure option for inheriting classes to implement their own undo, without losing the undo of the base class (unless the inheriting class explicitly implements IUndoable as well, but let's not go there :P )

    In the richtextbox example, the Undo is overriden, but the base Undo is not called. Exactly the sort of situation that could cause the base class implementation of 'undo' to be skipped. But here, if something (a menu item for example) calls Undo of IUndoable the explicit implementation is called, forcing the required base class undo functionality to run, but also the overriden implementation of the inheriting class (richtextbox)