управление зависимостями Как создать свой сайт > Вебмастеру > Создание своего сайта > Управление зависимостями в PHP-коде

Управление зависимостями в PHP-коде

Кто не может ссужать, тот не должен и брать взаймы.
«Экспансия 1», Юлиан Семёнов.



Управление зависимостями в программном коде — слишком огромная тема, чтобы её можно было осветить в рамках одной статьи. Здесь можно очень много говорить и бесконечно теоретизировать. Я постараюсь не скатиться до очередного урока по основам ООП, но постараюсь, в основном, здесь изложить мой (и наш, как команды) опыт, связанный с применением ООП, который, так или иначе, связан с управлением зависимостями. Я также коснусь, достаточно кратко, так называемых IoC (Inversion of Control) контейнеров — как попытку внести в мир PHP решений из Java. Попытку, пока не слишком удачную.

Данная статья основана на докладе Юдина С. Ю. на phpconf2007.

Качество архитектуры

Признаки загнивающего проекта

    Итак, любой программный код имеет взаимозависимости одних частей от других. Классы требуют наличия других классов, одни функции вызывают другие и т. д. По мере роста любого проекта, взаимозависимостей становится всё больше и больше. Требования к проекту изменяются, разработчики иногда вносят быстрые и не всегда удачные решения. Если зависимостями грамотно не управлять, то проект неизбежно начнёт загнивать. Код становится сложнее понимать, он чаще ломается, становится менее гибким и трудным для повторного использования. В итоге скорость разработки падает, проект сопротивляется изменениям и вот уже среди разработчиков звучат призывы «Давайте всё переделаем! В следующий раз мы сделаем супер-архитектуру». Вот наиболее распространённые признаки плохого или загнивающего в плане кода проекта:

  • Закрепощённость (rigid) — система отчаянно сопротивляется изменениям, невозможно сказать, сколько займёт реализация той или иной функциональности, так как изменения, скорее всего, затронут многие компоненты системы. Из-за этого вносить изменения становится слишком дорого, так как они требует много времени.
  • Неустойчивость, хрупкость (fragile) — система ломается в непредвиденных местах, хотя изменения, которые были проведены до этого, сломанные компоненты явно не затрагивали.
  • Неподвижность или монолитность (not reusable) — система построена таким образом и характер зависимостей таков, что использовать какие-либо компоненты отдельно от других не представляется возможным.
  • Вязкость (high viscosity) — код проекта таков, что сделать что-либо неправильно (грязно «похакать») намного проще, чем сделать что-то правильно.
  • Неоправданные повторения (high code duplication) — размер проекта намного больше, чем он мог бы быть, если бы абстракции применялись чаще.
  • Чрезмерная сложность (overcomplicated design) — проект содержит решения, польза от которых неочевидна, они скрывают реальную суть системы, усложняя её понимание и развитие.

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

Как сделать лучшую архитектуру

    За долгие годы умные люди выработали некоторые основополагающие принципы ООП, соблюдение которых позволяет создавать лучшую архитектуру:

  • Высокое сцепление кода (High Cohesion) — код, ответственный за какую-либо одну функциональность, должен быть сосредоточен в одном месте.
  • Низкая связанность кода (Low Coupling) — классы должны иметь минимальные зависимости от других классов.
  • Указывай, а не спрашивай (Tell, Don’t Ask) — классы содержат данные и методы для оперирования этими данными. Классы не должны интересоваться данными из других классов.
  • Не разговаривай с незнакомцами (Don’t talk to strangers) — классы должны знать только о своих непосредственных соседях. Чем меньше знает класс о существовании других классов или интерфейсов — тем более устойчив код.

    Все эти рекомендации направлены на то, чтобы постараться развести классы по сторонам, сосредоточить сильные взаимосвязи в одном месте и провести чёткие разграничительные линии в коде.

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

  • Принцип персональной ответственности (Single Responsibility Principle) — класс обладает только 1 ответственностью, поэтому существует только 1 причина, приводящая к его изменению.
  • Принцип открытия-закрытия (Open-Closed Principle) — классы должны быть открыты для расширений, но закрыты для модификаций. Кажется, что это невозможно, однако стоит вспомнить шаблон проектирования Strategy и становится более или менее понятно.
  • Принцип подстановки Лискоу (Liskov Substitution Principle) — дочерние классы можно использовать через интерфейсы базовых классов без знания о том, что это именно дочерний класс. Иначе — дочерний класс не должен отрицать поведение родительского класса и должна быть возможность использовать дочерний класс везде, где использовался родительский класс.
  • Принцип инверсии зависимостей (Dependency Inversion Principle) — зависимости внутри системы стоятся на основе абстракций. Модули верхнего уровня не зависят от модулей нижнего уровня. Абстракции не зависят от подробностей.
  • Принцип отделения интерфейса (Interface Segregation Principle) — клиенты не должны попадать в зависимость от методов, которыми они не пользуются. Клиенты определяют, какие интерфейсы им нужны.

    Мы не будем на них останавливаться подробно. Они хорошо освещены в книге Р. Мартина «Быстрая разработка программ». Попробуем лишь составить небольшую логическую цепочку. Итак, принцип персональной ответственности говорит нам о том, что классы должны иметь минимальное количество ответственностей. Как следствие, они будут меньше в размерах. Принцип открытия-закрытия приветствует делегирование вместо изменения кода классов. Принцип подстановки Лискоу не даёт нам рождать высокие деревья наследования, указывая, где с наследованием уже пора заканчивать. Принцип инверсии зависимостей призывает отказываться от статических зависимостей и строить архитектуру на основе интерфейсов, которые определяют, что именно модули нижних уровней должны уметь делать для того, чтобы верхние уровни были довольны. Принцип отделения интерфейса призывает создавать небольшие и чёткие интерфейсы, структуру которых диктуют клиенты. А что в итоге? В итоге в системе, где разработчики следуют этим принципам, больше интерфейсов, небольших классов, много делегирования, часто применяются различные шаблоны проектирования.

Пример

    Рассмотрим небольшой пример. Допустим, нам необходимо реализовать простейший спайдер, который должен обходить сайт по url-ам и класть контент в mysql-индекс.

    Базовое решение будет таким:

class WebSpider{
  protected $indexer;
   function __construct($connection)  {
    $this->indexer = new MySQLIndexer($connection);
  }
  function crowl($url)  {
    $this->_crawlRecursive($uri, $uri);
  }
  function _crawlRecursive($uri, $context_uri) {
    if(!$content = file_get_contents($uri))
      return;
 
    if($this->_isCacheHit($uri))
      return;
 
    $this->_markCached($uri);
 
    $this->indexer->add($content, $url);
 
    foreach($this->_extractUrls($content) as $link)
      $this->_crawlRecursive($link, $uri);
  }
  […]
}

    Через некоторое время нам потребовалось не индексировать определённые страницы. Не долго думая, мы ввели новый класс и расширили поведение:

class WebSpider{
  protected $indexer;
  protected $uri_extractor = true
  function __construct($connection, $exclude_uri = array())  {
    $this->indexer = new MySQLIndexer($connection);
    $this->uri_extractor = new UrlExtractor($exclude_uri);
  }
  function crowl($url)  {
    $this->_crawlRecursive($uri, $uri);
  }
  function _crawlRecursive($uri, $context_uri) {
    if(!$content = file_get_contents($uri))
      return;
 
    if($this->_isCacheHit($uri))
      return;
 
    $this->_markCached($uri);
 
   $this->indexer->add($content, $url);
 
    foreach($this->uri_extractor->extractUrls($content) as $link)
      $this->_crawlRecursive($link, $uri);
  }
  […]
}

    Потом оказалось, что нам нужно обходить ссылки только в определённых доменах, а остальные пропускать. Плюс к тому же теперь MySQL-индекс нас больше не устраивает. Можно было бы изменить поведение UrlExtractor ещё разок и ввести дополнительный параметр в WebSpider, чтобы он мог создавать объект отличного от MySQLIndexer класса, но на этот раз мы этого делать не будем: налицо загнивание класса WebSpider — его приходится менять каждый раз при возникновении новых требований. Вместо этого мы обезопасим класс WebSpider от подобных (конечно, не всех) изменений — мы будем передавать экземпляры экстрактора и индексера в конструктор.

class WebSpider{
  protected $indexer;
  protected $uri_extractor = true
  function __construct($indexer, $uri_extractor)  {
    $this->indexer = $indexer;
    $this->uri_extractor = $uri_extractor;
  }
  function crowl($url)  {
    $this->_crawlRecursive($uri, $uri);
  }
  function _crawlRecursive($uri, $context_uri) {
    if(!$content = file_get_contents($uri))
      return;
 
    if($this->_isCacheHit($uri))
      return;
 
    $this->_markCached($uri);
 
   $this->indexer->add($content, $url);
 
    foreach($this->uri_extractor->extractUrls($content) as $link)
      $this->_crawlRecursive($link, $uri);
  }
  […]
}

    Этим шагом мы избавились от статической зависимости класса WebSpider на классы MySQLIndexer и UriExtractor. Таким образом, мы согласовали наш код с принципом Open-Closed. WebSpider открыт для расширений (ему можно передать любой другой объект, поддерживающий соответствующий интерфейс). Если мы дополнительно введём интерфейсы SpiderIndexer и SpiderUriExtrator и заставим наши MySQLIndexer и UriExtractor их реализовывать — мы приведём наш код в соответствие с принципом инверсии зависимостей: все зависимости в нашем коде будут строиться на основе интерфейсов:

interface SpiderIndexer{
  function index($uri, $content);
}
 
interface SpiderUriExtractor{
  function extractUrls($content);
}
 
class MySQLIndexer implements SpiderIndexer{
  […]
}
 
class UriExtractor implements SpiderUriExtractor {
  […]
}
 
class WebSpider{
  protected $indexer;
  protected $uri_extractor = true
  function __construct(SpiderIndexer  $indexer, SpiderUriExtractor  $uri_extractor)  {
    $this->indexer = $indexer;
    $this->uri_extractor = $uri_extractor;
  }
  function crowl($url)  {
    $this->_crawlRecursive($uri, $uri);
  }
  function _crawlRecursive($uri, $context_uri) {
    if(!$content = file_get_contents($uri))
      return;
 
    if($this->_isCacheHit($uri))
      return;
 
    $this->_markCached($uri);
 
   $this->indexer->add($content, $url);
 
    foreach($this->uri_extractor->extractUrls($content) as $link)
      $this->_crawlRecursive($link, $uri);
  }
  […]
}
// …
$indexer = new MySQLIndexer($connection);
$uri_extractor = new UriExtractor($uri_exclude, $allowed_domains);
$spider = new WedSpider($indexer, $uri_extractor);
$spider->crowl($starting_url);

    Теперь мы получили независимый класс WebSpider, который можно будет использовать на других подобных задачах. Этот код, к тому же, намного проще протестировать.

    Думаю, что здесь всё понятно.

Что такое хорошая архитектура?

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

    По мере накопления опыта (а это не всегда было лёгким процессом) мы пришли к выводу, что самый главный принцип — всё же, пожалуй, KISS — Keep It Simple Stupid и YAGNI — You Arent Gonna Need It. Чем проще решение по архитектуре или просто для использования (но выполняющее свою чётко определённую задачу), тем лучше.

    Исходя из этого, я бы хотел выделить следующие признаки хорошей архитектуры:

  • Низкая стоимость создания и поддержки — иногда самое хорошее решение — самое первое, которое сработало, так как оно не требует много времени и позволяет оценить полученный результат и при необходимости внести коррективы.
  • Простота — чем меньше архитектурных решений, тем лучше. 1 класс, который решает ровно одну проблему здесь и сейчас, возможно, лучше, чем набор из 1 класса, 3-х декораторов, одной фабрики и одного фасада, которые в будущем помогут решить 5 схожих проблем.
  • Очевидность и простота использования — минимум телодвижений, чтобы получить результат.
  • Расширяемость — система легко вбирает новый функционал.
  • Устойчивость — грамотное разделение между компонентами приводит к тому, что ошибки, если они были внесены в результате модификаций, чётко локализуются по своим зонам. Если у вас есть тестовое покрытие, тогда это будет значить, что у вас сломается небольшое количество текстов в одном из пакетов (или на один из классов), вместо всего набора.
  • Возможность повторного использования — низкое количество зависимостей от других компонентов определяет возможности по повторному использованию. Однако, здесь нужно чётко осознавать, какие именно компоненты будут повторно использованы — действовать упреждающе иногда слишком дорого.

    Да, наверное, именно в этом порядке. Простые решения идут впереди, но оставляют шансы на проявления ООП-эго в ситуациях, когда это действительно необходимо.

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

    Ок, с этим пока всё понятно. Идём дальше.

Самые важные зависимости

Звёздные объекты

    В любом проекте существует набор объектов, которые требуются большому количеству клиентов. К таким можно отнести соединение с базой данных, объекты конфигурации, пользователь, система прав, запрос (request) или ответ (response) системы, логгер, кеш — этот список можно продолжать. Раз они требуются большому числу клиентов, значит, они должны быть, как минимум, легко доступны практически в любой точке приложения. Для приложения средних размеров этот список уже большой — всё через конструктор не передашь. Кроме этого, иногда возникает необходимость в том, чтобы сменить, скажем, метод обработки ошибок, драйвер базы данных, механизм кеширования и т. д. Идеально, если наше приложение потребовало бы при этом минимальных модификаций, не затрагивая при этом кода, реализующего бизнес-логику.

    Важным моментом, касающимся «звёздных» объектов, является и то, что они, чаще всего, представляют компонентам приложения доступ к каким-либо глобальным (внешним) ресурсам, таким как платёжная система, база данных, сессионные данные. Исходя из этого, мы должны иметь возможность лениво инициализировать (lazy initialization) эти внешние ресурсы или подключаться к ним как можно позже, а при модульном тестировании — иметь возможность вообще избежать взаимодействия с внешними ресурсами, то есть обеспечить изоляцию (isolation).

    Именно на этих «звёздных» объектах я и хочу остановиться в данном докладе. Наша практика показала, что управление зависимостями для остальных классов намного менее острая проблема. Обычно применение инверсии зависимостей в задачах типа WebSpider происходит само собой. Там главное — не переборщить и стараться предугадать всё и вся. Если вы занимаетесь разработкой через тестирование, где для облегчения тестирования требуется бОльшая декомпозиция, чем обычно, то всё равно 80% проблемных случаев — это всё те же «звёздные» объекты. Для остальных 20% случаев есть набор методов, которые позволяют решать проблемы зависимости без глобальных архитектурных решений. В качестве примера можно назвать фабричные методы, которые в тестах позволят легко подменить реальные классы заглушками или ту же передачу необходимых объектов в конструктор.

Характер зависимостей

    Раз у многих клиентов имеются зависимости от этих популярных объектов, становится важным характер этих зависимостей.

    Характер зависимости может быть

  • Динамическим — когда мы легко можем подменить один объект другим и клиент об этом не узнает, если интерфейсы всё также поддерживаются.

    Пример динамической зависимости:

class Server{ 
  protected $logger
  function __construct(Logger $logger) {
    $this->logger = $logger;
  }
  function serve()  {
    […]
    $this->logger->logOk(‘Served Ok’);
  }
}
  • Статическим — когда класс зависимого объекта указан явно и для смены одного объекта на другой нам нужно поменять исходный код.

    Пример статической зависимости:

class Server{ 
  function serve(){
    […]
    Log :: logOk(‘Served Ok’);
  }
}

    Здесь мы жёстко (статически) привязали класс Server к классу Log (серьёзность нашего «преступления», на самом деле, зависит от реализации класса Log).

Динамический характер зависимостей, очевидно, является предпочтительнее — это диктуется принципом инверсии зависимостей (Dependency Inversion). Р. Мартин так объясняет принцип инверсии зависимости:

  • Избегайте инициализации объектов конкретных классов, объекты должны инициализироваться фабриками.
  • Избегайте композиции или ассоциаций с конкретными классами.
  • Объекты не должны порождаться статичными классами.

    Конечно, это относится не ко всем классам, а только к тем, что имеют склонность к изменению в будущем. Разумеется, эти рекомендации можно не применять к базовым (встроенным в язык) или к стабильным библиотечным классам без внешних зависимостей.

    Рекомендуем вам ознакомиться со статьёй Дениса Баженова , которая прекрасно объясняет, что такое инверсия зависимостей и какие формы она может принимать. Здесь мы коснёмся этого очень кратко, так как нас интересует именно практический аспект его применения в php-приложениях.

    В теории есть 2 основных способа обеспечения инверсии зависимостей:

  • Внедрение (Inject) зависимостей (Dependency Injection, push подход)
  • Получение (Lookup) зависимостей (Service Locator, pull подход)

    Первый подход предполагает, что клиент ведёт себя пассивно по отношению к зависимым объектам — он ждёт, что ему их передадут в конструктор (Constructor Injection), через специальный set-метод (Setter Injection) или напрямую поставят атрибут (Field Injection).

    Пример Constructor Injection:

class Client(){
  protected $server;
 
  public function __construct(Server $server){
    $this->server = $server;
  }
 
  public function action(){
    [...]
    $this->server->serve();
    [..]
  }
}

    Второй подход предполагает наличие какого-то источника, откуда клиент может брать нужные ему объекты.

    Пример использования Service Locator:

class Client(){
  protected $server;
 
  public function __construct(){
    $this->server = Locator :: instance()->getServer();
  }
 
  public function action(){
    [...]
    $this->server->serve();
    [..]
  }
}

    Для нас сейчас важна суть принципа инверсии зависимостей — иметь возможность смены в клиентском коде конкретного экземпляра «звёздного» объекта на объект другого класса, реализующего тот же интерфейс.

Реалии PHP

    Вот несколько наиболее распространённых в PHP способов обеспечения связи клиентского кода со «звёздными объектами»:

  • Глобальные переменные — самый простой и самый распространённый в ранних php-приложениях способ, да и сейчас такой способ часто используют. Этот способ можно в принципе отнести к pull-приёму.
  • Передача объектов по цепочке — решение, при котором часто используемые объекты передаются по цепочке на сколь угодную глубину. Нами этот способ раньше использовался, например, для передачи объектов Запроса и Ответа приложения. В целом этот способ имеет слишком большие недостатки тем, что раздувает количество передаваемых параметров в конструкторы или методы классов. Пытаясь преодолеть этот недостаток, некоторые вводят понятие контекста. Такой подход предусматривает создание такого весьма тяжёлого контейнера, который в себе хранит (может также отвечать за инициализацию) все нужные приложению объекты. Этот контекст передаётся между всеми объектами приложения, если тем нужно что-либо из этого контекста. Как правило, контекст содержит чёткий, предопределённый набор объектов и чёткий интерфейс. Такой подход применяется в php-фреймворках Symfony и CodeIgniter. Передача объектов по цепочке — push-приём (Constructor Injection или Setter Injection). Хотя, в случае с контейнером — уже двойной Inject + Lookup.
  • Реализация через паттерн одиночки (Singleton) — почему-то очень многие разработчики применяют этот паттерн для обеспечения глобального доступа к часто используемым объектам. Наверное, потому что этот паттерн очень легко реализовать и применить, а затем можно говорить, что я применяю шаблоны проектирования. На заре нашей практики мы тоже реализовывали через одиночки почти всё, что подворачивалось под руки и что лень было передавать по цепочке через 2 или более уровней — другого способа мы не знали. Потом оказалось, что с ними не слишком удобно при тестировании и на самом деле одиночки — это пример статической зависимости, чего хотелось бы избежать. Pull-подход.
  • Глобальное хранилище, реестр (Registry) — чаще всего реестр реализуют через полностью статический класс, который содержит основные методы set и get. Реестр, по сути, это обычный глобальный массив, обвёрнутый в статический класс. Иногда реестр дополнительно наделяют функционалом для осуществления мгновенного слепка текущего состояния с тем, чтобы это состояние можно было легко вернуть обратно в любой момент — такой функционал часто востребован в модульном тестировании. Реестр — это pull-подход.

    Я сюда не включил Inversion of Control контейнеры, так как они являются нетипичным приёмом, мы их рассмотрим отдельно.

Глобальные переменные

    Глобальные переменные имеют самый большой недостаток — они слишком уязвимы. Любой кусок кода может изменить содержимое глобальной переменной и мы можем долго искать, где же именно это произошло. Иногда в таких ситуациях помогает отладчик, но это при условии, что мы обладаем единой точкой входа на установку глобальной переменной, т. е. в случае использования proxy-объекта или набора специализированных функций.

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

    Глобальные переменные, в принципе, могут обеспечить нас и ленивой инициализацией, и динамическим характером связей, если использовать специальные proxy-объекты, которые допускают отложенное подключение файлов и создание реальных (проксируемых) объектов.

    Например:

abstract class BaseProxy
{
  protected $is_resolved = false;
  protected $original;
 
  abstract protected function _createOriginalObject();
 
  function resolve(){
    if($this->is_resolved)
      return $this->original;
 
    $this->original = $this->_createOriginalObject();
    $this->is_resolved = true;
 
    return $this->original;
  }
 
 function __call($method, $args = array()){
    $this->resolve();
    if(method_exists($this->original, $method))
      return call_user_func_array(array($this->original, $method), $args);
  }
 
  function __get($attr){
    $this->resolve();
    return $this->original->$attr;
 }
 
  function __set($attr, $val) {
    $this->resolve();
    $this->original->$attr = $val;
  }
}

    Конкретные прокси-классы должны перекрывать метод _createOriginalObject(). Такие же прокси можно использовать и с Registry для обеспечения lazy initialization. При желании можно легко выделить интерфейс, который будет реализовать и proxy, и реальный рабочий класс.

Одиночки (Singletons)

    Одиночки (Singleton) — реализация часто используемого класса через одиночку имеет один очень сильный недостаток — она связывает клиентов с классом одиночки статически, так как название класса одиночки указывается в коде явно. Из-за этого провести изоляцию, например, при тестировании — очень сложно.

    Есть некоторый набор рекомендаций, которые позволяют смягчить эту проблему:

  • Введение метода setInstance(), который позволяет замещать instance одиночки на другой. Если такой метод существует, тогда, по сути, одиночка превращается в интерфейс и его базовую имплементацию, которую можно заменить.
  • Есть ещё решение, когда свои методы одиночка делегирует другому объекту, поддерживающему тот же интерфейс. Например:
interface Logger {
  function logOk($message);
}
 
class Log implements Logger {
  protected $driver;
 
  function instance(){...}
  function logOk($message){$this->driver->logOk($message); }
 
  function setDriver($driver){$this->driver = $driver;}
}

Передача контейнера по цепочке

    Передача объектов по цепочке с использованием контейнера — в целом, очень неплохой приём, который обеспечивает чёткий интерфейс получения «звёздных» объектов клиентским кодом. При необходимости расширения базового контейнера можно создать дочерний класс. Такой контейнер позволяет легко обеспечить lazy initialization часто используемых объектов.

    В Symfony, правда, контейнер к тому же реализован через одиночку, то есть он может быть получен вообще в любой точке приложения:

class sfContext{
  protected
    $controller        = null,
    $databaseManager   = null,
    $request           = null,
    $response          = null,
    $storage           = null,
    $logger            = null,
    $user              = null;
 
  protected static
    $instance          = null;
 
  protected function initialize()  {
    $this->logger = sfLogger::getInstance();
    if (sfConfig::get('sf_logging_enabled'))    {
      $this->logger->info('{sfContext} initialization');
    }
 
    if (sfConfig::get('sf_use_database')) {
      // setup our database connections
      $this->databaseManager = new sfDatabaseManager();
      $this->databaseManager->initialize();
    }
  }
  public static function getInstance() {
    if (!isset(self::$instance)){
      $class = __CLASS__;
      self::$instance = new $class();
      self::$instance->initialize();
    }
    return self::$instance;
  }
  public static function hasInstance()  {
    return isset(self::$instance);
  }

  public function getResponse() {
    return $this->response;
  }
  public function setResponse($response) {
    $this->response = $response;
  }
  […]
}

    Я нашёл следующие недостатки глобальных контейнеров:

  • Практика показала, что в такие контейнеры также загоняются и дополнительные сервисные и фабричные методы. В тестах иногда нужно иметь возможность изменить поведение этих методов. Однако при реализации контейнера, как в Symphony, мы имеем, фактически, статическую зависимость клиентского кода от класса контейнера и эту подмену осуществить не удастся.
  • Если же контейнер не является одиночкой, а передаётся всегда явно, тогда нужно реализовать метод его получения из любого места приложения, так как делать метод setContext($context) в каждом классе не слишком приятно.

    В принципе, для преодоления этих недостатков, достаточно сделать главный контейнер, в котором будет храниться набор конкретных контейнеров. В этом случае получится вариант решения, который мы долго использовали в Limb3. До появления современной версии пакета Toolkit (об этом чуть ниже).

Реестр (Registry)

Реестр (Registry) — объектная форма обычного ассоциативного массива, которую иногда реализуют в виде полностью абстрактного класса или через одиночку.

class Registry {
    var $_cache_stack;
 
    function Registry() {
        $this->_cache_stack = array(array());
    }
    function setEntry($key, &$item) {
        $this->_cache_stack[0][$key] = &$item;
    }
    function &getEntry($key) {
        return $this->_cache_stack[0][$key];
    }
    function isEntry($key) {
        return ($this->getEntry($key) !== null);
    }
    function &instance() {
        static $registry = false;
        if (!$registry) {
            $registry = new Registry();
        }
        return $registry;
    }
    function save() {
        array_unshift($this->_cache_stack, array());
        if (!count($this->_cache_stack)) {
            trigger_error('Registry lost');
        }
    }
    function restore() {
        array_shift($this->_cache_stack);
    }
}

    Обратите внимание на методы save() и restore() — эти методы позволяют легко изолировать состояния реестра в тестах друг от друга. Например:

class MyTest extends UnitTestCase {
    function MyTest() {
        $this->UnitTestCase();
    }
    function setUp() {
        $registry = Registry::instance();
        $registry->save();
        $this->user = new MockUser();
        $registry->setEntry(‘user’, $this->user)
    }
    function tearDown() {
        $registry = Registry::instance();
        $registry->restore();
    }
    function testStuffThatUsesTheRegistry() {
        ...
    }
}

    Недостаток Registry — в нечётком содержимом, когда нельзя понять, что в данный момент хранится в реестре, а чего нет. Плюс необходимость дополнительных телодвижений для организации отложенной инициализации (lazy initialization).

Limb Toolkit — Service Locator "по-нашему"

Старый вариант

    Начав с одиночек, мы быстро осознали их недостатки в модульном тестировании и попробовали реализовать версию Service Locator, которая бы была удобной в использовании и не мешала тестированию.

    Первым решением стал глобальный контейнер (видоизмененный Registry), которых хранит набор других контейнеров или тулкитов.

    Всё это выглядело следующим образом:

class Limb{
  var $_toolkits = array(array());
 
  function instance(){…}
 
  function register ($toolkit, $name = 'default'){
    $instance = Limb :: instance();
    array_push($instance->toolkits[$name], $toolkit);
  }
 
  function restore($name = 'default') {
    $instance = Limb :: instance();
    if (isset($instance->toolkits[$name]))
      return array_pop($instance->toolkits[$name]);
  }
 
  function toolkit($name = 'default') {
    $instance = Limb :: instance();
    if (isset($instance->toolkits[$name]))
      return end($instance->toolkits[$name]);
  }
 
  function save($name = 'default') {    
    $toolkit = clone(Limb :: toolkit($name));
    $toolkit->reset();
    Limb :: register ($toolkit, $name);
  } 
}

class ServiceToolkit {
  var $logger;
  function getLogger() {
    if(!is_object($this->logger))
      $this->logger = new DefaultLoggerClass();
    return $this->logger;
  }
  function setLogger($logger)
  {
    $this->logger = $logger;
  }
}

ServiceToolkit нужно было регистрировать в Limb:

$toolkit = new ServiceToolkit();
Limb :: register($toolkit, 'service');

    Для получения объекта-логгера использовался следующий код:

$toolkit = Limb :: toolkit('service');
$db = $toolkit->getLogger();

Новый вариант

    Данное решение не имело недостатков глобального контейнера. Наподобие того, что используется в Symfony. Долгое время оно нас устраивало, пока каждый раз указывать имя toolkit-а нам не надоело, а их количество не стало заметно расти. Мы решили, что на самом деле toolkit — он всего один, а его интерфейс должен формироваться динамически. Получилась новая версия, архитектура которой выглядит следующим образом.

    Идея, в целом, осталась та же. Есть глобальный контейнер — lmbToolkit (тулкит или ящик с инструментами), выполненный через одиночку, в него через методы merge() и extend() добавляются инструменты (tools). При помощи методов save() и restore() можно создавать мгновенную копию состояния тулкита, запоминать её в стеке и восстанавливать при необходимости — это используется в тестах для изоляции изменений в тулките рамками одного теста.

    В lmbToolkit перекрыт метод _ _call() и он делегирует все вызовы соответствующим tools, которые в него были до этого добавлены:

class LogTools extends lmbAbstractTools
{
  protected $logger;
 
  function getLogger()
  {
    if(!is_object($this->logger))
      $this->logger = new DefaultLoggerClass();
    return $this->logger;
  }
  function setLogger($logger)
  {
    $this->logger = $logger;
  }
}
 
lmbToolkit :: merge(new LogTools ());
[…]
 
lmbToolkit :: instance()->getLogger(); // DefaultLoggerClass

    Клиенты не знают, кто именно поддерживает метод getLogger(). В тестах мы легко можем заменить реализацию метода getLogger() другой при помощи метода lmbToolkit :: merge($tools) (преимущество имеет последний tools, который был зарегистрирован в тулките):

class OtherLoggerTools extends lmbAbstractTools
{
  function getLogger()
  {
    if(!is_object($this->logger))
      $this->logger = new AnotherLoggerClass();
    return $this->logger;
  }
}
 
class SomeClassTest extends UnitTestCase{
  function setUp()  {
    lmbToolkit :: save();
    lmbToolkit :: merge(new OtherLoggerTools()); 
  }
 
  function tearDown()  {
    lmbToolkit :: restore();
  }
  [...]
}

    Благодаря методам save() и restore() изменения в составе tools будут изолированы лишь каждым тестовым методам.

    Тулкит имеет как достоинства, так и недостатки.

Плюсы:

  • Легко расширяется.
  • Избавляет клиентов от знаний, кто именно реализует нужные им методы.
  • Позволяет иметь в инструментах (tools) любые методы, в том числе фабричные и сервисные.
  • Отлично обеспечивает изоляцию в тестах.
  • Позволяет заменять инструменты в тестах только частично.

Минусы:

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

    Тулкит используется обычно в рабочем коде конечных приложений, а также в фасадных классах пакетов. На более низких уровнях мы стараемся обходиться простой передачей нужных объектов через конструкторы.

Inversion of Control (IoC) контейнеры

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

    Общепринятого русского термина для IoC-контейнера пока нет, поэтому будем использовать «IoC-контейнер».

IoC-контейнеры получили распространение в Java. Самые известный, пожалуй, это Spring и Pico. Для Pico-контейнера есть порт под PHP, который написал Павел Козловский, об этом порте мы расскажем чуть ниже.

    Рассмотрим небольшой пример и покажем, как используются IoC-контейнеры.

public interface Server {
    void serve(Object client);
}
 public class ConcreteServer implements Server {
    public void serve(Object client) {
        System.out.println("I’m serving a client " + client);
    }
}
 public class Client {
    Server server;
    public Client(Server server) {
        this.server = server;
    }
    public void action() {
        server.serve(this);
    }
}

В случае применения Pico на Java связь Client и ConcreteServer будет выглядеть следующим образом:

MutablePicoContainer pico = new DefaultPicoContainer();
pico.registerComponentImplementation(Server.class, ConcreteServer.class);
pico.registerComponentImplementation(Client.class);
 
Client client = (Client) pico.getComponentInstance(Client.class);
client.action();

    Pico самостоятельно определит, что в качестве параметра для конструктора класса Client подходит класс ConcreteServer, так как он реализует интерфейс Server.

В Spring же связи можно описывать XML-файлом:

<beans>
 
  <bean id="server"
    class="com.my_app.ConcreteServer"/>
 
  <bean id="client"
    class="com.my_app.Client">
    <property name="server">
      <ref bean="server"/>
    </property>
  </bean>
</beans>

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

BeanFactory factory = new XmlBeanFactory(new FileInputStream("dependency.xml"));
Client client = (Client)factory.getBean("client");
client.action();

    IoC-контейнеры используются на самых верхних уровнях приложения, например для сборки всех фасадов в единое целое.

    Контейнеры используются следующим образом:

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

Преимущества контейнеров для PHP-приложений, да и вообще IoC-контейнеров, весьма спорны. Возможно, это одна из причин, по которой внедрение зависимостей (DependencyInjectino) не получило широкого распространения в php-framework-ах, в отличие от lookup-подхода, который выглядит проще и очевиднее в использовании.

Ioc-контейнеры для PHP

    Как бы то ни было, рассмотрим 2 решения, которые существуют для PHP и которые являются попытками перенести DI из Java:

  • PHP-порт Pico Container
  • Phemto

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

Pico Container for PHP

    Оригинальный Pico Container позиционирует себя как минималистический IoC-контейнер, в отличие от того же Spring.

    Павел Козловский сделал попытку создать порт под PHP этого проекта. Первый релиз был сделан в начале 2005, более или менее активная разработка прекратилась в начале 2006 года.

    К сожалению, на порт нет практически никакой документации, которая может пролить свет на тонкости использования php-версии контейнера, кроме модульных тестов.

    Рассмотрим небольшой пример настройки контейнера, когда класс зависит от 2-х объектов, реализующих один и тот же интерфейс.

interface Touchable {
    public function touch();
}
 
 
class SimpleTouchable implements Touchable{
    public function touch()     {
        $this->wasTouched = true;
    }
}
 
class AlternativeTouchable implements Touchable{
    public function touch()     {
        $this->wasTouched = true;
    }
}
 
class SeveralDependanciesWithInterfaces{
	private $simpleTouchable;
	private $alternativeTouchable; 
 
	function __construct(Touchable $simpleTouchable, Touchable $alternativeTouchable)	{
		$this->simpleTouchable = $simpleTouchable;
		$this->alternativeTouchable = $alternativeTouchable;
	}
 
	function touch()	{
		$this->simpleTouchable->touch();
		$this->alternativeTouchable->touch();
	}
 
	function getAlternativeTouchable()	{
		return $this->alternativeTouchable;
	}
}

    В этом случае настройка Pico будет выполнена след. образом:

$pico = new DefaultPicoContainer();
$pico->regComponentImpl('SimpleTouchable');
$pico->regComponentImpl('AlternativeTouchable');
$pico->regComponentImpl(
	'SeveralDependanciesWithInterfaces', //key 
	'SeveralDependanciesWithInterfaces', //class name
    	array('simpleTouchable' => new BasicComponentParameter('SimpleTouchable'),
    	'alternativeTouchable' => new BasicComponentParameter('AlternativeTouchable')));    			  
$ci = $pico->getComponentInstance('SeveralDependanciesWithInterfaces');

    Самый большой недостаток здесь — раздутый синтаксис, особенно когда необходимо обеспечить позднее подключение php-кода:

$pico = new DefaultPicoContainer();		
$pico-">regComponent(new LazyIncludingComponentAdapter(
			new ConstructorInjectionComponentAdapter('LazyIncludeModelWithDpendencies'),
			‘path/to/first_class.php’));
		$pico->regComponent(new LazyIncludingComponentAdapter(
			new ConstructorInjectionComponentAdapter('LazyIncludeModelDependend'),
			‘path/to/second_class.php’));

PHP Pico — наиболее законченное DI решение, которое существует для PHP (на момент написания этой статьи). PHP Pico поддерживает Constructor Injection и базово — Setter Injection. Есть проблемы с созданием декораторов, но в целом, если у вас появится желание поиграть с DI для PHP — модульных тестов на PHP Pico должно хватить, чтобы нормально разобраться, как этот инструмент завести.

Phemto

    Автор Phemto — Маркус Бейкер, создатель пакета для тестирования SimpleTest.

    Phemto — наверное, самый минимальный инструмент для реализации DI (250 строк кода), который можно было придумать.

    Документации нет, вместо неё — модульные тесты.

    Как и в случае с PHP Pico, развитие Phemto не ведётся уже больше года.

    Использование Phemto имеет более компактный вид, чем Pico:

    interface Number {
        function getValue();
    }
 
    class One implements Number {
        function getValue() { return 1; }
    }
 
    class Two implements Number {
        function getValue() { return 2; }
    }
 
    class Adder implements Number {
        public $result;
 
        function __construct(One $a_one, Two $a_two) {
            $this->result = $a_one->getValue() + $a_two->getValue();
        }
 
        function getValue() {
            return $this->result;
        }
    }
       $injector = new Phemto();
   $injector->register('One');
   $injector->register('Two');
   $injector->register('Adder');
   $result = $injector->instantiate('Adder');
   $this->assertEqual($result->result, 3);

    Phemto также поддерживает Lazy Include (с первоначальным кешированием).

Выводы

    Хорошая архитектура формируется не сразу, а под влиянием требований. Главным условием формирования хорошей архитектуры — просто не давать коду загнивать, что достигается рефакторингом и проверкой при помощи тестов.

    Что касается IoC-контейнеров, вывод здесь можно сделать следующий — IoC-контейнеры не получили для PHP широкого распространения. Причин здесь несколько:

  • Неочевидные преимущества IoC-контейнеров даже для тех, кто широко применяет TDD. Service Locator гораздо проще и нагляднее, несмотря на то, что он прячет зависимости клиентов. Для тестов проще огранизовать обходные пути, чтобы обеспечить возможность изоляции, чем заморачиваться с DI. KISS и YAGNI победили.
  • IoC-контейнеры на самом деле могут быть источником дополнительных проблем, ошибки в них ловить достаточно сложно.
  • Область применения IoC-контейнеров невелика — верхний уровень достаточно больших приложений. Для библиотек применение IoC неоправданно — достаточно организовать явную передачу нужных параметров в конструкторы.
  • Дизайн, ориентированный на DI, на самом деле, выставляет много деталей на показ, которые большинству клиентов не нужны.

    Кстати, это относится не только для PHP, это характерно также и для Java, и для других языков. IoC-контейнеры были незначительное время hype-ом. Затем многие осознали, что возможностей Service Locator-а, в большинстве случаев, им хватит за глаза, а грамотное его применение не снижает качество архитектуры.

    Для того, чтобы повысить качество архитектуры, достаточно избавиться от статических зависимостей на классы, работающие с внешними ресурсами, и внедрить механизм по динамической доставке популярных объектов до клиентов.

Автор: Вики.Агилдев.Ру.

Комментарии:


⇓ 

Поделись ссылкой на Seoded.ru с друзьями, знакомыми и собеседниками в соцсетях и на форумах! А сам сайт добавь в закладки! Так победим.

Поделиться ссылкой на эту страницу в:

Полезные ссылки:

Все сайты зависят от хороших статей Заработок фотографа зависит от этого...

Ещё материалы по этой теме:

Инверсия зависимостей Registry вместо Singleton Статические вызовы Прощай Smarty Графическое меню на CSS
основан в 2008 г. © Все права на материалы сайта Seoded.ru принадлежат Алексею Вострову.
Копирование (полное или частичное) любых материалов сайта возможно только с разрешения автора и при указании ссылки на источник.
Ослушавшихся находит и забирает Бабайка!