Domanda

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?

È stato utile?

Soluzione

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.

Altri suggerimenti

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.

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.

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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
scroll top