вторник, 9 апреля 2013 г.

Критический взгляд на принцип инверсии зависимостей

DISCLAIMER: У автора этой статьи нет цели подорвать авторитет или каким-то образом обидеть столь уважаемого камрада, как «дядюшка» Боб Мартин. Речь здесь идет скорее о более тщательном обдумывании принципа инверсии зависимостей и анализ примеров, использованных при его описании.

Принцип инверсии зависимостей (Dependency Inversion Principle, DIP) был впервые описан Бобом Мартином в одноименной статье, опубликованной в журнале C++ Report в 1996 году. Затем, практически в неизменном виде он был опубликован в книгах Боба Мартина «Принципы, паттерны и методики гибкой разработки» [Mattin2006].

По ходу статьи я буду приводить все необходимые цитаты и примеры из вышеупомянутых источников. Но чтобы не было «спойлеров» и ваше мнение оставалось объективным, я бы рекомендовал потратить 10-15 минут и ознакомиться с оригинальным описанием этого принципа в статье [Martin96] или книге [Martin96].

------------------------------------------------

Принцип инверсии зависимостей звучит так [Martin2006 p.190]:

А. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те и другие должны зависеть от абстракций.

В. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Давайте начнем с первого пункта.

Разбиение на слои

У лука есть слои, у торта есть слои, у людоедов есть слои и у программных систем – тоже есть слои! – Шрек (с)

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

В результате мы получаем примерно следующую диаграмму:

image

Рисунок 1 – «Наивная» схема разбиения на слои

С точки зрения Боба Мартина такая схема разбиения системы на слои является наивной. Недостатком такого дизайна является «коварная особенность: слой Policy зависит от изменений во всех слоях на пути к Utility. Эта зависимость транзитивна.» [Martin2006 p.192].

Хм… Весьма необычное утверждение. Если говорить о платформе .NET, то зависимость будет транзитивной только в том случае, если текущий модуль будет «выставлять» модули нижних уровней в своем открытом интерфейсе. Другими словами, если в Mechanism Layer есть открытый класс, принимающий в качестве аргумента экземпляр StringUtil (из Utility Layer), то все клиенты уровня Mechanism Layer становятся зависимыми на Utility Layer. В противном случае, транзитивность изменений отсутствует: все изменения нижнего уровня ограничены текущем уровнем и не распространяются выше.

Чтобы понять мысль Боба Мартина нужно вспомнить, что впервые принцип инверсии зависимостей был описан в далеком 1996-м году [Martin96], и в качестве примеров использовался язык С++. В исходной статье сам автор пишет о том, что проблема транзитивности есть лишь в языках без четкого разделения интерфейса класса от реализации. В С++ и правда проблема транзитивных зависимостей актуальна: если файл PolicyLayer.h включает посредством директивы «include» MechanismLayer.h, который, в свою очередь включает UtilityLayer.h, то при любом изменении в заголовочном файле UtilityLayer.h (даже в «закрытой» секции классов, объявленных в этом файле) нам придется перекомпилировать и развернуть заново всех клиентов. Однако в С++ эта проблема решается путем использования идиомы PIml, предложенной Гербом Саттером и сейчас тоже не столь актуальна.

Решение этой проблемы с точки зрения Боба Мартина заключается в следующем:

«Слой более высокого уровня объявляет абстрактный интерфейс служб, в которых он нуждается. Затем слои нижних уровней реализуются так, чтобы удовлетворять этим интерфейсам. Любой класс, расположенный на верхнем уровне, обращается к слою соседнего снизу уровня через абстрактный интерфейс. Таким образом, верхние слои не зависят от нижних. Наоборот, нижние слои зависят от абстрактного интерфейса служб, объявленного на более высоком уровне… Таким образом, обратив зависимости, мы создали структуру, одновременно более гибкую, прочную и подвижную.» [Martin2006 p.192]

image

Рисунок 2 – Инвертированные слои

В некотором роде такое разбиение разумно. Так, например, при использовании паттерна наблюдатель, именно наблюдаемый объект (observable) определяет интерфейс взаимодействия с внешним миром, поэтому никакие внешние изменения не могут на него повлиять.

Но с другой стороны, когда речь заходит именно о слоях, которые представляются обычно сборками (или пакетами в терминах UML), то предложенный подход вряд ли можно назвать жизнеспособным. По своему определению, вспомогательные классы нижнего уровня используются в десятке разных модулях более высокого уровня. Utility Layer будет использоваться не только в Mechanism Layer, но еще и в Data Access Layer, Transport Layer, Some Other Layer. Должен ли он в таком случае реализовывать интерфейсы, определенные во всех модулях более высокого уровня?

Очевидно, что такое решение сложно назвать идеальным, особенно учитывая то, что мы решаем проблему, не существующую на многих платформах, таких как .NET или Java.

Понятие абстракции

Многие термины настолько «въедаются» в наш мозг, что мы перестаем обращать на них внимание. Для большинства «объектно-ориентированных» программистов это означает, что мы перестаем задумываться над многими заезженными терминами, как «абстракция», «полиморфизм», «инкапсуляция». Чего над ними думать, ведь все и так понятно? ;)

Однако для того, чтобы точно понять смысл принципа инверсии зависимостей и второй части определения, нам нужно вернуться к одному из этих фундаментальных понятий. Давайте посмотрим на определение термина «абстракция» из книги Гради Буча [Booch2007]:

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

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

Давайте вернемся к определению: Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Какой пример возникает в голове теперь, после того, как мы вспомнили, что же такое абстракция? Когда абстракция начинает зависеть от деталей? Примером нарушения этого принципа может служить абстрактный класс GZipStream, который принимает MemoryStream, а не абстрактный класс Stream:

abstract class GZipStream
{
   
// Абстракция GZipStream принимает конкретный поток
    protected GZipStream(MemoryStream memoryStream) { }
}

Другим примером нарушения этого принципа может быть абстрактный класс репозитория из слоя доступа к данным, принимающий в конструкторе PostgreSqlConnection или строку подключения для SQL Server, что делает любую реализацию такой абстракции завязанной на конкретную реализацию. Но это ли имеет ввиду Боб Мартин? Если судить по примерам, приведенных в статье или в книге, то под понятием «абстракции» Боб Мартин понимает нечто совсем иное.

Принцип DIP по Мартину

Для объяснения своего определения Боб Мартин дает следующее пояснение.

Чуть упрощенная, но все еще весьма действенная интерпретация принципа DIP выражается простым эвристическим правилом: «Зависеть надо от абстракций». Оно гласит, что не должно быть зависимостей от конкретных классов; все связи в программе должны вести на абстрактный класс или интерфейс.

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

В качестве же иллюстрации нарушения принципа DIP вообще, и первого «проясняющего» пункта, в частности, приводится следующий пример:

public class Button
{ 
   
private Lamp
lamp;
   
public void
Poll()
    {
       
if (/* какое-то условие */)
            lamp.TurnOn();
    } }

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

У кода есть одна интересная особенность. За редким исключением, код сам по себе не может быть корректным или не корректным; баг это или фича зависит от того, что от него ожидается. Даже если нет формальной спецификации (что является нормой), код некорректен лишь в том случае, когда он делает не то, что от него требуется или предполагается. Именно этот принцип лежит в основе контрактного программирования [Meyer2000], в котором спецификация (намерения) выражаются непосредственно в коде в форме предусловий, постусловий и инвариантов.

Глядя на класс Button я не могу сказать ошибочен дизайн или нет. Я могу точно сказать, что имя класса не соответствует его реализации. Класс нужно переименовать в LampButton или убрать из класса Button поле Lamp.

Боб Мартин настаивает на том, что данный дизайн некорректен, поскольку «высокоуровневая стратегия приложения не отделена от низкоуровневой реализации. Абстракции не отделены от деталей. В отсутствие такого разделения стратегия верхнего уровня автоматически зависит от модулей нижнего уровня, а абстракция автоматически зависит от деталей» [Martin2006].

Во-первых, я не вижу в данном примере «стратегий верхнего уровня» и «модулей нижнего уровня»: с моей точки зрения, классы Button и Lamp находятся на одном уровне абстракции (во всяком случае, я не вижу аргументов, доказывающих обратное). Тот факт, что класс Button может кем-то управлять не делает его более высокоуровневым. Во-вторых, здесь нет «абстракции, зависящей от деталей», здесь есть «реализация абстракции, зависящая от деталей», что совсем не одно и тоже.

Решение по Мартину такое:

image

Рисунок 3 – «Инвертирование зависимостей»

Лучше ли данное решение? Давайте посмотрим…

Главным плюсом инвертирования зависимостей «по Мартину» является инвертирование владения. В исходном дизайне, при изменении класса Lamp пришлось бы изменяться классу Button. Теперь класс Button «владеет» интерфейсом ButtonServer, а он не может измениться из-за изменения «нижних уровней», таких как Lamp. Все как раз наоборот: изменение класса ButtonServer возможно только под воздействием изменений класса Button, что приведет к изменению всех наследников класса ButonServer!

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

Даже тот факт, что мы ограничиваемся наследованием интерфейсов, а не реализации делает этот дизайн переусложненным. Ведь на данном решении Боб Мартин не останавливается и предлагает, в случае необходимости, обобщить его путем замены ButtonServer на SwitchableDevice. Но это не решает проблему, а лишь усложняет все дело. Главная проблема такого дизайна в том, что мы связываем общим интерфейсом два семейства типов: кнопки и лампы не смогут развиваться независимо; изменения в одном семействе приведут к обязательным изменениям в другом.

Как по мне, в данном случае можно поступить проще. Зачем что-то выдумывать, когда в нашем арсенале уже давно есть инструмент для решения задачи инвертирования владения: паттерн наблюдатель. В нашем случае, интерфейс ButtonServer является классическим наблюдателем, поэтому я бы в .NET использовал события, а для другой платформы, переименовал бы ButtonServer в ButtonObserver. Интерфейс наблюдателя (грубо говоря, набор событий) всегда контролируется тем, кто эти события предоставляет, поэтому интерфейс ButtonObserver будет контролируется именно классом Button, а не наблюдателями, которые этот интерфейс реализуют.

В таком случае, никто не удивится, что при изменении класса Button, например, при появлении третьего состояния у кнопки, в интерфейсе ButtonObserver появится еще один метод! Но я бы при этом не делал класс Lamp наблюдателем класса Button; вместо этого я бы переложил связь кнопки и лампы на более высокий уровень:

image

Рисунок 4 – Использование «посредника»

По сути, это LampController – это посредник (медиатор), который реализует логику более высокого уровня: наблюдает за состоянием кнопки и управляет лампой.

В данном случае мы упрощаем развитие каждой части нашей системы, поскольку медиатор является барьером, который гасит изменения в одной части системы, не давая им распространиться на другую часть системы! Любые изменения класса Button приведут к изменению LampController, но не приведут к изменению класса Lamp и наоборот.

При таком подходе мы не вводим лишних связей, не используем наследования (LampController наследует ButtonObserver, но мы могли бы использовать события вместо этого). Классы Button и Lamp максимально автономны и могут изменяться относительно свободно, не влияя друг на друга.

Мы можем пойти дальше и выделить интерфейсы IButton и ILamp, и отвязать медиатор от конкретных ламп и кнопок. Теперь, как минимум, становится ясно, что класс LampController является стратегией более высокого уровня, которую мы хотим использовать повторно. (Хотя я предпочту такое усложнение делать лишь тогда, когда в этом будет необходимость!)

Интересно, что в своей оригинальной статье Боб Мартин предлагает аналогичное решение (только LampController у него называется ButtonAdapter), но советует он его лишь в том случае, если у нас нет доступа на изменение класса Lamp. С моей точки зрения, отсутствие изменений в классе Lamp для его использования в связке с некоторой кнопкой является отличным показателем слабосвязного дизайна и именно к нему я бы и рекомендовал стремиться.

Следуя принципу инверсии зависимостей

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

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

image

Рисунок 5 – «Горшочек – не вари» (с)

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

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

После чего дается такое уточнение:

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

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

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

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

Заключение

Поймите меня правильно: я руками и ногами за хороший дизайн. Я за дизайн, который является «гибким, надежным и повторноиспользуемым», но как этого добиться? Приведет ли к хорошему дизайну буквальное следование принципу инверсии зависимостей? Как нам понять, что он означает, если в его определении такие термины как абстракция трактуются по-своему, а проблемы транзитивности зависимостей взяты из С++?

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

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

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

30 комментариев:

  1. Блестяще!!

    Действительно почти всегда банан это просто банан.

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

    ОтветитьУдалить
  2. Каким образом из "Each of
    the lower level layers are
    represented by an abstract
    class. The actual layers are
    then derived from these
    abstract classes. Each of
    the higher level classes
    uses the next lowest layer
    through the abstract interface. Thus, none of the layers depends upon any of
    the other layers. Instead,
    the layers depend upon abstract classes" вы получили совершенно противоположное "Слой более высокого уровня объявляет абстрактный интерфейс служб, в которых он нуждается. Затем слои нижних уровней реализуются так, чтобы удовлетворять эти интерфейсам..."? Сравните свой пример и пример из оригинальной статьи.

    ОтветитьУдалить
  3. @makajda: да, в целом, вы правы. Борьба с изменениями - это один из аспектов борьбы со сложностью.

    @Bulat: Моя цитата взята из книги, а не из статьи.

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

    В своей книге Мартин дает более развернутое представление о том, что он имеет ввиду: что этот абстрактный слой контролируется именно верхним уровнем, а не нижним. Т.е. "драйвят" изменения клиенты абстракции, а не классы, ее реализующие.

    (Я знаю, что это может сбивать с толку; если судить лишь по приведенной Вами цитате, то Боб Мартин на 100% прав; мы таким подходом пользуемся сплошь и рядом. Но почитайте дальнейшие примеры Мартина, чтобы понять, что он имеет ввиду.)

    ОтветитьУдалить
  4. Эх ООПешники, придумали сами себе проблем, а теперь решаете.

    ОтветитьУдалить
  5. Поправьте, пожалуйста:

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

    "Очевидно, что такое решение сложно назвать идеальным, особенно учитывая то, что мы решаем проблему, не существующие во многих платформах, таких как .NET или Java." - "проблемы".

    "Теперь давайте еще раз вспомним о том, что такое абстракция и ответим на вопрос: есть ли здесь «абстракций», которая зависит от деталей?" - "абстракция".

    ОтветитьУдалить
  6. @Анатолий: респект!
    Поправил очепятки!

    ОтветитьУдалить
  7. Сергей, добрый день.

    Рассматриваемую книгу Мартина я читал совсем недавно, но в более поздней версии: "Принципы, паттерны и методики
    гибкой разработки на языке C#", 2010 г.

    Зацепил Ваш взгляд на пример с лампой и кнопкой. Не понятен вывод о том, что в предложенном Мартином варианте ButtonServer зависит от Button. Как раз наоборот - экземпляр Button всего лишь ссылается на экземпляр ButtonServer. Если, как Вы говорите, у кнопки появится третье состояние, ей самой придется решить, как в этом случае работать с ButtonServer. Но это не является причиной для изменения интерфейса.
    Сам Мартин, продолжая свой пример, говорит о том, что в данном случае название ButtonServer не слишком уместно, и логичнее было бы назвать интерфейс SwitchableDevice, окончательно разрывая смысловую связь с кнопкой. В итоге с таким интерфейсом смогут работать любые клиенты, не только Button. Но зависимость остается инвертированной: реализация лампы зависит от интерфейса, который заявлен ее клиентами.

    И еще пару слов по поводу взаимодействия слоев приложения. Не раз замечал, что добавление нового метода, например, в слой доступа к данным происходит именно потому, что клиентскому приложению понадобилась новая фича. То есть по факту интерфейс DAL определяется именно потребностями клиентского приложения, а не наоборот. И это, как я понимаю, и есть мартиновский принцип DI.
    А Utility Layer в вашей трактовке (используемый в различных частях приложения) - это скорее не слой, а библиотека-тулкит. Не думаю, что в таком случае стоит вести речь об инверсии зависимостей в чистом виде, тут больше подошел бы адаптер.

    ОтветитьУдалить
  8. @Дмитрий: мы говорим с вами об одной и той же книге, просто я указал дату публикации оригинала, а вы пишите о дате публикации русского изадния.

    Да, Боб Мартин затем пишет об обобщении решения и переходу на SwitchableDevice, но дальше у него идет сверх странная фраза:
    В таком случае у этого интерфейса нет владельца. Мы получаем любопытную ситуацию, когда интерфейс может использоваться разными клиентами и реализовываться разными серверами. Стало быть, интерфейс должен существовать сам по себе, не примыкая ни к какой группе.
    Это звучит сверх подозрительно, честно. Интерфейс - это и есть абстракция; и теперь у нас появляется два семейства классов ("управлятели" и "управляемые"), которые на него завязаны. Согласно его же (Мартина) метрикам стабильности, если у интерфейса есть 10 клиентов - то дизайн менее стабилен, чем когда у нас есть 2 интерфейса с 5 клиентами у каждого.

    Интерфейсы - это же не сферические кони в вакууме. Они определяют некоторые семейства объектов, которые могут развиваться и изменяться. А это значит, что это лишь вопрос времени, когда API этого интерфейса начнет не устраивать одну из сторон, что приведет к каскадным изменениям *обоих иерархий*

    По поводу слоев: я с вами совершенно согласен, что слой нижнего уровня предоставляет услуги верхнему уровню и изменяется лишь по требованию верхнего уровня. Но ведь Utility Layer (судя по названию) - это и есть библиотеки, слой утилит; поэтому для меня совсем не понятен аргумент того, что нам нужно инвертировать от него зависимости. С другой стороны, в разделе о "слоях" речь в исходной статье шла исключительно о С++, поэтому я вообще не уверен, что мы должны учитывать "слои" при обсуждении языков, в которых эта транзитивная зависимость слоев отсутствует.

    Еще одно замечание по поводу слоев: да, интерфейс DAL-а определяется слоем верхнего уровня, но он им не контролируется. Просто потому, что вы хотите "реюзать" этот DAL в трех приложениях. Кто из них будет "контролировать" его интерфейс в этом случае?

    ОтветитьУдалить
  9. - При таком подходе мы не вводим лишних связей, не используем наследования

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

    - Если следовать этому принципу буквально, то мы можем создать класс, реализация которого должна зависеть от абстракции, реализация которой должна зависеть от абстракции, реализация которой … ну, вы поняли.

    Там рядом есть ещё буква S - Single Responsibility Principle, который говорит о том, что класс делает полезную работу в зоне своей небольшой ответственности, остальное отдает другим. С помощью DI все эти кирпичи собираются вместе и любой из них можно заменить без проблем в любой момент.

    И ещё - нет ничего страшного в изменении контракта (если вы конечно не пишете либу которую пользуют тысячи людей), страшно то, насколько трудоемко будут вноситься изменения и насколько тяжело будет тестировать отдельные компоненты потом.

    ОтветитьУдалить
  10. @Mich:
    > реализация интерфейса к наследованию не имеет отношения...

    Предлагаю полистать Мейера. Существует множество типов наследования. Наследование классов моделирует IS-A отношение между типами, реализация интерфейса - это другой вид *наследования*, моделирующий Can-Do отношение между типами.

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

    >пример с Utils - высосан из пальца...

    WCF используется в сотне приложений;) Повторное использование модулей нижнеих уровней подразумевает использование этих слоев в нескольких контекстах.
    Пример Мартина высосан из пальца, поскольку транзитивной зависимости в .NET просто не существует;)

    >пример с кнопкой у которой появилась третья кнопка ...

    Я писал о третьем состоянии и он кажется вполне реальным;) Мой поинт в том, что у нас есть лишняя связь между мирами "управлятелей" и "управляемых", которая лишь усложняет жизнь, ничего не привнося в замен.

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

    И да, я полностью "за" независимость классов и возможность их использования в разных контекстах. Именно поэтому реализация кнопкой интерфейса ButtonServer-а кажется абсурдной! Зачем такой малютке вообще завязываться на кого-то? У нее есть методы TurnOn и TurnOff, пусть это будет проблемой вызывающей стороны определить время, когда и какой из этих методов вызвать.

    И еще: в исходной статье Боб Мартин предлагает вариант, аналогичный моему ButtonController-у, только назвал его он ButtonAdapter (почему-то этот пример выпилен из книги);) Только он рекомендует использовать этот подход лишь в случае, когда класс лампы нельзя изменить.

    > Там рядом есть еще буква S - ...

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

    > И еще - нет ничего страшного в изменении контракта ...

    Да, я с вами полностью согласен! Но дело в том, что вы, в таком случае, не согласны с камрадом Мартином, который предлагает абстрагироваться от *любого бизнес класса*, поскольку его интерфейс является нестабильным!

    Для меня, например, это совсем не повод выделять интерфейс, только потому что этот класс написал я или мой коллега. Вот если этот класс завязан на внешние ресурсы, то я обязательно от них буду "абстрагироваться", но не буду параноить и добавлять интерфейс для каждого своего класса. А Вы?

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

    ОтветитьУдалить
  11. Сергей спасибо за этот цикл публикаций по управлению зависимостям. Но за теорией я кое-что не улавливаю, как этим пользоваться на практике. Допустим из слоя бизнес-логики я хочу обратиться к слою-репозитория для работы с персистентными данными.
    Сервис-локатор плох для этого и я использую контейнер DI:
    var repository = conteiner.Resolve();
    Но согласно теории контейнер желательно использовать на самом верхнем уровне приложения и не простаскивать до уровня бизнес-логики. Как правильно вызвать объект репозитория в подобном случае?

    ОтветитьУдалить
  12. @Alex: я рекомендую плистать книгу Марка Симана "Dependency Injection in .NET". Там много примеров использования контейнера и должно быть понятнее, как сдружить паттерны инверсии зависимостей (используемые на нижнем уровне) с композицией приложения (на верхнем уровне).

    ОтветитьУдалить
  13. @Sergey:

    > Это звучит сверх подозрительно, честно. Интерфейс - это и есть абстракция; и теперь у нас появляется два семейства классов ("управлятели" и "управляемые"), которые на него завязаны.
    > А это значит, что это лишь вопрос времени, когда API этого интерфейса начнет не устраивать одну из сторон, что приведет к каскадным изменениям *обоих иерархий*

    Сергей, возможно, я не понял Вашу мысль, но - в чем тут проблема? Любой интерфейс некоторыми классами реализуется, а некоторыми - используется. И, безусловно, любой интерфейс подвержен изменениям, что влечет и изменения в классах.
    И, если не трудно, можете предложить Ваш вариант реализации системы, в которой есть набор объектов-переключателей (Кнопка, Рубильник и т.п.), которым не важно, что именно переключать, и набор объектов в состоянием Вкл/Выкл (Лампа, Мотор), которым не важно, кто именно изменяет их состояние.

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

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

    ОтветитьУдалить
  14. @Дмитрий:

    > можете предложить яваш вариант реализации системы, в которой есть набор объектов-переклчателей...

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

    > Честно скажу: ни разу еще не получилось использовать DAL с предыдущего проекта.

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

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

    ОтветитьУдалить
  15. Сергей, на Ваш взгляд, есть ли смысл тестировать медиатор/посредник/контроллер, изображенный на рисунке 4? Понятно, что в реальном приложении взаимодействие между классами может быть несколько сложнее, чем в демонстрационном примере.
    И если смысл есть, то не вернёмся ли мы обратно к ситуации "Горшочек не вари"?
    Спасибо.

    ОтветитьУдалить
  16. @TDenis: можно на ты;)

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

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

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

    ОтветитьУдалить
  17. Сергей,

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

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

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

    З.Ы. За блог тебе спасибо, он шикарный, не первый год читаю.

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

    ОтветитьУдалить
  19. Зачем нужно особо оговаривать пункт B в определении инверсии зависимостей? Это же по сути есть определение абстракции.

    ОтветитьУдалить
  20. @beavis: а разве можно дать определение абстракции, используя этот же термин в его же определении? бесконечную рекурсию-с получим.

    Ну и посмотрите, как этот пункт расшифровывается и сравните его с общепринятым понятием абстракции;)

    ОтветитьУдалить
  21. Может быть вся неразбериха с паттерном инверсии зависимостей из-за не совсем удачного примера? Возьмем к примеру некий фреймворк Х который будет рулить нашим приложением. Он будет на самом высоком инфраструктурном уровне. И этот фреймворк предоставляет интерфейсы чтобы мы встраивались в него в разных местах. Не это ли хороший пример данного паттерна в действии?

    ОтветитьУдалить
    Ответы
    1. Примеры - это главная боль любого автора:))

      Так что я, вообще, не против DIP-а, это полезная штука, просто ее смысл очень мало кто понимает. Очень часто используют именно DI, а не DIP, а между ними - очень большая разница!

      (Я как раз недавно писал про разницу меджу этими вещами тут: http://sergeyteplyakov.blogspot.com/2014/11/di-vs-dip-vs-ioc.html)

      Удалить
  22. Что-то с этим принципом DIP окончательно запутался.
    Как тогда звучит правильная формулировка? Чего именно хотят достичь?
    Вот возьмем к примеру вашу книгу "Паттерны проектирования на платформе .NET".

    Кусок определения по Мартину: "Модули верхнего уровня не должны зависить от моделей нижнего уровня. И те и другие должны зависеть от абстракций". Это TRUTH или нет? :) Если истинно, то есть несколько замечаний по примерам из вашей книги:
    1) Рис. 21.5 на стр. 291. Пример вроде как понятен. Но есть несколько вопросов.
    - Appliction в явном виде зависит от Reporter. Почему здесь не делали инверсию зависимостей?
    - Что, если Application выделит на своем уровне интерфейс IReporter? Чем хорошо, а чем плохо? Единственное, в этом случае нужен будет еще один слой выше Верхнего уровня... И кто-то должен создать и передать завсимость сверху.
    - Почему интерфейс IReportFormatter на нижнем уровне, а не на верхнем? Это связано с тем, что он был выделен при работе над классом Reporter?

    2) MVC (рис. 21.3 на стр. 289). Почему View непосредственно обращается к Model (линия Query)? Т.е. почему запросы к модели делаются напрямую, перескакивая через уровень? Не логичней ли было запрос делать через Controller? И не было бы лишней зависимости от нижнего уровня. А так View будет знать о Model.

    3) MVP (рис. 21.4 на стр. 290). Почему интерфейс IView на уровне приложения, а не выше, на уровне представления? Ведь инверсии зависимостей в этом случае вроде как нет? В чем приемущества одного и второго варианта (когд IView на уровне представления или на уровне приложения)?

    ОтветитьУдалить
    Ответы
    1. Алексей, давайте форкнем обсуждение.

      Ветка 1 - Терминология и цель.

      > Как тогда звучит правильная формулировка?

      Автор этого принципа - Боб Мартин, поэтому звучать принцип по другому не может. Вот определение: "Модули верхнего уровня не должны зависить от моделей нижнего уровня. И те и другие должны зависеть от абстракций"

      Так что это TRUTH. Вопрос теперь в том, что автор хотел этим сказать и как на его понимать и применять на практике.

      Главная проблема - это понятие "абстракции" в этом определении. Исходя из общепринятых примеров, описывающих этот принцип, почему-то считается, что абстракция не возможна без выделения интерфейсов. Возможно в каком-то мире это и так, но с точки зрения ООП - это не так.

      Абстракция - это высокоуровневое, видимое поведение объекта, которое обеспечивается за счет скрытых деталей и невидимых подробностей (за счет инкапсуляции). StringBuilder - это абстракция, String - это абстракция, List - это абстракция.

      Когда же выделяется интерфейс, то говорят, что появляется дополнительный абстрактный слой (абстракция более высокого уровня?), которая скрывает не только детали реализации в конкретном классе, но и прячет конкретный класс как таковой.

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

      Теперь по поводу целей (это и правда важно): это слабосвязанный дизайн и контроль за распространением изменнеий.

      Удалить
    2. Ветка 2.

      > 1) Рис. 21.5 на стр. 291. Пример вроде как понятен. Но есть несколько вопросов.
      > - Appliction в явном виде зависит от Reporter. Почему здесь не делали инверсию зависимостей?

      В моей модели есть две составляющие - стабильный класс составления отчетов и "переменчивый" кусок, который отвечает за форматирования отчета.
      Выделение IReporter-а в данном случае приведет к усложнению системы, поскольку в текущей модели мы уже получаем нужную гибкость. Если требования изменятся и нужно будет заменять весь модуль отчетности, то тогда и будет выделен нужный интерфейс.

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

      Да, и здесь инверсия зависимостей не нужна, поскольку зависимости сверху вниз - это нормально.

      > - Что, если Application выделит на своем уровне интерфейс IReporter? Чем хорошо, а чем плохо? Единственное, в этом случае нужен будет еще один слой выше Верхнего уровня... И кто-то должен создать и передать завсимость сверху.

      У нас же выйдет матрешка. Application - это и есть самый высокий уровень. К тому же, если он выделит IReporter, то класс Reporter с более низкого уровня должен будет его реализовать. А вот это уже зависимость очень плохая!

      > - Почему интерфейс IReportFormatter на нижнем уровне, а не на верхнем? Это связано с тем, что он был выделен при работе над классом Reporter?

      Да, это и есть инверсия зависимостей: класс Reporter контролирует свои собственные зависимости и переменные части. Именно поэтому именно ему видней, каким должен быть интерфейс IReporterFormatter, и именно он его контролирует.

      Удалить
    3. Ветка 3.

      > 2) MVC (рис. 21.3 на стр. 289). Почему View непосредственно обращается к Model (линия Query)?

      Обычно это очень нормально, поскольку модель - это нечто фундаментальное в этой связке.

      > Т.е. почему запросы к модели делаются напрямую, перескакивая через уровень? Не логичней ли было запрос делать через Controller?

      Контроллер - это вообще неведомый зверь в этой связке и он скорее является медиатором, который активно массажирует вью и модель. Но если бы он был посередине, то попытка все общение от View провести через него потребовало бы дублирования почти всех интерфейсов модели, что не выгодно. Плюс, нужно задавать себе главный вопрос: а в чем проблема, если один компонент/слой знает о другом?

      > И не было бы лишней зависимости от нижнего уровня. А так View будет знать о Model.

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

      Удалить
    4. > 3) MVP (рис. 21.4 на стр. 290). Почему интерфейс IView на уровне приложения, а не выше, на уровне представления?

      > Ведь инверсии зависимостей в этом случае вроде как нет? В чем приемущества одного и второго варианта (когд IView на уровне представления или на уровне приложения)?

      (Только IView тут на уровне презентера, а не приложения, это большая разница).

      Но ведь это и есть тот самый DIP: класс сам контролирует интерфейс своей зависимости. Если поместить IView в вид, то тогда средний уровень начнет зависить от верхнего, что не хорошо. Плюс, верхний уровень будет зависеть от всяких потрохов, которые презентер не будет понимать. Плюс, как добиться нескольких реализаций представления, если они объявлены в контректом модуле?

      Удалить
    5. Большое спасибо за ответ)
      Стало более понятно и теперь вроде уже раскладывается все по правильным полочкам. Еще надо немного осмыслить и может появятся новые вопросы :)

      Удалить