Thursday, August 26, 2010

AgileDev, принципы SOLID

Сегодня хотел ещё раз перечитать очень хорошую статью про применение в PHP на практике принципов SOLID, а сайт с этой статьёй недоступен. Достал из web.archive.org, и решил продублировать у себя, т.к. очень уж полезная вещь, эти SOLID.
Оригинал находится на wiki.agiledev.ru.



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


Управление зависимостями в программном коде – слишком огромная тема, чтобы ее можно было осветить в рамках одной статьи. Здесь можно очень много говорить и бесконечно теоретизировать. Я постараюсь не скатиться до очередного урока по основам ООП, но постараюсь в основном здесь изложить мой (и наш, как команды) опыт, связанный с применением ООП, который, так или иначе, связан с управлением зависимостями. Я также коснусь достаточно кратко так называемых 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-объекты, которые допускают отложенное подключение файлов и создание реальных (проксируемых) объектов.


Например:

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  SpeakingTools extends  lmbAbstractTools{ 
  function  sayHello( $name )   { 
    echo “Hello, ”. $name ;
  } 
} 
 
lmbToolkit :: merge ( new  SpeakingTools ( ) ) ;
[ …] 
 
lmbToolkit :: instance ( ) ->sayHello ( ‘Ivan’) ; // echos Hello, Ivan. 
Клиенты не знают, кто именно поддерживает метод sayHello(). В тестах мы легко можем заменить реализацию метода sayHello() другой при помощи метода lmbToolkit :: merge($tools) (преимущество имеет последний tools, который был зарегистрирован в тулките):

class  OtherSpeakingTools extends  lmbAbstractTools{ 
  function  sayHello( $name )   { 
    echo “I don’t know you, ”. $name ;
  } 
} 
 
class  SomeClassTest extends  UnitTestCase{ 
  function  setUp( )   { 
    lmbToolkit :: save ( ) ;
    lmbToolkit :: merge ( new  OtherSpeakingTools( ) ) ; 
  } 
 
  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-кода:

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-а в большинстве случаев им хватит за глаза, а грамотное его применение не снижает качество архитектуры.


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



Ссылки по теме

No comments:

Post a Comment