duandai3964 2016-04-07 21:51
浏览 51

如何编写遵循依赖注入模式的工厂?

I'm new to DI pattern, thus, here is my question.

Edit 11 apr.

If I have certain factory that implements some business logic and at runtime creates its main object and its dependencies, depending on input arguments given, how should I implement it so that it wouldn't break the DI principle? (Should it avoid directly calling the new operator to instantiate its main object (being produced by this factory) and its dependencies, shouldn't it?)

(end of edit 11 apr)

Consider the following CarFactory exemple:

interface IEngine {}

class RegularEngine implements IEngine {}

class AdvancedEngine implements IEngine {}

class Car
{
    private $engine;

    public function __construct(IEngine $engine)
    {
        $this->engine = $engine;
    }
}

class CarFactory
{
    public function __invoke(bool $isForVipCustomer = false): Car
    {
        // @todo Here, CarFactory creates different Enginges and a Car,
        // but they should be injected instead?
        $engine = $isForVipCustomer ? new AdvancedEngine : new RegularEngine;
        return new Car($engine);
    }
}

// And here is my composition root:
$carFactory = new CarFactory;
$carForTypicalCustomer = $carFactory();
$carForVipCustomer = $carFactory(true);

Now, as you can see, my factory implementation breaks the DI principle: instead of receiving instances from the invoking code (from Injector, if I use DI container), it creates them by itself. So, I tried to rewrite it so that it doesn't break the DI principle. Here is what I got:

interface ICarSimpleFactoryForCarFactory
{
    public function __invoke(IEngine $engine): Car;
}

interface IEngineSimpleFactoryForCarFactory
{
    public function __invoke(): IEngine;
}

class CarFactory
{
    private $carSimpleFactory;
    private $regularEngineSimpleFactory;
    private $advancedEngineSimpleFactory;

    public function __construct(
        ICarSimpleFactoryForCarFactory $carSimpleFactory,
        IEngineSimpleFactoryForCarFactory $regularEngineSimpleFactory,
        IEngineSimpleFactoryForCarFactory $advancedEngineSimpleFactory
    )
    {
        $this->carSimpleFactory = $carSimpleFactory;
        $this->regularEngineSimpleFactory = $regularEngineSimpleFactory;
        $this->advancedEngineSimpleFactory = $advancedEngineSimpleFactory;
    }

    public function __invoke(bool $isForVipCustomer = false): Car
    {
        $engine = ($isForVipCustomer ? $this->advancedEngineSimpleFactory :
            $this->regularEngineSimpleFactory)();
        return ($this->carSimpleFactory)($engine);
    }
}

// And here is my composition root:
// (sinse I'm now using DI, I need to inject some dependencies here)
$carFactory = new CarFactory(
    new class implements ICarSimpleFactoryForCarFactory {
        public function __invoke(IEngine $engine): Car
        {
            return new Car($engine);
        }
    },
    new class implements IEngineSimpleFactoryForCarFactory {
        public function __invoke(): IEngine
        {
            return new RegularEngine;
        }
    },
    new class implements IEngineSimpleFactoryForCarFactory {
        public function __invoke(): IEngine
        {
            return new AdvancedEngine;
        }
    }
);
$carForTypicalCustomer = $carFactory();
$carForVipCustomer = $carFactory(true);

I had to introduce 2 new interfaces of small factories whose only responsibility is to give the opportunity to the Dependency Injector to inject instances to them and, respectively, to main CarFactory. (If I use DI container.)

It seems to me, that such a solution is overcomplicated and breaks the KISS design principle: just to move the use of “new” operator from my classes to composition root, I had to create an interface and its implementation, only to let the injector do its work?

So, what is the correct way to implement factories, following DI pattern? Or, the solution provided by me is the only correct answer to this question?

  • 写回答

1条回答 默认 最新

  • duano3557 2016-04-07 23:00
    关注

    I think the entire discussion depends on where in your application do you need your cars, how often, and in what kind of variety.

    First things first: Your initial factory is already horrible. Using __invoke() does not really help making the code calling this method clear, and using a boolean parameter to switch between the only two implementations likely is a violation of other good principles, because you restrict the result of this factory to two possible outcomes.

    But apart from that, the essential question is: How many cars does your application need, and when. Take the example of doing exactly the same with database connection classes: You'd probably only need exactly one type of database at any given time for one task, so the class encapsulating the code to query the database will require an instance of the database interface in its constructor just like your IEngine interface.

    Because the object allowing high level database access is considered a long term object (it will be eventually created once, then live for as long as needed, and only be destroyed when the PHP script ends), it's creation should take place at the composition root. How?

    A factory class that needs to be instantiated is not necessary! It's overkill. Back to your car example, at the end of your script you need an instance of car with an engine inside, and it should be the only car needed her for a moment.

    Instead of writing:

    $carFactory = new CarFactory;
    $carForTypicalCustomer = $carFactory();
    $carForVipCustomer = $carFactory(true);
    

    you can also write

    $engine = new RegularEngine;
    $carForTypicalCustomer = new Car($engine);
    

    No factory at all.

    Imagine instead of programming cars yourself, you use a library. It would be tedious to always remember how to create cars from this vendor (perhaps you always forget where you put the manual for this), so it is more convenient to put the code creating a car into a function - or even into a static method of a factory class:

    class CarFactory {
        static public getInstance() {
            $engine = new RegularEngine;
            return new Car($engine);
        }
    }
    # and in your code
    $carForTypicalCustomer = CarFactory::getInstance();
    

    This separates the knowledge of doing all the tiny details from the actual place where just the whole thing is needed.

    I skip the discussion about customizing the output of above factory because it would be easy to either add in the boolean parameter, or allow a string with a class name, or allow an instance of the IEngine interface or anything, because that's irrelevant to my point.

    Let's think about a different situation: Instead of your application needing one car object for the whole application life, it has a certain place where new car objects have to be produced in huge amounts (just think of trying to send emails to different recipients or something like this). Such a car object does not belong into the composition root. The other class in need of unlimited amounts of car objects needs a factory instance to create new cars. And this other object belongs to the composition root, and there it must get the CarFactory injected. How to do this: Scroll back to the start of this answer, it is a recursive process.

    Your overwhelming amount of additional interfaces for your second car factory comes from the fact that you started to create a dependency injection framework. Maybe you should continue for educational purposes, but I would consider this useless. There are very nice and small examples of DI frameworks available, one of them even fits into a tweet with less than 140 characters. Lets look at it: http://twittee.org/

    class Container {
     protected $s=array();
     function __set($k, $c) { $this->s[$k]=$c; }
     function __get($k) { return $this->s[$k]($this); }
    }
    

    That's all you need for a fully working DI container. What does happen in there? You create an instance and then add "properties" to it, which will trigger calling the magic __set method and internally save the parameter to an array.

    You have to assign closures (and note that there is no error checking, so dont use it in production). This is simple for scalar values like DB connection parameters, or the color of a car. In order to do something useful, you can also assign closures that accept one parameter, the DI container itself, and should return whatever you desire.

    Let's configure your first car example:

    $c = new Container();
    $c->car = function ($c) {
        return new Car($c->engine);
    }
    $c->engine = function () {
        return new RegularEngine();
    }
    $carForTypicalCustomer = $c->car
    

    Again, this does not account for configurability. How to do that?

    $c = new Container();
    $c->typicalCar = function ($c) {
        return new Car($c->regularEngine);
    }
    $c->regularEngine = function () {
        return new RegularEngine();
    }
    $c->vipCar = function ($c) {
        return new Car($c->advancedEngine);
    }
    $c->advancedEngine = function () {
        return new AdvancedEngine();
    }
    $carForTypicalCustomer = $c->regularCar;
    $carForVipCustomer = $c->vipCar;
    

    There we are: The building plans for the two types of cars are moved into closures that are doing the correct stuff, and you still have to decide if you want the one or the other type of car - there is no boolean parameter anymore, but I hope I gave you the impression that usually in applications you don't have such a parameter affecting the build process of objects at the composition root. Adding such a parameter there is a non-existing problem. But what if your really want it? Just imagine that the type of car comes from a configuration file or something:

    $c = new Container();
    $c->car = function ($c) {
        return new Car($c->engine);
    }
    $c->engine = function ($c) {
        return ($c->engineType) ? new AdvancedEngine : new RegularEngine;
    }
    $c->engineType = function () { 
        return true; 
    }
    $carForTypicalCustomer = $c->car
    

    Instead of using a fixed return true for the engine type, you can add any code that reads configuration from somewhere.

    Note that in this state of my example, this short DI container already comes to it's edge cases. It would be more convenient to assign the scalar values directly instead or having to encapsulate them in closures. More advanced DI containers have this feature - for example look at Pimple.

    Even more advanced DI containers will look at the code of the class that is requested to be instantiated and try to figure out by themself (or with the help of code annotations), which other depended classes need to be instantiated.

    For example, PHP-DI has a very nice autowiring feature that will simply try to call new $typehint if the typehint in the constructor points to a single class. In your case, the typehint points to an interface which cannot be instantiated, and you are required to tell PHP-DI which implementation you need. You have to select either the RegularEngine or the AdvancedEngine.

    Note that the advanced DI containers will usually treat any created object to have singleton behavior, i.e. they will only create one instance when first asked for it, and on subsequent requests will return the SAME instance. So it makes no use to add configurability for the engine - as long as the Car object is used as a long-term object, this will be completely ok.

    If however you need plenty of new instances, you could add a factory class into the DI container, and inject this factory where car instances are needed, and this factory might just provide one simple method with the boolean parameter and emit new car instances with different engines all day long. However, short-term objects don't belong into the composition root, and therefor are not in the scope of dependency injection (as the general concept) or DI containers - they belong to the application code, aka "business logic". And if the Car object and the Engines belong to the same domain, or are located in the same package, it is perfectly valid to just call new Car(new RegularEngine) if there is no need for ANY more customization. Most often you don't need perfect configurability for everything, you only need one specific job done. That's the time to only do THAT ONE job, and nothing in addition. You ain't gonna need it! (YAGNI)

    评论

报告相同问题?

悬赏问题

  • ¥100 嵌入式系统基于PIC16F882和热敏电阻的数字温度计
  • ¥15 cmd cl 0x000007b
  • ¥20 BAPI_PR_CHANGE how to add account assignment information for service line
  • ¥500 火焰左右视图、视差(基于双目相机)
  • ¥100 set_link_state
  • ¥15 虚幻5 UE美术毛发渲染
  • ¥15 CVRP 图论 物流运输优化
  • ¥15 Tableau online 嵌入ppt失败
  • ¥100 支付宝网页转账系统不识别账号
  • ¥15 基于单片机的靶位控制系统