What is a Dependency Injection Container?

2019-01-25 20:25发布

问题:

I am trying to understand the role of a Dependency Injection Container because it strikes me as fundamental in maintainable code.

As I understand it, a DIC is just as the title suggests: a container where all your dependencies are collected together. Instead of seeing new Foo\Bar all over the application, all the new instances are generated inside of the container and then passed through to each other where they are needed (e.g., Model is instantiated with an instance of Database, which is instantiated with an Instance of Config).

I have attempted to make a very simple DIC. This is the result.

In my front controller I am instantiating a new App\Core\Container.

My Container looks like this:

<?php

namespace App\Core;

use App\Config;

class Container
{
    public $config;
    public $router;
    public $database;

    public $model;
    public $view;
    public $controller;

    public function __construct()
    {
        $this->config   = new Config;
        $this->router   = new Router;
        $this->database = new Database($this->config);
    }

    public function add()
    {
        // add dependencies from the outside?
    }

    public function getInstance(/* string */)
    {
        // return an instance for use somewhere?
    }

    public function newModel($model)
    {
        $model = $this->getModelNamespace() . $model;
        $this->model = new $model($this->database);
        return $this->model;
    }

    private function getModelNamespace()
    {
        $namespace = 'App\Models\\';
        if (array_key_exists('namespace', $this->params = [])) {
            $namespace .= $this->params['namespace'] . '\\';
        }
        return $namespace;
    }

    public function newView($params)
    {
        $this->view = new View($this->model, $params);
        return $this->view;
    }

    public function newController($controller)
    {
        $controller = $this->getControllerNamespace() . $controller;
        $this->controller = new $controller;
        return $this->controller;
    }

    private function getControllerNamespace()
    {
        $namespace = 'App\Controllers\\';
        if (array_key_exists('namespace', $this->params = [])) {
            $namespace .= $this->params['namespace'] . '\\';
        }
        return $namespace;
    }
}

Questions

  • Could my implementation above, although very simple, be classed as a basic dependency injector?
  • Is a Dependency Injection Container generally comprised of one class?

回答1:

Note: the first three headings answer your questions, while the following ones answer anticipated questions and provide coverage of anything in the first two sections.

Could this be classified as a dependency injection container?

No, this does not look like a dependency injection container. A dependency injection container is meant to reduce the work that instantiation requires by determining, creating, and injecting all dependencies. Rather what you have there appears to be a combination of a factory and a service locator.

Factories abstract the creation of objects. This is essentially what your Container class is doing. By calling designated methods (i.e., newModel), your container takes on the responsibility of locating the exact object to be instantiated and constructing an instance of that object.

The reason I would call this a "poor" factory is that it's beginning to look like it might be used to locate services. Service locators work by hiding an object's dependencies: instead of being dependent on GenericService, an object might depend on a service locator. Given the service locator, it can request an instance of GenericService. I see similar behavior beginning to take hold in your add() and getInstance() methods. Service locators are generally considered anti-patterns because they abstract dependencies therefore making code impossible to test!

Is a dependency injection container comprised of one class?

It depends. You could very easily make a simple dependency injection container with one class. The issue is that the nature of a simple container tends to get more advanced into a not-so-simple container. When you start improving your pattern, you need to consider how the different components play together. Ask yourself: do they follow SOLID principles? If not, refactoring is necessary.

What is a dependency injection container?

I said it above, but again: a dependency injection container is meant to reduce the work that instantiation requires by determining, creating, and injecting all dependencies. A DIC will look at all dependencies of a class, and all dependencies those dependencies may have, and so on... In this sense, the container is responsible for hierarchically instantiating all dependencies.

The Container class you provide relies on very strict definitions of pre-defined classes. For example, classes in your model layer appear to only be dependent on a database connection. (Similar statements can be said about classes in your controller & view layer).

How does a dependency injection container find dependencies?

A dependency injection container will detect dependencies. Typically this happens through 1 of 3 mechanisms: autowiring, annotations, and definitions. PHP-DI docs provide a good idea of what all three of these entail here. In short, though: autowiring detects dependencies by reflecting on a class, annotations are used to write in dependencies using comments above a class, and definitions are used to hard-code dependencies. Personally, I prefer autowiring because it's clean & simple.

Can I create a simple dependency injection container or no?

Yes, you can. Start with the idea that an injector should be able to instantiate any object (service, view, controller, etc...). It needs to look at the relevant object and hierarchically instantiate all dependencies (hint: possibly through some method of recursion).

A quick example of a simple injector using autowiring looks like this:

<?php
class Injector
{
    public function make($className)
    {
        $dependencies = [];

        //Create reflection of the class-to-make's constructor to get dependencies
        $classReflection = new ReflectionMethod($className, "__construct");

        foreach($classReflection->getParameters() as $parameter) {
            $dependencyName = $parameter->getClass()->getName();

            //Use the injector to make an instance of the dependency
            $dependencies[] = $this->make($dependencyName);
        }

        $class = new ReflectionClass($className);

        //Instantiate the class with all dependencies
        return $class->newInstanceArgs($dependencies);
    }
}

Tested with something like the following, you can see how the injector recursively checks and instantiates all dependencies

class A {
    protected $b;
    public function __construct(B $b) { $this->b = $b; }
    public function output(){ $this->b->foo(); }
}

class B {
    protected $c;
    public function __construct(C $c) { $this->c = $c; }
    public function foo() { $this->c->bar(); }
}

class C {
    public function __construct() { }
    public function bar() { echo "World!"; }
}

$injector = new Injector;

$a = $injector->make("A");
//No need to manually instantiate A's dependency, B, or B's dependency, C

$a->output();

This basic injector has obvious faults. For example, there is an opportunity to create a recursion disaster if two classes are dependent on each other (there should be a check for that). However, as is, this works as a basic example of what an injector looks like.

Injector vs. Dependency Injection Container

To make this more powerful and fall under the definition of "dependency injection container", you'd want a way to share instantiated instances across multiple make() calls. For example, you may have another method called share(). This method would store the instance passed to it. Whenever a class is built through the make() method and depends on a class previously shared, instead of instantiating a new instance, it would use the already-instantiated one.

For a simple & powerful dependency injection container I suggest Auryn, but by all means, try to understand & create your own before using the ones already available.