3

In C#/.NET, I have a class that I want to provide extension points for. I can do this either using inheritance:

public class Animal {
    public virtual void Speak() { }
}
public class Dog : Animal {
    public overrides void Speak() => Console.WriteLine("Woof");
}
var dog = new Dog();
dog.Speak();

Or using passed-in delegates:

public class Animal {
    private Action speak;
    public Animal(Action speak) => this.speak = speak;
    public void Speak() => speak();
}
var dog = new Animal(() => Console.WriteLine("Woof"));
dog.Speak();

I can already see some differences between them:

  • Access to the base behavior -- if via inheritance, the overriding method can choose whether to invoke the base method or not; if via delegates, there is no automatic access to the base behavior.
  • Can there be no behavior? -- if via inheritance, there is always some behavior at Speak, either the base class behavior, or the derived class behavior. When using delegates, the delegate field could potentially contain null (although with nullable reference types, this shouldn't happen).
  • Explicit definition of scoped data / members -- When extending via inheritance, other members or data defined in the derived class are explicitly defined as being part of a class. When using delegates together with lambda expressions, lambda expressions can access the surrounding scope, but the parts of that scope aren't necessarily explicitly defined as such (e.g. closed-over variables).

When is it appropriate to expose extension points via inheritance, and when is it appropriate to use delegates?

5
  • 4
    You forgot the canonical third way: passing in an object which is derived from some interface - which is the probably most popular mechanics for dependency injection (though passing in a delegate can be seen as a form of dependency injection as well).
    – Doc Brown
    Apr 19, 2020 at 19:14
  • 1
    Where are all the guys living in design pattern when you need them to explain the open/closed principles?
    – gnasher729
    Apr 20, 2020 at 8:54
  • 1
    Because it sort of matters for the options presented: How much of the base class is expected to be extended/overridden? Most of it? One/two at most? Because the second option will be significantly impacted by requiring a large volume of delegates.
    – Flater
    Apr 20, 2020 at 9:44
  • This may also be interesting. Apr 20, 2020 at 11:27
  • 1
    Shortly, I would say that delegates offers more dynamic to the mechanism, ie you may even change the delegate dynamically. Apr 20, 2020 at 15:11

4 Answers 4

-1

It's worth noting that, under the covers, inheritance is implemented somewhat like the delegate style pattern you illustrate.

In a situation where inheritance is supported directly in the language, with plenty of sugar already provided, there's probably no general case for using your delegate style pattern, if you are only achieving what you can achieve with inheritance anyway.

9
  • If all I want is just the addition of a single behavior, encapsulated in a single method, then extending with inheritance feels like a bit too much ceremony, and requires an additional type to be defined and understood.
    – Zev Spitz
    Apr 19, 2020 at 20:30
  • @ZevSpitz, but you've got to go through ceremony anyway, including the definition of the delegate in the base class, the construction of the object, and you've got to bear in mind the understanding that the object can have different behaviours (but without that being indicated explicitly by the type system). That's on top of the hassle of dealing with an unconventional pattern, to achieve what can be achieved with a standard functionality.
    – Steve
    Apr 19, 2020 at 20:53
  • Since when are delegate methods “unconventional”?
    – gnasher729
    Apr 20, 2020 at 9:02
  • RE ceremony: with inheritance, I have to define a method/signature on the base type, and create a whole new type with an overridden method. For extension via delegates, all I need is a field/property/parameter of a delegate type, to which I can set or pass either a lambda expression or a method group. This feels like much less ceremony; it's a smaller API surface -- one type instead of two. Admittedly, every object instantiation requires passing / setting the delegate, but I don't think you would suggest creating a new derived type just to pre-fill the fields in the base type.
    – Zev Spitz
    Apr 20, 2020 at 10:13
  • 1
    @ZevSpitz, but analogously, you have you define a new delegate type, you have to define a storage field on the base type, you have to define the method, and on each construction you have to provide that method (which if "dogs" are a common thing, you'll end up wanting a factory method too). This is at least as heavyweight, in my view, for the type of situation you've illustrated.
    – Steve
    Apr 20, 2020 at 10:23
5

Beware: the following is giving you only a rough "rule of thumb" which is surely not a complete tutorial on how to use inheritance correctly, but you can use it as a first "litmus test" for what you were asking.

To create behavioural extension points, a popular classic approach is the strategy pattern. When you look at the pattern in full, you find both in it:

  • An abstract behaviour (the strategy) which is injected like a delegate into a context class. In your "Animal" example, this could be an interface or abstract base class ISpeakStrategy, with derivations like DogSpeakStrategy or CatSpeakStrategy, where an ISpeakStrategy object is injected into a context object Animal.

  • Inheritance to allow the extension of the set of available strategies, without touching any existing code.

Now there are situations where you may need extension points, but using the strategy pattern this way makes things more complex than necessary:

  1. when the strategy interface requires only a single method, and the name of the interface is not really important, then using a single delegate instead of a full-blown inheritance hierarchy is usually suffient

  2. when a separation between a "context" object and a strategy is not required, because the context class would be trivial / almost empty, then using a stand-alone inheritance hierarchy is sufficient.

So think of how the extension point would look like with the strategy pattern, then consider to leave out everything which overcomplicates your design.

3

Using a "delegate" method (e.g. an Action), you attain a larger degree of flexibility because the "injected" Action can be manipulated from a distance. This offers a fully decoupled behavior, so you can "fine-tune" instance behavior without ever meeting the class instance again after instantiation.

So, you can do something like the following:

//Application Root
BarkOptions barkOptions = new BarkOptions()
{
    BarkVolume = 40;
}

Action barking = () =>
{
   Console.WriteLine("Woofed at " + barkOptions.BarkVolume + " dB");

   //BarkVolume is a variable that is controlled by a distant "options" object,
   //which may be available, for example, through some Options panel in your
   //application, for the user to control. Therefore, between each call to
   //wolf.Bark(), the volume may have been changed, thus providing more control.
}

Animal wolf = new Animal(barking);

//Then, the two objects take their separate ways down your object graph.

Bear in mind that, yes, this is powerful, but sometimes, this might not be what you want, of course. If this is not what you want and you simply want to encapsulate the behaviour entirely within the class, you are clearly looking at simply creating methods and overriding them in derived classes.

Beyond that, do take Doc Brown's answer seriously, there are better options than those you propose.

2
  • I could also encapsulate the behavior you describe entirely within a separate class (e.g. Barker, with a barkOptions field), and pass the method of the instance itself (i.e. not with a lambda expression). I would then have the derived behavior encapsulated in a separate class without using inheritance. But your points still stand.
    – Zev Spitz
    Apr 20, 2020 at 12:39
  • 1
    @ZevSpitz Certainly! Your proposition is even more empowering. My general suggestion is that Func or Action objects are simply used as a convenience when you don't really want to extend them to create additional meaning (i.e. use the Type system to strong-type a dedicated class/type). Apr 20, 2020 at 12:47
0

When you write a class, you first follow the YAGNI principle: Don’t plan for extensibility because You Ain’t Gonna Need It. And because you have no clue right now how that extensibility might work.

Once you need to extend your class, you know what you are going to need. Usually the best is to add a property which might be a Boolean, or other simple value, or a delegate, or just a closure. Then modify the class to react properly to that property. Add documentation what happens if the property isn’t set (for example: if the background Color property is not set, then “white” is used).

Note that using inheritance means that you lose the freedom to modify the class without modifying its subclasses, and delegates or closures allow you to specifically address the differences between two instances without any surrounding code.

4
  • 2
    There is a reasonable balance to be struck with YAGNI. Indeed, don't spend excessive time planning for extensions you don't yet know you need; but it's equally possible that OP has a reasonable expectation of requiring extensions either down the line or now (the question does not reveal enough context to evaluate this, IMHO). OP's question seems to be premised on already knowing the former but not the latter; and if that is the case, this answer doesn't quite apply in this scenario (but it's still good advice in general even if not specifically applicable here)
    – Flater
    Apr 20, 2020 at 9:51
  • I'm trying to extract a set of shared functionality from two code bases, and while I'm still exploring precisely what needs to be extended, I do have a general idea (e.g. some way to mutate a given object into another, build an object suitable for a WPF DataContext from some serialized model etc). // using inheritance means losing the freedom to modify the class, but using delegate extension means losing the freedom to modify the field/property/parameter type. It's admittedly a smaller surface, but it's still there.
    – Zev Spitz
    Apr 20, 2020 at 10:35
  • 3
    @Flater, I agree. The YAGNI principle surely applies to purely speculative features, or when people get carried away trying to be overly general (often in a way that creates extreme complexity without actually being general). It doesn't mean failing to accommodate things which experience shows are reasonably foreseeable, or using specialist solutions when general ones will do. The dividing line is, like everything, a matter of judgment.
    – Steve
    Apr 20, 2020 at 10:56
  • 1
    "using inheritance means losing the freedom to modify the class" - I thought this was suspicious too. Delegates still need to be plumbed into the base class, and the same constraints arise between the design of one and the design of the other - for example, you can't change the delegate signature without also changing all the delegated implementations.
    – Steve
    Apr 20, 2020 at 10:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.