Replacing Inheritance with Strategy Pattern and Dependency Injection

dependency-injectiondesign-patternsstrategy-pattern

For example:

var duckBehaviors = new Duckbehavior();
duckBehaviors.quackBehavior = new Quack();
duckBehaviors.flyBehavior = new FlyWithWings();
Duck mallardDuck = new Duck(DuckTypes.MallardDuck, duckBehaviors)

As the Duck class contains all the behaviors(abstract), creating a new class MallardDuck (which extends Duck) does not seem to be required.

Reference: Head First Design Pattern, Chapter 1.

Best Answer

Sure, but we call that composition and delegation. The Strategy Pattern and Dependency Injection might seem structurally similar but their intents are different.

The Strategy Pattern allows runtime modification of behavior under the same interface. I could tell a mallard duck to fly and watch it fly-with-wings. Then swap it out for a jet pilot duck and watch it fly with Delta airlines. Doing that while the program is running is a Strategy Pattern thing.

Dependency Injection is a technique to avoid hard coding dependencies so they can change independently without requiring clients to be modified when they change. Clients simply express their needs without knowing how they will be met. Thus how they are met is decided elsewhere (typically in main). You don't need two ducks to make use of this technique. Just something that uses a duck without knowing or caring which duck. Something that doesn't build the duck or go looking for it but is perfectly happy to use whatever duck you hand it.

If I have a concrete duck class I can have it implement it's fly behavior. I could even have it switch behaviors from fly-with-wings to fly-with-Delta based on a state variable. That variable could be a boolean, an int, or it could be a FlyBehavior that has a fly method that does whatever flying style without me having to test it with an if. Now I can change flying styles without changing duck types. Now Mallards can become pilots. This is composition and delegation. The duck is composed of a FlyBehavior and it can delegate flying requests to it. You can replace all your duck behaviors at once this way, or hold something for each behavior, or any combination in between.

This gives you all the same powers that inheritance has except one. Inheritance lets you express what Duck methods you're overriding in the Duck subtypes. Composition and delegation requires the Duck to explicitly delegate to subtypes from the start. This is far more flexible but it involves more keyboard typing and Duck has to know it's happening.

However, many people believe that inheritance has to be explicitly designed for from the beginning. And that if it hasn't been, that you should mark your classes as sealed/final to disallow inheritance. If you take that view then inheritance really has no advantage over composition and delegation. Because then either way you have to either design for extensibility from the start or be willing to tear things down later.

Tearing things down is actually a popular option. Just be aware that there are cases where it's a problem. If you've independently deployed libraries or modules of code that you don't intend to update with the next release you can end up stuck dealing with versions of classes that know nothing about what you're up to now.

While being willing to tear things down later can free you from over designing there is something very powerful about being able to design something that uses a duck without having to know what the duck will actually do when used. That not knowing is powerful stuff. It lets you stop thinking about ducks for awhile and think about the rest of your code.

"Can we" and "should we" are different questions. Favor Composition over Inheritance doesn't say never use inheritance. There are still cases where inheritance makes the most sense. I'll show you my favorite example:

public class LoginFailure : System.ApplicationException {}

Inheritance lets you create exceptions with more specific, descriptive names in only one line.

Try doing that with composition and you'll get a mess. Also, there is no risk of the inheritance yo-yo problem because there is no data or methods here to reuse and encourage inheritance chaining. All this adds is a good name. Never underestimate the value of a good name.

Related Topic