đ Mastering the Strategy Pattern in C#:
Stop using inheritance for behavior.
If there is one lesson that elevates a developer from âJuniorâ to âSenior,â itâs learning to prefer Composition over Inheritance. The Strategy Pattern is the ultimate manifestation of this principle.
In this deep dive, we are going to look at a common scenarioâbuilding a car manufacturing system. We will see how a naive approach traps us in a web of dependencies (The âGod Classâ), and how the Strategy Pattern acts as a âPlugin Systemâ to restore sanity, testability, and flexibility.
1. The Domain Scenario
Imagine we are building the software backbone for a futuristic automotive group. We have three manufacturers:
Company A: Makes standard economy cars.
Company B: Makes hybrids.
Company C: An experimental division working on flying cars and AI.
The Challenge: We need to model these cars. Some fly, some donât. Some use petrol, some use electric, some are hybrid. New features (like âAutonomous Modeâ) are coming next year.
2. The Naive Approach (The Trap)
The instinctual approach is to use inheritance. We create a base Car class and shove every possible feature into it.
public abstract class Car
{
// Core features
public void Drive() { /* ... */ }
// The Problem: Anticipating every future feature in the base class
public virtual void Fly() { /* Default implementation? Empty? Throw Exception? */ }
public virtual void ActivateHybrid() { /* ... */ }
public virtual void AutonomousPark() { /* ... */ }
}Why this fails (The âGod Classâ Anti-Pattern)
As soon as you create EconomyCar (which canât fly) or VintageCar (which isnât hybrid), you hit a wall.
Polluted Interface:
EconomyCartechnically has a.Fly()method. This is confusing for other developers using your code.Liskov Substitution Violation: You might override
Fly()to throw aNotImplementedException. Now, any code iterating through a list ofCarobjects and calling.Fly()risks crashing the application.Fragile Base Class: If
Company Cneeds a change to theFlylogic, you might edit the base class, accidentally breakingCompany Aâs code.
3. The Solution: The Strategy Pattern
The Strategy Pattern suggests: Take what varies and encapsulate it.
Instead of the Car being the behavior, the Car has a behavior. We break the monolithic Car class into a host and a set of pluggable components.
Step 1: Define the Interfaces (The Sockets)
We identify the behaviors that vary: Flying and Powertrain.
// The contract for any flying behavior
public interface IFlyStrategy
{
void Fly();
}
// The contract for any engine behavior
public interface IPowertrainStrategy
{
void Drive();
}Step 2: Implement the Strategies (The Plugs)
We create concrete classes for every variation. These are small, easy to test, and isolated.
// --- Flying Strategies ---
public class JetPropulsion : IFlyStrategy
{
public void Fly() => Console.WriteLine(âđ Thrusters engaged! We are flying.â);
}
public class NoFly : IFlyStrategy
{
public void Fly() => Console.WriteLine(âđŤ This car cannot fly.â);
}
// --- Powertrain Strategies ---
public class PetrolEngine : IPowertrainStrategy
{
public void Drive() => Console.WriteLine(ââ˝ Vroom! Driving on petrol.â);
}
public class ElectricEngine : IPowertrainStrategy
{
public void Drive() => Console.WriteLine(â⥠Silent but deadly. Driving on electric.â);
}Step 3: The Context (The Car)
The Car class no longer implements the logic. It simply holds references to the interfaces and delegates the work.
public abstract class Car
{
// Composition: The Car HAS strategies
private IFlyStrategy _flyStrategy;
private IPowertrainStrategy _powertrainStrategy;
// We can inject strategies via constructor
protected Car(IFlyStrategy flyStrategy, IPowertrainStrategy powertrainStrategy)
{
_flyStrategy = flyStrategy;
_powertrainStrategy = powertrainStrategy;
}
// Dynamic Swapping: We can change behavior at runtime!
public void SetFlyStrategy(IFlyStrategy newStrategy) => _flyStrategy = newStrategy;
// Delegation: âI donât know how to fly, I ask my strategy to do it.â
public void PerformFly() => _flyStrategy.Fly();
public void PerformDrive() => _powertrainStrategy.Drive();
}4. Putting It Together
Now, creating specific car models is just a matter of âconfiguringâ them with the right LEGO blocks.
// Company A: Standard Petrol Car
public class CityCommuter : Car
{
public CityCommuter() : base(new NoFly(), new PetrolEngine()) { }
}
// Company C: Experimental Flying Car
public class SkyRacer : Car
{
public SkyRacer() : base(new JetPropulsion(), new ElectricEngine()) { }
}The âAha!â Moment: Runtime Swapping
Because we rely on interfaces, we can change a carâs behavior while the program is running. Imagine a video game where a car picks up a âFlight Power-upâ:
var myCar = new CityCommuter();
myCar.PerformFly(); // Output: âThis car cannot fly.â
// PLAYER PICKS UP POWER-UP
Console.WriteLine(â--- UPGRADE APPLIED ---â);
myCar.SetFlyStrategy(new JetPropulsion());
myCar.PerformFly(); // Output: âThrusters engaged! We are flying.â5. Why This is âClean Codeâ (SOLID Analysis)
The Strategy Pattern is not just about organizing code; it is about organizing change.
Naive Code: Resists change. New features break old features.
Strategy Code: Welcomes change. New features are just new classes waiting to be plugged in.
By decoupling the What (The Car) from the How (The Engine/Wings), you build a system that can evolve as fast as your business requirements.
refer the GITHUB repo for this pattern - the above implementation may differ https://github.com/DileepSreepathi/StrategyDesignPattern


