Dependency injection in factories

2019-04-09 00:03发布

问题:

I'm really new to DI, but I really want to try using it.

There's something I don't understand. Here's a simple pseudocode of a factory, I'm using a lot.

class PageFactory {
   public function __construct(/* dependency list */) {
      ... //save reference to the dependencies
   }

   public function createPage($pagename) {
       switch ($pagename) {
           case HomePage::name:
               return new HomePage(/* dependency list */);
           case ContactPage::name:
               return new ContactPage(/* dependency list */);
           ...
           default:
               return null;
       }
   }
}

It has a really simple logic, it chooses the implementation instance based on a string. It's very useful, because I can choose at a later time what page I need, and only that one will be created.

How would I rewrite this code, so my page instances would be created by a dependency container, thus I wouldn't need to handle dependencies for the factory and the pages it creates?

The only solution I see is to make the container I want to use, a dependency for the factory, and make calls to it from within the factory. I have lot's of problem with that.

First, I don't want to couple the container into my application, and every factory it has.

Second, and my biggest problem is, that the call for the container is really messy, it's stringly typed (i.e. $container->get('Foo');). I would like to use it as few times as possible. Only once if possible.

EDIT:

I do not want to write a DI container. I want to use an existing one. My question is on usage. How would I use a DI container instead or within the above factory, while keeping the logic on instance selection.

EDIT 2:

I started using Dice as the DI container, because it's lightweight, and knows everything I need. I would prefer if I could use it in a single place and build up the whole application. For that, I'll need a way to get rid of these factories somehow, or modify it in some way to make those pages behave like dependencies, so the DI container would provide instances to them.

EDIT 3:

Yes, I need this for testing purposes. I'm also new to testing, but it is very awesome so far, I really like it.

These pages are what MVC frameworks call controllers. But all the MVC frameworks I checked out are not making their controllers testable, because they create instances of them automatically. And because they are created by the system, their constructor parameters are not customizable by the user.

There is a simple way to check this for any framework. I just look up the way I should use a database in a controller in that specific framework. Most frameworks are either procedural, or use some service locator, either way, they are getting their dependencies from public scope, something I do not want to do. This is why I'm not automating the controller instantiation. The downside is that I now have these weird factories, which carry a lot of dependencies. And I would like to substitute this task to a DI container.

Most frameworks implement their own testing mechanism, which are more like functional testing, instead of unit testing, but I do not want to do that either.

回答1:

Note: dependency injection is a design pattern, while DI containers are libraries, which produce instances by utilizing dependency injection ... or they are shitty service locators, that someone is selling as latest buzz-thing

A properly implemented DI container is basically a "smart factory". But implementing one will probably be way beyond your current capabilities. It's kinda really complicated, because a good DI container will build the whole dependency tree.

For example:

Let's say you have a class Foo which needs instances of Alpha and Beta passed in construct. But there is an issue. Instance of Beta also needs in constructor a PDO DSN and an instance of Cache.

A well made DI container will be able to construct the whole dependency tree at once.

Instead of making one yourself, you should go with already existing DI container.

My recommendation would be Auryn.



回答2:

I know, it's an old question. But I'm currently looking for an answer to a similar, but more general question: how to correctly implement DI pattern in factories which decide what and how they create directly in runtime? Maybe my answer can help someone, who'll find this question in search engine (like I did)? And, maybe, it'll also be useful to you? Or, you'll share some of your experience that you've gained during these almost two years passed… — I hope, you're already not so “new to DI” as you were when you were asking this question on SO :) (I'm currently new to DI)

I found that this is a common question, but there isn't a common answer to it, especially in PHP. For example, here is how this problem is supposed to be solved in Guice (popular Java framework from Google, supporting DI):

  • https://github.com/google/guice/wiki/InjectingProviders
  • https://github.com/google/guice/wiki/AssistedInject

Someone proposes to „new-up“ objects (create using the “new” operator) directly in such factories, that's OK they say. E. g. Miško Hevery, one of two original developers of AngularJS — popular JS framework from Google, — in his article «To “new” or not to “new”…» introduces his own separation principle: he says, it's OK to create “value objects” whenever and whereever you need them, whereas “service objects” can only be injected via DIC and are not allowed to be created directly.

But I personally disagree with them, because such factories can have some business logic, that makes it impossible to consider them a part of composition root of app (where newing-up is only allowable).

The solution: inject trivial factories into factories

IMO, the only solution that follows the DI pattern is to create special trivial “injection friendly” factories that depend on injector and return objects that they obtain directly from callin injector's methods. Sinse direct access to injector, like direct newing-up of own dependencies, is allowable only in composition root, therefore, the declaration of all these providers should be done in composition root. I'll demostrate my suggestion with the following example.

You wrote that you were going to use PHP-DI as DIC. Me too, I decided to use it in my projects, therefore, the examples below will also use it.

// 1. First, define interfaces of trivial factories that'll be used to
// create new objects using injector.
interface HomePageTrivialFactoryInterface {
    public function __construct(
        DI\Container $container
        // Injector is needed to fetch instance directly from it.
        // List of other dependencies that are already known at design
        // time also goes here.
    );
    public function __invoke(
        // List of dependencies that are computed only in runtime goes here
        // You may name this method something else, “create” for example,
        // but then you'll also have to specify this method's name when
        // you'll wire things together in container definitions on step #3.
    ): HomePage;
}
// ContactPageTrivialFactoryInterface is defined similarly

// 2. Now in PageFactory::createPage we'll use the injected trivial
// factories to create page objects.
class PageFactory {
    private $homePageTrivialFactory;
    private $contactPageTrivialFactory;

    public function __construct(
        HomePageTrivialFactoryInterface $homePageTrivialFactory,
        ContactPageTrivialFactoryInterface $contactPageTrivialFactory
        // list of other dependencies that are already known at design time
        // also goes here
    ) {
        // save reference to the dependencies
    }

    public function createPage(
        $pagename
        // list of other dependencies that are computed only at runtime goes
        // here
    ) {
        switch ($pagename) {
            case HomePage::name:
                return ($this->homePageTrivialFactory)(
                    // Write here all the dependencies needed to create new
                    // HomePage (they're listed in
                    // HomePageTrivialFactoryInterface::get's definition).
                    // Here you may use both the dependencies obtained from
                    // PageFactory::__construct (known at design time) and
                    // from PageFactory::createPage methods (obtained at
                    // runtime).
                );
            case ContactPage::name:
                return ($this->contactPageTrivialFactory)(
                    /* dependency list, similarly to HomePage */
                );
            // ...
            default:
                return null;
        }
    }
}

// 3. Now, let's set up the injection definitions in the composition root.
// Here we'll also implement our TrivialFactoryInterface-s.
$containerDefinitions = [
    HomePageTrivialFactoryInterface::class => DI\factory(
        function (DI\Container $container): HomePageTrivialFactoryInterface
        {
            return new class($container)
                implements HomePageTrivialFactoryInterface
            {
                private $container;

                public function __construct(
                    DI\Container $container
                    // list of other design time dependencies
                ) {
                    // save reference to the dependencies
                }

                public function __invoke(
                    // list of run time dependencies
                ): HomePage
                {
                    return $this->container->make(HomePage::class, [
                        // list of all dependencies needed to create
                        // HomePage goes here in the following form.
                        // You may omit any dependency and injector will
                        // inject it automatically (if it can).
                        // 'constructor parameter name of dependency' =>
                        //     $constuctor_parameter_value_of_dependency,
                        // etc - list here all needed dependencies
                    ]);
                }
            };
        }
    ),
    // ContactPageTrivialFactoryInterface is defined similarly
];

// 4. Finally, let's create injector, PageFactory instance and a page using
// PageFactory::createPage method.
$container = (new DI\ContainerBuilder)
    ->addDefinitions($containerDefinitions)
    ->build();
$pageFactory = $container->get(PageFactory::class);
$pageFactory->createPage($pageName);

In the example above, when I wired up trivial factories to DI container, I, among other things, declared the interfaces of these factories and implemented them with inline anonimous classes instances (this feature was introduced in PHP 7). If you don't wanna bother yourself to write such interfaces, you may skip this and write these factories directly, without interfaces. The simplified example is listed below. Note that I omit steps 1, 2 and 4 in the example: step #1 gets removed because we no longer need to define those trivial interfaces, and steps 2 and 4 remain unchanged, except that I remove type hints from PageFactory constructor, referencing already non-existent interfaces. The only step that changed is the 3-rd step, which is listed below:

// 3. Now, let's set up the injection definitions in the composition root.
// Here we'll also implement our TrivialFactory-s and wire them to
// PageFactory constuctor parameters.
$containerDefinitions = [
    PageFactory::class => DI\object()
        ->constructorParameter('homePageTrivialFactory', DI\factory(
            function (
                DI\Container $container
                // list of other dependencies that are already known at
                // design time also goes here
            ) {
                function (
                    // list of run time dependencies
                ) use($container): HomePage
                {
                    return $container->make(HomePage::class, [
                        // list of all dependencies needed to create
                        // HomePage goes here in the following form:
                        // 'constructor parameter name of dependency' =>
                        //     $_constuctor_parameter_value_of_dependency,
                        // etc - list here all needed dependencies
                    ]);
                }
            }
        ))
        // ContactPageTrivialFactory is wired and defined similarly
    ,
];

And finally, if you think it's OK to new-up objects in app's composition root (and that's probably really OK), you also may do it in these trivial factories instead of injecting the injector and creating instances using injector. But in such a case, you'll also have to manually instantiate all the dependencies of HomePage (or other page), which is OK if there are no such dependencies, but is undesirable if there are many of them. IMO it's better inject the injector and create object using it: this allows to manually specify only our trivial factories — and not other dependencies.

So, @SinistraD, what do you think about this suggested approach?



回答3:

EDITED

After using the DI container for a few days I realized how simple the solution actually is, that I am now really embarrassed. It also helped that bad_boy recommended routing.

DI as router output handler

I can use the DI container to handle the output of a simple router. The problem with routers is that they return a class name to the framework, thus it's up to the framework to instantiate them. This is a problem, because then the constructor would be predefined (or simply empty) and dependencies would only come from public scope, or a service locator.

But in the case of a DI container the pages are already made by a framework, and not the user. So the solution is simply to allow for such a routing to exist, but then let the DI framework handle the output.

So it would look something like this:

$router = $di->create(Router::class);
$pageClassName = $router->getRequestedPageClassName();
$page = $di->create($pageClassName);
echo $page->render();

This way I'm using the DI in a single place, in the root of my application, and I can have many routers containing any logic and dependencies, with any number of pages with any dependencies.

::class constants

I also had a great problem with these. Mainly, that they are PHP 5.5. I solved it by writing a small PHP preprocessor, that takes a PHP file, changes every ClassName::class to "ClassName", saves it to a special location not visible by my IDE, and I've set up my autoloader to only load the processed PHP files. And now, I can use ::class constants in my PHP 5.3 setup, by just adding a special extension to a PHP file, before the .php.



回答4:

I think you are in a few ways missing the point of DI & DIC, one of the main points is to pass the container when creating instances.

class Router {

   private $loader;
   private $routes

   function __construct($container){
      $this->loader = $container->get('loader');
   }
}

Yeah, it uses strings as identifiers. Thats the whole point, it avoids tight coupling. And the signature for creating instances becomes much simpler.

If your want your IDE to be able to decode whats behind the property you can use docblocks

/**
* @property LoaderInterface $loader
*/ 

So how would you use that for a factory?

class NotificationFactory {

   /**
    * @property ContainerInterface $container
    */
   private $container;

   function __construct($container){
     $this->container = $container;
   }

   public function create($user, $message){
     return new Notification(array(
        "to" => $user->email,
        "subject" => $message
     ), 
     $container);
   }
}

Simple enough, then in the Notification class you need a translator:

class Notification {

    /**
    * @property ContainerInterface $container
    */
    private $container;
    /**
    * @property TranslatorInterface $translator
    */
    private $translator;

    function __construct($container){
       $this->container = $container;
       $this->translator = this->container->get('translator');
    }

    function createMessage($message, $lang){
       return $translator->translate($message, $lang);
    }
}

The whole point here is that you can hotswap any component anywhere along the chain which makes testing much easier.

You other issue when creating controllers seems to be that you are going about it wrong.

If your controllers all have the same creator args:

class FooController {
  function __construct($container, $request, $route){
     /** stuff happening here **/
  }

  function showAction($id) {
    return new Response(/** stuff happening here **/);
  }
}

Then you can have a simple request matcher which pares together requests and controllers/actions.

I suggest you have a look at Symfony2 which has testable controllers and a router which is pretty easy to grasp and use as stand alone component and also Fabian Potenciers excellent article What is Dependency Injection?