Статьи

Работа с зависимостями

Композиционный стиль программирования

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

Для кого-то, кто исходит из императивного стиля программирования, этот стиль может показаться абстрактным и запутанным, но преимущества заключаются в гибкости кода. Если различные объекты связаны через композицию, части могут быть заменены без изменения кода. Это облегчает повторное использование компонентов и подключение к коду, предоставляя оболочку здесь или там. Это особенно полезно во время тестирования, так как становится проще моделировать внешние зависимости (такие как база данных или сервер smtp).

Однако у композиции есть темная сторона — зависимости.

Так что же такое зависимость?

Ради этого поста я приведу довольно наивный пример. Предположим, что мы строили приложение адресной книги. Это будет включать сущность типа Person В нашем коде у нас может быть класс для доступа к базовой таблице в базе данных. Изначально это может выглядеть так:

 
class PersonGateway {
  protected $db;
  function __construct() {
    $this->db = new PDO("mysql:host=localhost;dbname=addressbook", "root", "secret");
  }
  function getPerson($id) {
    $stmt = $this->db->prepare("select * from persons where id = :id");
    $stmt->execute(array(':id' => $id));
    return $stmt->fetch();
  }
}

Однако нам также нужен объект типа Address Что нам нужно, так это отдельный объект базы данных, которым могут пользоваться два наших класса шлюза. Этот тип объекта называется зависимостью [1] — классы шлюза называются зависимостями.

Примечания:
[1] В комментариях мне было указано, что здесь я использую зависимость в несколько ином смысле, чем в UML. Тип зависимости, о котором я говорю, работает на уровне экземпляра объекта, а не на уровне класса. Смотрите хорошее объяснение медового монстра.

Не могли бы вы передать мне эту зависимость, пожалуйста?

Грубо говоря, есть только два способа, которыми можно удовлетворить зависимость. Либо зависимость получается через глобальный символ [2], либо она передается извне. Глобальные символы являются наименее абстрактным методом и поэтому часто обращаются к менее опытным программистам. Однако их конкретность также делает их очень негибкими. Глобальные символы, как правило, трудно или невозможно изменить во время выполнения, поэтому многие преимущества агрегирования теряются. Кроме того, глобальные переменные скрывают побочные эффекты, что делает их идеальными сосудами для внесения трудно обнаруживаемых ошибок в ваше приложение. По этим причинам мы передадим зависимости, чтобы наш шлюз выглядел следующим образом:

 
class PersonGateway {
  protected $db;
  function __construct($db) {
    $this->db = $db;
  }
  ...
}

Примечания:
[2] Для глобальных символов я считаю глобальные переменные, константы или статические члены любого типа (включая печально известный синглтон)

Утечка зависимостей

Как правило, вставка зависимостей — хороший инструмент. В нашем приведенном выше примере мы могли бы легко заменить базу данных на макет, если бы мы хотели протестировать ее. Однако это обходится управлением. Предположительно, мы бы сейчас пошли и написали какое-то приложение, которое зависит от нашего шлюза. Некоторое место в будущем, мы думаем, что нам нужно добавить идентификационную карту к шлюзу. Как предписывает композиционный стиль, мы предоставляем их как внешние зависимости, но для этого мы меняем конструктор шлюза.

Теперь нам нужно вернуться назад и отредактировать все места, где создается экземпляр шлюза, и предоставить новые зависимости. Это нехорошо — пользователь не должен беспокоиться об изменениях интерфейса, просто потому, что мы переопределяем внутреннее поведение нашего компонента. И есть еще одна проблема. Теперь пользователь должен знать о классах загадочного звучания, которые его не волнуют. У нас не хватает инкапсуляции.

Написание контейнера

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

 
class Container {
  function new_PDO() {
    return new PDO("mysql:host=localhost;dbname=addressbook", "root", "secret");
  }
  function new_PersonGateway() {
    return new PersonGateway($this->new_PDO(), $this->new_IdentityMap());
  }
  function new_AddressGateway() {
    return new AddressGateway($this->new_PDO(), $this->new_IdentityMap());
  }
}

Но подожди секунду. Этот контейнер создает два соединения с базой данных, когда мы уже решили, что нам нужно только одно. Однако нам нужны два разных экземпляра таблицы идентификаторов, поскольку они должны отслеживать разные типы.

Как оказалось, классы можно разделить на два вида в зависимости от их уникальности в приложении. Есть общие объекты, такие как соединение с базой данных, и есть временные объекты, такие как карта идентификации. И, кроме того, шлюзы сами по себе также являются общими — мы бы не хотели иметь их несколько копий.

Чтобы решить эту проблему, мы разделим наш контейнер на две части: контейнер и фабрика. Контейнер содержит общие экземпляры и предоставляет общедоступный интерфейс, а фабрика содержит методы для создания экземпляров. Простая реализация может выглядеть так:

 
class Container {
  protected $factory;
  protected $instances = array();
  function __construct($factory) {
    $this->factory = $factory;
  }
  function get($classname) {
    $classname = strtolower($classname);
    if (!isset($this->instances[$classname]) {
      $this->instances[$classname] = $this->create($classname);
    }
    return $this->instances[$classname];
  }
  function create($classname) {
    return $this->factory->{'new_'.$classname}($this);
  }
}
class Factory {
  function new_PDO($container) {
    return new PDO("mysql:host=localhost;dbname=addressbook", "root", "secret");
  }
  function new_PersonGateway($container) {
    return new PersonGateway($container->get('PDO'), $container->create('IdentityMap'));
  }
  function new_AddressGateway($container) {
    return new AddressGateway($container->get('PDO'), $container->create('IdentityMap'));
  }
}

Положить контейнер для использования

Здесь есть возможности для улучшения, но у нас уже есть очень прочный контейнер для использования. Одним из больших преимуществ отделения фабрики от контейнера является то, что тривиально отключить выбранные классы во время тестирования. Скажем, я хотел написать unittest для класса PersonGateway и для этого я хотел заменить базу данных заглушкой. Вот как:

 
class TestFactory extends Factory {
  function new_PDO($container) {
    return new MockPdo();
  }
}
class TestOfPersonGateway extends UnitTestCase {
  function setUp() {
    $this->container = new Container(new TestFactory());
  }
  function test_finder_selects_from_database() {
    $statement = new MockPdoStatement();
    $statement->expectOnce("execute");
    $this->container->get('PDO')->expectOnce("prepare");
    $this->container->get('PDO')->setReturnValue("prepare", $statement);
    $gateway = $this->container->get('PersonGateway');
    $gateway->getPerson(42);
  }
}

Поскольку в тестовом примере используется подкласс реальной фабрики, его не нужно обновлять каждый раз, когда я решаю изменить зависимости моего класса. Там также нет скрытых побочных эффектов; Контейнер содержит любые общие экземпляры, поэтому для каждого теста я получаю совершенно новую настройку.

Но это еще не все — закажите сегодня и получите бесплатное пространство имен!

Скоро выйдет PHP 5.3, и одним из наиболее ожидаемых улучшений является введение пространств имен. Только с одним глобальным пространством имен классы PHP имеют тенденцию становиться очень длинными , чтобы избежать столкновений имен. Пространства имен позволяют нам использовать более короткие имена для адресации классов, при этом избегая столкновений имен.

Эта функция была настолько запрошена, что она была перенесена обратно в PHP 5.3, хотя изначально планировалось, что она будет представлена ​​в PHP 6. Хотя нам не нужно ждать PHP 5.3. С нашим контейнером выше легко написать оболочку, которая предоставляет нам пространство имен:

 
class Namespace {
  protected $prefix;
  protected $container;
  function __construct($prefix, $container) {
    $this->prefix = rtrim($prefix, '_') . '_';
    $this->container = $container;
  }
  function get($classname) {
    return $this->container->get($this->prefix . $classname);
  }
  function create($classname) {
    return $this->container->create($this->prefix . $classname);
  }
}

Предполагая, что мы уже написали фабрику, мы можем теперь использовать Zend Framework без очень длинных имен:

 
$container = new Namespace("Zend_Cache_Backend", $container);
$memcached = $container->get("Memcached");

Конечно, реальные пространства имен, поддерживаемые языком, также буферизуют другие символы, кроме имен классов, но в объектно-ориентированном коде их в любом случае будет немного. Исключением являются устаревшие процедурные библиотеки PHP, которых нельзя избежать. В недавнем сообщении Майк Набережный описал, как с этим справиться, совершенно аналогично нашему контейнеру.

Этот пост был переведен на немецкий язык .