Вопрос

I have a dilemma on class designing. I'm doing my best to respect SOLID principles but I don't know how to handle dependency injection.

Here is my dilemma:

  • I read it is a bad practice to instantiate objects inside classes to avoid introducing dependency. So where should our dependencies be created in a full object application? In a special object which is only responsible for dependency instantiations? If yes, what is the name of this object and how to define it? Is it what we call "controller"?
  • This "controller", what is the right way to unit test it? Should we unit test it?
  • In a full POO application, how to avoid passing our objects (often the same) between classes? For example, a DB object, Log, ... In this way, we take the risk to have constructors with many parameters, don't we?

In order to illustrate my dilemma, I tried to create a use case.

I want to create a script (that I partially implemented below) that generates a file and print another one:

<?php

/**
 * Generate a file, add it to the queue and print the next one
 */
class Script
    public function run() {
        #Generate file
        $fileComputor = new FileComputer(...);
        $file = $fileComputor->compute();

        #Instantiate dependencies for printing
        $db = new Db(new PdoAdapter());
        $printerDriver = new Driver(new HttpRequest(new CurlAdapter()));
        $log = new Log($db);
        $queue = new Queue($db);
        $statsUpdater = new StatsUpdater($db);
        $monitor = new Monitor(new StatsdDriver(new SocketAdapter()));

        #Add generated file to the queue
        $queueModel->push($file);

        #Print the next file on queue if existing
        $printer = new Printer($printerDriver, $log, $queue, $monitor, $statsUpdater);
        $printer->print();
    }
}

class Printer {
    protected $_driver;
    protected $_log;
    protected $_queue;
    protected $_monitor;
    protected $_statsUpdater;

    /**
     * $driver          : Driver used to send documents to the printer
     * $log             : Log actions in database
     * $queue           : Handle the print queue
     * $monitor         : Send metrics to Statsd (to feed the graphit GUI)
     * $statsdUpdater   : Consolidate the statistics database 
     */
    public function __construct($driver, $log, $queue, $monitor, $statsUpdater) {
        $this->_driver = $driver;
        $this->_log = $log;
        $this->_queue = $queue;
        $this->_monitor = $monitor
        $this->_statsUpdater = $statsUpdater;
    }

    public function print() {
        if ($this->_queue->hasNext()) {
            $file = $this->_queue->getNext();

            $this->_driver->print($file);

            $this->_log->log('File has been printed');
            $this->_monitor->sendCount(1);

            $this->_statsUpdater->increment();
        }
    }
}

?>

What do you think about this implementation?

Every feature we will want to plug into our Printer class will result into a new dependency to pass to the constructor (if for example we want to also generate a syslog, to measure time that takes the printer to process, etc).

In a close future, we will have between 10 and 15 parameters into the constructor call.

Это было полезно?

Решение

So where should our dependencies be created in a full object application? In a special object which is only responsible for dependency instantiations?

You have 2 options:

  • you create all the objects yourself, at the root of your application, i.e. in your front controller (index.php) for example. If you application is a bit big, that becomes quickly a hell.
  • you use a Dependency Injection Container. That object will be responsible of creating objects (and injecting them their dependencies in the constructor). Same here: you have to use/call the container only at the root of your application, i.e. in the front controller (index.php) for example.

If yes, what is the name of this object and how to define it? Is it what we call "controller"?

That's the Container. To give you an example, here is PHP-DI - Understanding DI.

You can use dependency injection on the controller (and I would recommend to do it): you can get dependencies in the controller's constructor (just like in any service). Some frameworks makes that difficult though (Symfony for example).

This "controller", what is the right way to unit test it? Should we unit test it?

No really. Some containers allow you to configure "Factories" to generate some objects.

For example, if creating the DBConnection object is complex, you could write a factory class, that has a method that creates the DBConnection object. So you could test the factory class. But I wouldn't say that's necessary.

In a full POO application, how to avoid passing our objects (often the same) between classes?

You should never pass instances around, because you should never call a constructor: all objects are constructed by the container.

So it becomes really simple: you write each class by taking dependencies in the constructor, and that's it. You don't care about dependencies and what those dependencies need.

For example, a DB object, Log, ... In this way, we take the risk to have constructors with many parameters, don't we?

Yes. You said you expect to have like 15-20 parameters in the constructor: that's not good at all.

You should generally try to have 2-3 parameters maximum. Having to many means that your class has too much responsibilities, it does to many things.

You can try splitting up the code of your class into several smaller/more targeted classes, or use events for example.

If we take your example, your printer could rather be like:

public function print($file) {
    $this->driver->print($file);

    $this->log->log('File has been printed');
    $this->monitor->sendCount(1);

    $this->statsUpdater->increment();
}

It makes more sense: a printer prints a file.

It's one dependency less (the queue). And then you can have a PrintQueueProcessor that watches the queues, takes the next file and calls the printer to print it.

The printer does one job (print a file), the queue processor does one job (unqueue files to print them).

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top