Конфигурационные значения — это тоже зависимости

В ходе моей профессиональной деятельности мне приходилось видеть большое количество кода - как современного, так и устаревшего. И довольно часто присутствует следующая проблема: конфигурационные значения запрашиваются изнутри класса вместо того, чтобы передаваться в него извне т.е. используется аналог глобальных переменных или service locator для получения конфигурации вместо dependency injection.

Вот обобщенный пример:

class Db
{ 
    protected $type, $host, $user, $pass, $name;

    public function __construct()
    {
        $this->type = getenv('DB_TYPE');
        $this->host = getenv('DB_HOST');
        $this->user = getenv('DB_USER');
        $this->pass = getenv('DB_PASS');
        $this->name = getenv('DB_NAME');
    }

    public function newConnection()
    {
        return new PDO(
            "{$this->type}:host={$this->host};dbname={$this->name}",
            $this->user,
            $this->pass
        );
    }
}

Конечно, этот пример следует хорошим практикам, скрывая изменяемые данные такие как переменные окружения. Аналогично можно обращаться к $_ENV или $_SERVER вместо getenv(). Однако эффект такой же, как если бы здесь использовались глобальные переменные или service locator - класс обращается за свою область видимости для получения значений, необходимых для его работы. Так же извне класса неизвестно от каких конфигурационных значений зависит класс.

Лучше ли следующий код?

class Db
{
    public function __construct()
    {
        $this->type = Config::get('db.type');
        $this->host = Config::get('db.host');
        $this->user = Config::get('db.user');
        $this->pass = Config::get('db.pass');
        $this->name = Config::get('db.name');
    }
}

Грубо говоря, этот код идентичен прежнему. Здесь общий объект Config выступает в качестве singletone, через который происходит работа с конфигурационными значениями из любого места; он работает как статичный servise locator. Несмотря на то, что service locator является IoC, со многих точек зрения он проигрывает Dependency injection. Как и раньше, класс обращается за свою область видимости чтобы получить необходимые данные.

Что если мы будем явно передавать объект Config, как в коде ниже?

class Db
{
    public function __construct(Config $config)
    {
        $this->type = $config->get('db.type');
        $this->host = $config->get('db.host');
        $this->user = $config->get('db.user');
        $this->pass = $config->get('db.pass');
        $this->name = $config->get('db.name');
    }
}

Этот вариант немного лучше. По крайней мере теперь мы видим, что классу Db необходимы какие-то значение из Config. Но мы до сих пор не можем сказать, какие именно? Это все равно что передать service locator.

Встречая подобные участки кода в реальных проектах я пришел к выводу, что конфигурационные значения должны передаваться, как и любые другие зависимости - через конструктор. Я предлагаю делать это так:

class Db
{
    public function __construct($type, $host, $user, $pass, $name)
    {
        $this->type = $type;
        $this->host = $host;
        $this->user = $user;
        $this->pass = $pass;
        $this->name = $name;
    }
}

Просто, аккуратно очевидно, легко тестируется. Если вы используете DI контейнер, получение значений из окружения и передача их в конструктор класса Db будет тривиальной задачей (Если ваш DI контейнер не умеет так, то вам стоит задуматься о смене его на более мощный).

Кроме того, я считаю, что в некоторых случаях будет разумным сделать так:

class DbConfig
{
    protected $type, $host, $user, $pass, $name;

    public function __construct($type, $host, $user, $pass, $name)
    {
        $this->type = $type;
        $this->host = $host;
        $this->user = $user;
        $this->pass = $pass;
        $this->name = $name;
    }

    public function getDsn()
    {
        return "{$this->type}:host={$this->host};dbname={$this->name}";
    }

    public function getUser()
    {
        return $this->user;
    }

    public function getPass()
    {
        return $this->pass;
    }
}

class Db
{
    protected $dbConfig;

    public function __construct(DbConfig $dbConfig)
    {
        $this->dbConfig = $dbConfig;
    }

    public function newConnection()
    {
        return new PDO(
            $this->dbConfig->getDsn(),
            $this->dbConfig->getUser(),
            $this->dbConfig->getPass()
        );
    }
}

В данном случае, объект DbConfig содержит список внедряемых конфигурационных данных и объект Db получаем необходимые значения в виде отдельной сущности. Тем не менее я считаю такой подход избыточным в большинстве случаев. Существует соблазн начать добавлять в класс DbConfig все больше и больше так, что вы итоге он превратиться в мини service locator.

Итог: Конфигурационные значения тоже являются зависимостью. И передавать их надо таким же способом, как и любые другие зависимости.

Это вольный перевод статьи "Configuration Values Are Dependencies, Too" которую написал Paul M. Jones.

Комментарии

Используйте Markdown
Спасибо за коментарий!
Ваше сообщение будет доступно после проверки.