The Tale of Ducks with Special Abilities: Understanding the Strategy Pattern
Imagine you're in charge of a pond where different types of ducks live. Each duck species has unique abilities. For example, the Wood Duck can fly and quack, while the Pekin Duck can quack but doesn’t fly much.
Initially, you might think of organizing the ducks using a class hierarchy where all ducks inherit common behaviors like flying and quacking. But soon, you realize this approach creates problems, especially when certain ducks change behavior or don't fit perfectly into the hierarchy. This is where the Strategy Pattern comes to the rescue.
The Strategy Pattern: What Is It?
The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable.
This means that instead of hardcoding a behavior directly into the duck class, we break it out into separate classes (strategies), making it easy to change or replace behaviors without touching the core duck code.
Encapsulating Behavior into Strategies (Defining a Set of Algorithms)
In our duck example, flying and quacking are the behaviors (algorithms) that can vary between different ducks. With the Strategy Pattern, we create separate classes for each unique behavior, and we can dynamically assign them to different duck species.
These behaviors (algorithms) include:
- Flying with wings (for ducks like the Wood Duck).
- Not being able to fly (for ducks like the Pekin Duck).
- Quacking loudly (for both Wood Duck and Pekin Duck).
- Staying silent (for ducks that don’t quack).
Here’s how we define these algorithms:
Flying Behavior Interface: This defines a general way for ducks to fly.
class FlyBehavior {
public:
virtual void fly() = 0;
};
Quacking Behavior Interface: This defines how ducks can quack.
class QuackBehavior {
public:
virtual void quack() = 0;
};
Next, we create concrete strategies (algorithms) that represent the specific ways ducks can fly or quack.
Flying Behaviors (Set of Algorithms):
We define two types of flying behaviors: one for ducks that can fly and one for ducks that can’t.
class FlyWithWings : public FlyBehavior {
public:
void fly() override {
cout << "Flying with wings!" << endl;
}
};
class NoFly : public FlyBehavior {
public:
void fly() override {
cout << "I can't fly!" << endl;
}
};
Quacking Behaviors (Set of Algorithms):
Similarly, we define behaviors for quacking loudly or remaining silent.
class QuackLoudly : public QuackBehavior {
public:
void quack() override {
cout << "Quacking loudly!" << endl;
}
};
class SilentQuack : public QuackBehavior {
public:
void quack() override {
cout << "..." << endl;
}
};
Composition Over Inheritance: Why It’s Better
Instead of hardcoding the flying and quacking behaviors into the duck classes through inheritance, we now compose the behaviors using strategy objects. This means we can easily swap behaviors at runtime or extend our system without touching the base duck class.
Here’s the key difference:
Inheritance locks you into a rigid class hierarchy. If a duck changes its ability to fly or quack, you’d have to rewrite the class or introduce conditions, leading to messy and hard-to-maintain code.
Composition lets you plug in and change behaviors dynamically without altering the rest of the system. The duck class stays clean, and behaviors are modular.
Putting It All Together: The Duck Class
Now, let’s integrate this into the main Duck class. Each duck will have its own flying and quacking behavior, which can be set dynamically.
class Duck {
protected:
FlyBehavior* flyBehavior;
QuackBehavior* quackBehavior;
public:
virtual void swim() {
cout << "Swimming..." << endl;
}
void performFly() {
flyBehavior->fly();
}
void performQuack() {
quackBehavior->quack();
}
virtual void display() = 0; // Set new behaviors at runtime
void setFlyBehavior(FlyBehavior* fb) {
flyBehavior = fb;
}
void setQuackBehavior(QuackBehavior* qb) {
quackBehavior = qb;
}
};
Defining Specific Ducks
Let’s create our specific duck species. We’ll start with the Wood Duck (which can fly and quack) and the Pekin Duck (which quacks but doesn’t fly).
class WoodDuck : public Duck {
public:
WoodDuck() {
flyBehavior = new FlyWithWings();
quackBehavior = new QuackLoudly();
}
void display() override {
cout << "I am a Wood Duck!" << endl;
}
};
class PekinDuck : public Duck {
public:
PekinDuck() {
flyBehavior = new NoFly();
quackBehavior = new QuackLoudly();
}
void display() override {
cout << "I am a Pekin Duck!" << endl;
}
};
Changing Behavior Dynamically
With the Strategy Pattern, you can change a duck’s behavior at runtime. Let’s say you have a Pekin Duck that suddenly learns to fly. You can dynamically swap its behavior without changing its class:
int main() {
Duck* woodDuck = new WoodDuck();
woodDuck->performFly(); // Output: Flying with wings!
woodDuck->performQuack(); // Output: Quacking loudly!
Duck* pekinDuck = new PekinDuck();
pekinDuck->performFly(); // Output: I can't fly!
pekinDuck->performQuack(); // Output: Quacking loudly!
// Change Pekin Duck's flying behavior at runtime
pekinDuck->setFlyBehavior(new FlyWithWings());
pekinDuck->performFly(); // Output: Flying with wings!
}
Summary: Why Use the Strategy Pattern?
- Encapsulation of behaviors: The ability to fly and quack are now encapsulated into their own classes, making them easy to manage, extend, or modify.
- Interchangeable algorithms: Different flying or quacking strategies (algorithms) can be swapped out easily, even at runtime.
- Flexible and reusable code: You can create new ducks with different combinations of behaviors without changing the core duck code. The same flying or quacking behaviors can be reused across different ducks.
By applying the Strategy Pattern, you make your system adaptable and easier to maintain. The key idea is to encapsulate what changes (behavior) and use composition to allow flexibility, making your duck pond a place where behaviors can evolve and adapt without a ripple of complexity! 🦆✨
Top comments (0)