#StandWithUkraine

Use Dependency Injection to build your Jedi army

Dependency Injection was one of those topics that took me a while to understand. However it took me much longer to be able to explain it as well. After a long time thinking on it I had finally grasped the chain of thought and some metaphors for it.

We are going to build a Jedi army!

Meet medieval army

Imagine you are working on a game of sorts. You are tasked to maintain a part of code that deals with Army class, which produces Soldier objects (among other things).

One day you get the news that the code will be used in the new game! With Jedi! And if things go well there are pirate and zombie games lined up after, the business is booming.

You need to take a hard look at the code and refactor it to be flexible enough for new requirements.

Here is medieval PHP Army we start with.

class Soldier
{
    public function __construct()
    {
        $this->weapon    = new Sword();
        $this->equipment = new Shield();
    }
}

class Army
{
    public function getSoldier()
    {
        return new Soldier();
    }
}

echo (new Army())->getSoldier();
// You see the mighty figure of de683d5a, wielding Sword and Shield.

Obviously this needs a lot of improvement or we will have to edit it by hand every time! Dependency Injection will be of huge help to remedy that.

Dependency Injection is…

Here is unbelievably laconic and precise definition of dependency injection:

Dependency Injection is passing arguments.

Yes, when you pass arguments to something you in fact inject dependencies. As opposed to those dependencies created inside of that something.

DI Soldier

What are the dependencies of our Soldier? Those Sword and Shield obviously, we need to pass them if our Soldier object can ever aspire to be something different.

class Soldier
{
    public function __construct($weapon, $equipment)
    {
        $this->weapon    = new $weapon;
        $this->equipment = new $equipment;
    }
}

class Army
{
    public function getSoldier()
    {
        return new Soldier(new Sword(), new Shield());
    }
}

echo (new Army())->getSoldier();
// You see the mighty figure of df77a7fb, wielding Sword and Shield.

Our Soldier is much more flexible now, but Army is still kind of meh. We can do better, let’s turn our army into Dependency Injection Container.

Dependency Injection Container

What now? Well, more of the same. While DI definition is simple, there is a lot of nuance how to use it in practice for flexible and convenient results.

DI Containers are frameworks which offer conventions on how to build DI solutions and make use of other related and helpful patterns.

Meet Pimple

Also known as that–library–with–horrible–name, Pimple is my DIC of choice and what we will use.

There are few things that I like about it:

  • the source is short enough to read and understand;
  • it uses ArrayAccess and anonymous functions for very easy API;
  • previous two things are pretty much entirety of it to learn and understand.

How would our Army look once we make a Pimple Container out of it?

DIC Army

Services

To change our Army implementation to DIC we make it extend Container class and rewrite hardcoded logic as services (using anonymous function convention).

To create a Soldier we ask our Army to execute a soldier service we defined and give us result.

class Army extends \Pimple\Container
{
    public function __construct(array $values = [])
    {
        parent::__construct();

        $this['weapon'] = function () {
            return new Sword();
        };

        $this['equipment'] = function () {
            return new Shield();
        };

        $this['soldier'] = function ($army) {
            return new Soldier($army['weapon'], $army['equipment']);
        };

        foreach ($values as $key => $value) {
            $this->offsetSet($key, $value);
        }
    }

    public function getSoldier()
    {
        return $this['soldier'];
    }
}

echo (new Army())->getSoldier();
// You see the mighty figure of 59a32f31, wielding Sword and Shield.

There are two important benefits to services:

  • they are executed lazily, only when they are needed;
  • they can be manipulated for that flexibility we want to achieve.

But it seems bulkier! While you are eager to reap the promised benefits already, there are some more things to fine tune here.

Factories

Services are by default shared. Let’s try to get two soldiers:

$army = new Army();
$soldier1 = $army->getSoldier();
$soldier2 = $army->getSoldier();
echo $soldier1;
// You see the mighty figure of ab94efb4, wielding Sword and Shield.
echo $soldier2;
// You see the mighty figure of ab94efb4, wielding Sword and Shield.

We got same Soldier twice! Our container remembered the first one we created and gave it again on second request.

In many cases this is desired and gets rid of old Singleton pattern, where classes have to messily keep track of their own instances.

But since we want to generate multiple soldiers, we need to change our service to a Factory kind (repeat for weapon and equipment ones):

$this['soldier'] = $this->factory(function ($army) {
    return new Soldier($army['weapon'], $army['equipment']);
});

// You see the mighty figure of c6e2f8e4, wielding Sword and Shield.
// You see the mighty figure of 12de6823, wielding Sword and Shield.

Now our soldiers are unique!

For one last bit we want to be able to configure our army without rebuilding whole services. We can do it with parameters.

Parameters

Let’s define which weapon we want Soldier to have as container parameter (repeat for equipment):

$this['weapon.class'] = 'Sword';

$this['weapon'] = $this->factory(function ($army) {
    return new $army['weapon.class'];
});

Let there be Jedi!

So we wrote a lot more code by now! Where are our benefits?

Now we can do an awesome thing like this:

$army = new Army([
    'weapon.class'    => 'Lightsaber',
    'equipment.class' => 'Force',
]);

echo $army->getSoldier(); // or just $army['soldier'] now

// You see the mighty figure of 27d22a24, wielding Lightsaber and Force.

We provided different parameters to our Army and it used our services to change the whole chain and produce a different kind of Soldier!

And we can just as easily replace whole services and have our Army adjust the results accordingly.

Full annotated code

class Soldier
{
    public $weapon;

    public $equipment;

    /**
     * We pass weapon and equipment as objects.
     */
    public function __construct($weapon, $equipment)
    {
        $this->weapon    = new $weapon;
        $this->equipment = new $equipment;
    }

    /**
     * Echo helper for demo purposes.
     */
    public function __toString()
    {
        return sprintf(
            'You see the mighty figure of %s, wielding %s and %s.' . "\n",
            hash('crc32b', spl_object_hash($this)),
            get_class($this->weapon),
            get_class($this->equipment)
        );
    }
}

/**
 * Our class extends Pimple container.
 */
class Army extends \Pimple\Container
{
    /**
     * Constructor accepts optional array of values to store or override defaults.
     */
    public function __construct(array $values = [])
    {
        // we need this to setup some internal bits, like factories storage
        parent::__construct();

        // parameter for easy configuration
        $this['weapon.class'] = 'Sword';

        // factory service, dependent on parameter
        $this['weapon'] = $this->factory(function ($army) {
            return new $army['weapon.class'];
        });

        $this['equipment.class'] = 'Shield';

        $this['equipment'] = $this->factory(function ($army) {
            return new $army['equipment.class'];
        });

        // factory service, dependent on two other services
        $this['soldier'] = $this->factory(function ($army) {
            return new Soldier($army['weapon'], $army['equipment']);
        });

        // store our passed values, possibly overriding defaults
        foreach ($values as $key => $value) {
            $this->offsetSet($key, $value);
        }
    }

    /**
     * Backwards compatibility method for old implementation.
     */
    public function getSoldier()
    {
        // we use array access notation which makes container run respective service 
        return $this['soldier'];
    }
}

// create instance of container and configure it with different parameters
$army = new Army([
    'weapon.class'    => 'Lightsaber',
    'equipment.class' => 'Force',
]);

echo $army['soldier'];

// You see the mighty figure of 27d22a24, wielding Lightsaber and Force.

Lessons learned

To sum it up you nailed your refactoring and learned how to:

  • use dependency injection (pass arguments) and make more flexible classes;
  • make your class a Pimple dependency injection container;
  • define services, factory services, and parameters;
  • create and configure instance of your container.

You are so ready to spin up Pirate army next time.

Related Posts