Dependency Injection: Letting the Caller Decide, Not the Class

Why Crow creating its own FlyingBehavior object still breaks testing, and how dependency injection, a close companion to Dependency Inversion, removes the last change from the class entirely.

September 14, 20263 min read17 / 20

The previous post fixed Crow's dependency on a concrete flying behavior by routing it through a FlyingBehavior interface instead. One small change still remained inside Crow itself: the line that decides which concrete behavior to create.

The Problem Testing Exposes

Say CrowOwlFlyingBehavior needed a real network call to check some external flight-pattern service. Would a test for Crow still be trustworthy if it depended on that call succeeding?

No. A test that fails because of a flaky connection, not because of an actual bug, is worse than no test at all. People stop trusting red test runs, and that's exactly when a real bug slips through.

The fix is a mock: a stand-in object that behaves predictably, with nothing real underneath it, no network call, no database. Write a MockFlyingBehavior whose makeFly() just returns instantly, and hand that to Crow instead of the real thing during a test.

Here's the catch: Crow still creates its own behavior object internally.

class Crow : public Bird, public Flyable { public: void fly() override { behavior->makeFly(); } private: std::unique_ptr<FlyingBehavior> behavior = std::make_unique<CrowOwlFlyingBehavior>(); };

There's no way, from outside Crow, to swap in MockFlyingBehavior for a test. The line creating the real behavior is buried inside the class, and nothing external can reach in and replace it.

Dependency Injection, in One Sentence

The fix has a name. It isn't one of the five SOLID principles itself. It's a separate idea, closely related to Dependency Inversion, that shows up constantly alongside it: dependency injection.

Stop creating the dependency inside the class. Let whoever constructs the object hand it in instead. The simplest way to hand something in is through the constructor.

class Crow : public Bird, public Flyable { public: Crow(std::unique_ptr<FlyingBehavior> behavior) : behavior(std::move(behavior)) {} void fly() override { behavior->makeFly(); } private: std::unique_ptr<FlyingBehavior> behavior; };

Crow no longer creates anything. It just declares what it needs, a FlyingBehavior, and trusts whoever builds a Crow to supply one.

What This Actually Buys

Real code decides what Crow actually flies like, at the point where it gets created:

auto realCrow = std::make_unique<Crow>(std::make_unique<CrowOwlFlyingBehavior>());

A test decides differently, with zero changes to Crow itself:

auto testCrow = std::make_unique<Crow>(std::make_unique<MockFlyingBehavior>());

If Crow wants to fly in a different way tomorrow, there's now nothing to change inside Crow at all. Not the class name, not the method name, not even a line inside the class. Whoever constructs the Crow just passes a different FlyingBehavior in, and Crow never notices the difference.

This closes the last gap from the previous post. Dependency Inversion got Crow depending on an interface instead of a concrete class. Dependency injection finished the job: it took the decision of which concrete class to use out of Crow's hands entirely, and put it wherever the object actually gets built, production code or a test file.