The Strategy Pattern and Giving a Duck a Trick of Its Own
How pulling flying and quacking out of the Duck class and into their own small boxes lets any duck swap tricks without ever changing the Duck class itself.
The last post ended with a problem. MountainDuck and CloudDuck needed the exact same flying trick, but they weren't parent and child, just cousins. So that trick got copy-pasted into both classes.
Here's the fix, in one sentence: take the flying trick out of Duck completely, put it in its own little box, and hand each duck the box it needs.
Pulling the Trick Out Into Its Own Box
Here's the whole idea in plain words. A trick that keeps changing shouldn't live inside the class that uses it. Put it in its own box instead, and make every box for that trick look the same from the outside.
That same look, from the outside, has a name. It's called an interface.
So fly() no longer lives inside Duck. Instead, Duck gets handed a small box whose only job is flying. The duck doesn't know how it flies. It only knows it has a box that can.
An interface just describes what a box looks like from the outside. It says nothing about what's inside it.
class IFlyBehavior {
public:
virtual ~IFlyBehavior() = default;
virtual void fly() = 0;
};
class IQuackBehavior {
public:
virtual ~IQuackBehavior() = default;
virtual void quack() = 0;
};Here's what that actually does to Duck. It no longer writes fly() or quack() itself. It just holds two boxes.
class Duck {
public:
// constructor comes next
private:
std::unique_ptr<IFlyBehavior> flyBehavior_;
std::unique_ptr<IQuackBehavior> quackBehavior_;
};Duck doesn't know which box it has yet. That's the next piece: filling those boxes in, then handing one to Duck when it's built.
Now build a few real boxes. Each one is a small class with exactly one job.
class SimpleFlyBehavior : public IFlyBehavior {
public:
void fly() override { std::cout << "Flapping and flying.\n"; }
};
class JetFlyBehavior : public IFlyBehavior {
public:
void fly() override { std::cout << "Soaring on mountain winds.\n"; }
};
class NoFlyBehavior : public IFlyBehavior {
public:
void fly() override { /* stays on the ground */ }
};
class SimpleQuackBehavior : public IQuackBehavior {
public:
void quack() override { std::cout << "Quack!\n"; }
};
class NoQuackBehavior : public IQuackBehavior {
public:
void quack() override { /* stays silent */ }
};Remember the MountainDuck and CloudDuck problem? It's gone. Both ducks just get handed the same JetFlyBehavior box. No copy-paste, because that flying trick now lives in exactly one place: inside JetFlyBehavior.
Every Duck Gets Its Box Handed to It
So how does a duck actually get one of these boxes? Through its constructor, the moment the duck is built. This is called constructor injection. A real backend project uses the same idea to hand a piece of code what it needs, instead of hardcoding it.
class Duck {
public:
Duck(std::unique_ptr<IFlyBehavior> flyBehavior,
std::unique_ptr<IQuackBehavior> quackBehavior)
: flyBehavior_(std::move(flyBehavior)),
quackBehavior_(std::move(quackBehavior)) {}
void performFly() { flyBehavior_->fly(); }
void performQuack() { quackBehavior_->quack(); }
private:
std::unique_ptr<IFlyBehavior> flyBehavior_;
std::unique_ptr<IQuackBehavior> quackBehavior_;
};Look at what Duck does now. It doesn't write any flying code. It doesn't write any quacking code either. It just holds two boxes, and asks each box to do its job.
Why std::unique_ptr here, and not a plain IFlyBehavior*? Because each duck should own its own box. Nobody else should be able to delete that box while the duck is still using it. A plain pointer can't guarantee that. unique_ptr can.
TypeScript and Java both skip this problem completely. Their garbage collectors clean up automatically, so a plain field in the constructor is all either one needs. This is the one spot where C++ makes you decide something a garbage-collected language decides for you.
There's no WildDuck class anymore. No RubberDuck class. No MountainDuck class. A "wild duck" is just a name for one particular pair of boxes.
Duck wildDuck(
std::make_unique<SimpleFlyBehavior>(),
std::make_unique<SimpleQuackBehavior>()
);
Duck rubberDuck(
std::make_unique<NoFlyBehavior>(),
std::make_unique<NoQuackBehavior>()
);
Duck mountainDuck(
std::make_unique<JetFlyBehavior>(),
std::make_unique<SimpleQuackBehavior>()
);Now watch what happens when something changes. Fix how a wild duck flies, and every wild duck gets the fix right away, since they all share one SimpleFlyBehavior box. Fix how a mountain duck flies, and no other duck even notices, since that trick was never inside Duck to begin with. Change one box, and nothing else has to change with it.
The General Shape of This Pattern
Every strategy pattern looks the same underneath, no matter which trick it wraps. There are always four pieces.
ExpandA duck holding an IFlyBehavior and an IQuackBehavior, each implemented by a couple of small interchangeable classes
The same trick even works for how a duck shows itself on screen, not just flying or quacking. Whenever two classes need the same trick but aren't parent and child, that's the sign the strategy pattern was built to catch.
The Essentials
- A box shape (an interface) only describes what a box looks like from the outside, never what's inside it.
- A duck gets its box through its constructor instead of one hardcoded class. This is called constructor injection.
- Once a trick lives outside the duck, changing one duck's trick never forces a change anywhere else.
g++ -std=c++17 main.cpp -o main && ./main (C++) or npm start (TypeScript) to see the finished fix run.Try It Yourself
Ducks are a toy example. Here's the same problem in a shape you'll actually meet at work: a checkout page that needs to accept a payment.
Picture checkout() written the naive way. If it's a credit card, do this. If it's PayPal, do that. Every new payment method means opening that same function and bolting on another branch. That's the MountainDuck and CloudDuck problem, wearing a different name.
Build it the strategy-pattern way instead.
- Design an
IPaymentMethodinterface with one method:pay(amount). - Build three boxes:
CreditCardPayment,PayPalPayment, andBankTransferPayment. Each one just prints what it charged ("Charged $42.00 to card ending in 4242."). - Build a
Checkoutclass. Give it anIPaymentMethodthrough its constructor, exactly the wayDucktook itsIFlyBehavior. - Add a fourth box,
CryptoPayment, without openingCheckoutat all. If you have to touchCheckoutto add it, something's wrong.
Same four pieces as Duck: something that needs a trick done, a box shape for that trick, a handful of boxes that each fill it differently, and a constructor that only ever asks for the shape.
Keep reading