инверсия зависимостей Как создать свой сайт > Вебмастеру > Создание своего сайта > Инверсия зависимостей

Инверсия зависимостей при проектировании Объектно-Ориентированных систем

И в Германии люди говорили, что хотели, но только шёпотом…
«Экспансия 1», Юлиан Семёнов.



Введение

    Можно бесконечно спорить о том, как правильно проектировать Объектно-Ориентированные системы и какие подходы и методики надо использовать. Но с одним утверждением согласны все опытные проектировщики: правильная декомпозиция системы и распределение обязанностей между объектами — очень важная и ответственная часть работы над любым проектом.

    В этой статье я опишу концепцию инверсии зависимостей. Эта методика позволяет сделать компоненты приложения менее связанными между собой. Благодаря этому, повышается возможность повторного использования кода и его мобильность.

    Данный материал необходим тем, кто занимается проектированием CMF/CMS-систем. Очень тяжело встретить framework-систему, которая бы не использовала принципа инверсии зависимостей. Тем не менее, даже если вы не занимаетесь написанием таких фундаментальных вещей, как каркасные системы, вам будет полезно ознакомиться с информацией, излагаемой в данной статье. Она должна дать ещё один ключ от чулана, где покоится хороший дизайн.

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

    Размышляя о хорошем дизайне архитектуры, нельзя не вспомнить и плохой дизайн. Все мы сталкивались с плохим дизайном. Некоторые из нас говорили своим коллегам: «Это не очень удачное решение». Некоторые слышали это в свой адрес. Но сколько из нас задумывалось над тем, какие черты присущи плохому дизайну?

    Выделяют, по меньшей мере, три характерных признака плохого дизайна архитектуры:

  • Жёсткость;
  • Хрупкость;
  • Монолитность.

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

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

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

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

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

Инверсия зависимостей

    Сразу оговорюсь, что инверсия зависимостей (Dependency Inversion, далее DI) выросла из инверсии контроля (Inversion of Control, далее IoC), которая применяется не только в объектном подходе, но и в процедурном.

    В начале 2004 года в своей статье Мартин Фаулер рассмотрел явление IoC в контексте ООП. Основываясь на мыслях, изложенных в данной статье, он подобрал более удачное определение для IoC в мире ООП: инъективная зависимость (Dependency Injection) или инверсия зависимостей (Dependency Inversion).

Инверсия зависимости — это особый вид IoC, который применяется в Объектно-Ориентированном подходе для удаления зависимостей между классами. Зависимости между классами превращаются в ассоциации между объектами. Ассоциации между объектами могут устанавливаться и меняться во время выполнения приложения. Это позволяет сделать модули менее связанными между собой. Ниже мы рассмотрим пример, который объяснит суть вышесказанного.

Принцип инверсии зависимостей

  1. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба типа модулей должны зависеть от абстракций;
  2. Абстракция не должна зависеть от реализации. Реализация должна зависеть от абстракции.

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

    Рассмотрим пример программы, которая копирует в файл данные, введённые с клавиатуры.

    У нас есть три модуля (в данном случае это функции). Один модуль (иногда его называют сервис) отвечает за чтение с клавиатуры. Второй — за вывод в файл. А третий — высокоуровневый — модуль объединяет два низкоуровневых модуля с целью организации их работы.

зависимости между модулями без использования инверсии зависимостей

    Ваш модуль copy может выглядеть примерно следующим образом.

while ( ($data = readKeyboard()) !== false )
{
  writeFile("./filename", $data);
}

    Низкоуровневые модули readKeyboard и writeFile обладают высокой гибкостью. Мы легко можем использовать их в контексте, отличном от функции copy. Однако, сама функция «copy» не может быть повторно использована в другом контексте. Например, для отправки данных из файла системному обработчику логов.

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

зависимости между модулями после применения принципа инверсии зависимостей
interface IReader
{
  public function read();
}
 
interface IWriter
{
  public function write($data);
}

    Модуль copy должен полагаться только на выработанные абстракции и не делать никаких предположений по поводу индивидуальных особенностей объектов ввода/вывода.

while ( ($data = $reader->read()) !== false )
{
  $writer->write($data);
}

    Примерно следующим образом выглядит использование нашего модуля пользователем.

$copier = new copier();
 
// Копирование данных с клавиатуры в файл
$copier->run(new keyboardReader(), new fileWriter('./filename'));
 
// Отправка данных из файла системному обработчику логов
$copier->run(new fileReader('./filename'), new syslogWriter());

    Теперь модуль copy можно использовать в различных контекстах копирования. Изменение поведения модуля-копировщика достигается путём ассоциации его с объектами других классов (но которые зависят от тех же абстракций).

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

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

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

Формы инверсии зависимостей

    Существует две формы инверсии зависимостей: активная и пассивная. Различие между ними состоит в том, как объект узнаёт о своих зависимостях во время выполнения.

    Ври использовании пассивной формы зависимые объекты «впрыскиваются» в зависимый. Зависимому объекту не надо прилагать никаких усилий, все нужные сервисы он получает через свой интерфейс.

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

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

  • Пассивная инверсия зависимостей:
    • constructor injection;
    • setter injection;
    • interface injection;
    • field injection.
  • Активная инверсия зависимостей (Dependency Lookup):
    • pull approach;
    • push approach.

Constructor Injection

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

class copier...
 
 public function __construct(IReader $input, IWriter $output) {
  $this->input = $input;
  $this->output = $output;
 }
}
 
$copy = new copier(new inputDriver(), new outputDriver());

Setter Injection

    Инъекция при помощи set-метода требует от вас определения отдельного set-метода для каждого из инъецируемых объектов. От предыдущего типа инъекции она отличается местом инъецирования.

class copier...
 
 public function setInput(IReader $input) {
  $this->input = $input;
 }
 public function setOtput(IWriter $output) {
  $this->output = $output;
 }
}
 
$copy = new copier();
$copy->setInput(new inputDriver());
$copy->setOutput(new outputDriver());

    Отмечу, что construction injection и setter injection не исключают друг друга.

class copier...
 
 public function __construct(IReader $input, IWriter $output) {
  $this->setInput($input);
  $this->setOutput($output);
 }
 
 public function setInput(IReader $input)...
 public function setOtput(IWriter $output)...
}

Interface Injection

Interface injection использует интерфейсы для осуществления связывания объектов.

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

interface injectReader
{
  public function injectReader(IReader $obj);
}
 
interface injectWriter
{
  public function injectWriter(IWriter $obj);
}

    Зависимый объект должен реализовывать все эти интерфейсы.

class copier
  implements injectReader, injectWriter...
 
  public function injectReader(IReader $obj)
  {
    $this->reader = $obj;
  }
 
  public function injectWriter(IWriter $obj)
  {
    $this->writer = $obj;
  }
}

    Определяется также единый интерфейс для всех сервисов.

interface injector
{
  public function inject($object);
}

    Каждый сервис реализует этот интерфейс таким образом, чтобы внедрить себя в зависящий объект.

class keyboardReader implements injector...
  public function inject($object)
  {
    if ( !$object instanceof injectReader )
    {
      throw new typeException($object);
    }
    $object->injectReader($this);
  }
  [...]
}
 
class fileWriter implements injector...
  public function inject($object)
  {
    if ( !$object instanceof injectWriter )
    {
      throw new typeException($object);
    }
    $object->injectWriter($this);
  }
  [...]
}

    Таким образом, сервисы сами внедряют себя в зависимый объект посредством установленного интерфейса.

$reader = new keyboardReader();
$writer = new stdoutWriter();
 
$copier = new copier();
 
$reader->inject($copier);
$writer->inject($copier);

Field Injection

    В некоторых языках программирования (Java/C#) существует возможность получить доступ к private/protected полям объекта. Эта техника может быть использована для внедрения сервисов в зависящий объект напрямую, без использования set-методов и конструкторов.

    Установить значение private переменной в Java можно следующим образом.

// bastion.java
public class bastion {
  private String str;
  public bastion() {
    str = "Catch me if you can";
  }
}
 
// Main.java
import java.lang.reflect.*;
 
public class Main {
  public static void main(String[] args) throws Exception {
    bastion obj = new bastion();
    Class type = obj.getClass();
    Field field = type.getDeclaredField("str");
 
    field.setAccessible(true);
    field.set(obj, "No problem");
    field.setAccessible(false);
  }
}

    Аналогичным образом, при помощи интроспекции, решается эта задача на C#.

using System;
using System.Reflection;
 
namespace privateAccessor
{
  class bastion {
    private String str;
    public bastion() {
      str = "Catch me if you can";
    }
  }
 
  class Application
  {
    [STAThread]
    static void Main(string[] args)
    {
      bastion obj = new bastion();
      Type type = obj.GetType();
 
      type.InvokeMember(
        "str",
        BindingFlags.SetField|BindingFlags.NonPublic|BindingFlags.Instance,
        null,
        obj,
        new object[]{"No problem"} 
      );
    }
  }
}

Dependency Lookup: Pull approach

    Поиск зависимостей по методу pull approach предполагает наличие в системе общедоступного объекта, который знает обо всех используемых сервисах. В качестве такого объекта может выступать объект, реализующий паттерн serviceLocator.

    Давайте взглянем на пример нашего класса copier, который использует serviceLocator.

class copier...
 
 public function __construct() {
  $input = locator::getInstance()->getReader();
  $output = locator::getInstance()->getWriter();
 
  $this->setInput($input);
  $this->setOutput($output);
 }
}

    Локатор реализует паттерн singleton. Благодаря этому доступ к нему можно получить из любого места приложения.

    Теперь все зависмости инкапсулированы внутри объекта локатора. Соответственно, если мы хотим изменить поведение объекта copier, мы должны заполнить локатор нужными нам объектами.

$locator = locator::getInstance();
 
$locator->setReader(new keyboardReader());
$locator->setWriter(new fileWriter('./filename'));
 
$copier = new copier();
$copier->run();

    Сам локатор выглядит следующим образом.

class locator {
    private $reader;
    private $writer;
 
    public static function getInstance() {
        // Singleton handler
    }
 
    public function getReader() {
        return $this->reader;
    }
 
    public function getWriter() {
        return $this->writer;
    }
 
    public function setReader(IReader $reader) {
        $this->reader = $reader;
    }
 
    public function setWriter(IWriter $writer) {
        $this->writer = $writer;
    }
}

    Это локатор с явным интерфейсом. Аналогом локатора, но с неявным интерфейсом, является паттерн registry. Локатор с неявным интерфейсом немного выигрывает в гибкости кода. Однако, с таким подходом связана проблема начальной инициализации локатора объектами. Но и у явного интерфейса есть свои минусы. Явный интерфейс обязывает вас описывать в коде все зависимости.

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

class readerLocator extends locatorPiece {
    private $reader;
 
    public function getReader() {
        return $this->reader;
    }
 
    public function setReader(IReader $reader) {
        $this->reader = $reader;
    }
}
 
class writerLocator extends locatorPiece {
    private $writer;
 
    public function getWriter() {
        return $this->writer;
    }
 
    public function setWriter(IWriter $writer) {
        $this->writer = $writer;
    }
}

    Теперь надо собрать основной локатор.

$locator = serviceLocator::getInstance();
 
$locator->addLocator(new readLocator());
$locator->addLocator(new writeLocator());
 
$source = $locator->getReader();
$destination = $locator->getWriter();

    Таким образом, набор сервисов локатора определяется его составными частями.

    Реализация такого локатора не должна вызвать у вас проблем. И всё же если это произошло, то вы можете обратиться к готовому решению.

Dependency Lookup: Push approach

    Методика push approach отличается от pull approach тем, как модуль узнаёт об объекте-локаторе. При использовании pull approach модуль сам получал локатор посредством метода-одиночки. Push approach характеризуется тем, что объект-локатор (или, как его иногда называют, context) передаётся в модуль извне.

class copier...
  public function __construct(IApplicationContext $context) {
    $this->context = $context;
  }
 
  public function run() {
    $input = $this->context->getReader();
    $output = $this->context->getWriter();
 
    [...]
  }
}

    Фактически данный подход похож на constructor injection + dependency lookup: pull approach. То есть для поиска сервиса используется локатор, но сам локатор внедряется в объект при помощи constructor injection.

IoC Контейнеры

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

    Наверное, самым ярким примером использования IoC контейнеров является проект Spring. Это очень мощный проект, который затрагивает очень много аспектов конструирования ПО. Одним из таких аспектов является конструирование объектов на основании его связей. Причём связи между объектами могут храниться как в самом коде приложения, так и задаваться XML-файлом.

<beans>
 
  <bean id="reader"
    class="com.copier.consoleReader"/>
 
  <bean id="writer"
    class="com.copier.systemLogWriter"/>
 
  <bean id="copier"
    class="com.copier.copier">
    <property name="source">
      <ref bean="reader"/>
    </property>
    <property name="destination">
      <ref bean="writer"/>
    </property>
  </bean>
 
</beans>

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

BeanFactory factory = new XmlBeanFactory(new FileInputStream("dependency.xml"));
copier copierService = (copier)factory.getBean("copier");
copierService.run();

    Одним из самых известных IoC контейнеров является picoContainer. Существует порт этого проекта на PHP, поэтому вы можете опробывать готовые решения перед тем, как пытаться сделать что-то самому.

Dependency Injection: Active vs. Passive?

    Между Active и Passive подходами есть различия, которые могут обусловить то, где более удачным будет применение той или иной формы инверсии зависимостей.

    Ключевым отличием injection от lookup является то, как пользовательский объект связывается с объектом реализацией сервиса. При использовании injection объект получает необходимые сервисы неявно. Эти сервисы впрыскиваются в пользовательский объект контейнером-сборщиком или самим программистом. Если вы используете lookup-подход, то ваш модуль должен явно запросить необходимые сервисы у локатора. Это различие во многом определяет разность между этими решениями.

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

    Использование локатора ставит перед вами другую проблему. Локатор прячет за собой все зависимости, но появляется зависимость от самого локатора. Является ли эта зависимость для вас обременяющей? Как показывает практика, интерфейс локатора очень редко меняется, поэтому на это можно закрыть глаза. В действительности, никто не боится зависимостей от классов, которые являются частью языковой платформы (например, DOMDocument).

    Использование DI в чистом виде (без использования IoC-контейнеров) с одной стороны даёт вам более понятный и предсказуемый код. В таком коде явно описываются зависимости объектов в данном контексте. Но такой метод ведёт к излишней перегрузке интерфейса. Довольно часто количество необходимых сервисов превышает один-два объекта. Если вы будете передавать все зависимости объекта через его интерфейс вручную, то очень скоро вы станете испытывать сложности с сопровождением такого кода. Некоторые люди считают, что приложения, использующие IoC-контейнеры, гораздо проще тестируются. Это не так. На самом деле тут нет практически никакой разницы между DL и DI. Довольно легко написать локатор, который будет давать возможность легко заменять сервисы mock или stub объектами. Это возможно как на уровне отдельных объектов (замена отдельного объекта), так и на уровне контейнера (замена всего контейнера или слияние нескольких контейнеров).

    На самом деле, все эти методики не являются взаимоисключающими. Существуют проекты, которые удачно совмещают Dependency Injection и Dependency Lookup. Так что окончательное решение по поводу удачности этих решений в какой-либо вашей конкретной ситуации можете вынести только вы сами.

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

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


⇓ 

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

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

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

Давайте выбирать хостинг правильно Ну, или использовать хостинг бесплатный, но хороший

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

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