Контакты

Инверсия зависимостей. Каков принцип инверсии зависимостей и почему он важен? Разбиение на слои

2 ответов

Хороший вопрос - слово inversion несколько удивительно (так как после применения DIP , модуль зависимостей нижнего уровня, очевидно, t теперь depend на модуле вызывающего абонента более высокого уровня: либо вызывающий, либо зависимый теперь более слабо связаны через дополнительную абстракцию).

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

Один момент, который следует отметить при чтении бумаги дяди Боба на DIP, - это то, что С++ не (и в момент написания, но не имеет) имеют интерфейсы, поэтому достижение этой абстракции в С++ обычно реализуется посредством абстрактного/чистого виртуального базового класса, тогда как в Java или С# абстракция для ослабления связи обычно заключается в развязывании путем абстрагирования интерфейса от зависимости и связывания модуля более высокого уровня (s) к интерфейсу.

Edit Просто уточнить:

"В некотором месте я также вижу, что он называется инверсией зависимостей"

Инверсия: Инвертирование управления зависимостями из приложения в контейнер (например, Spring).

Инъекция зависимостей:

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

Как насчет инверсии управления (IoC)?

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

Инверсия управления в качестве ориентира для проектирования служит для следующих целей:

  • Существует развязка выполнения определенной задачи из реализация.
  • Каждый модуль может сосредоточиться на том, для чего он предназначен.
  • Модули не делают никаких предположений о том, что делают другие системы, но полагаются на их контракты.
  • Замена модулей не влияет на другие модули.

Для получения дополнительной информации смотрите.

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

Данное описание разработано совместно с Владимиром Матвеевым в ходе подготовки к занятиям со студентами, изучающими Java.

Другие статьи из этого цикла:

Начну с определения «зависимости». Что такое зависимость? Если ваш код использует внутри себя какой-то класс или явно обращается к статическому методу какого-то класса или функции — это зависимость. Поясню примерами:

Ниже класс A внутри метода с именем someMethod() явно создает объект класса B и обращается к его методу someMethodOfB()

Public class A { void someMethod() { B b = new B(); b.someMethodOfB(); } }

Аналогично, например класс B обращается явно к статическим полям и методам класса System:

Public class B { void someMethodOfB() { System.out.println("Hello world"); } }

Во всех случаях когда любой класс (типа А) самостоятельно создает любой класс (типа B) или явно обращается к статическим полям или членам классов, это называют прямой зависимостью. Т.е. важно: если класс внутри себя работает внутри себя с другим классом — это зависимость. Если он еще и создает внутри себя этот класс, то это прямая зависимость.

Чем плохи прямые зависимости? Прямые зависимости плохи тем, что класс, самостоятельно создающий внутри себя другой класс, «намертво» привязывается к данному классу. Т.е. если явно написано, что B = new B(); , то тогда класс А всегда будет работать именно с классом B и никаким иным классом. Или если написано System.out.println("..."); тогда класс всегда будет выводить в System.out и никуда больше.

Для небольших классов зависимости не являются страшными. Такой код вполне может работать. Но в ряде случаев, чтобы ваш класс A смог универсально работать в окружении разных классов, ему возможно могут потребоваться другие реализации классов — зависимостей. Т.е. нужен будет например не класс B , а другой класс с тем же интерфейсом, или не System.out , а например, вывод в логгер (например log4j).

Прямую зависимость можно графически отобразить таким образом:

Т.е. когда вы в своем коде создаете класс А: A a = new A(); на самом деле создается не один класс А, а целая иерархия зависимых классов, пример которой на приведенной картинке. Данная иерархия «жесткая»: без изменения исходного кода отдельных классов нельзя подменить ни один из классов иерархии. Поэтому класс А в такой реализации плохо адаптируем для изменяющегося окружения. Скорее всего, его нельзя будет использовать ни в каком коде, кроме конкретно того, для которого вы его написали.

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

Public class A { private final B b; public A(B b) { this.b = b; } public void someMethod() { b.someMethodOfB(); } }

Т.о. класс А теперь получает свою зависимость через конструктор. Теперь чтобы создать класс А необходимо будет создать сначала его зависимый класс. В данном случае это B:

B b = new B(); A a = new A(b); a.someMethod();

Если ту же самую процедуру повторить для всех классов, т.е. в конструктор класса B передавать инстанс класса D , в конструктор класса D — его зависимости E и F , и т.д., то тогда получится код, все зависимости которого создаются в обратном порядке:

G g = new G(); H h = new H(); F f = new (g,h); E e = new E(); D d = new D(e,f); B b = new B(d); A a = new A(b); a.someMethod();

Графически это можно отобразить так:

Если сравнить 2 картинки — картинку выше с прямыми зависимостями и вторую картинку с внедрением зависимостей — то видно, что направление стрелочек поменялось на обратное. По этой причине идиома и называется «инверсией» зависимостей. Иными словами инверсия зависимостей заключается в том, что, класс не создает зависимости самостоятельно, а получает их в созданном виде в конструкторе (или иным образом) .

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

Недостаток внедрения зависимостей виден тоже с первого взгляда — объекты классов, спроектированных с использованием этого паттерна, трудоемко конструировать. Поэтому обычно внедрение (инверсию) зависимостей применяют совместно с какой-либо библиотекой, предназначенной для облегчения этой задачи. Например одна из библиотек Google Guice. См. .

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

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

Определение

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

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

Как видите, магическое число семь, плюс минус два встречается повсюду, так почему же ваш код должен отличаться.

На самом деле все принципы SOLID между собой сильно связаны и основная их цель — помощь в создании качественного, способного к масштабированию, программного обеспечения. Но последний принцип SOLID на их фоне действительно выделяется. Для начала посмотрим на формулировку данного принципа. Итак, принцип инверсии зависимостей (Dependency Inversion Principle — DIP): «Зависимость на абстракциях. Нет зависимости на что-то конкретное.» . Небезызвестный специалист в области разработки ПО, Роберт Мартин , также особенно выделяет принцип DIP и представляет его просто как результат следованию другим принципам SOLID — принципу открытости/закрытости и принципу подстановки Лисков. Напомним, что первый говорит о том, что класс не должен модифицироваться для внесения новых изменений, а второй касается наследования и предполагает безопасное использование производных типов некоторого базового типа без нарушения правильности работы программы. Роберт Мартин изначально сформулировал этот принцип следующим образом:

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

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

То есть разрабатывать классы нужно, оперируя абстракциями, а не конкретными их реализациями. И если следовать принципам OCP и LSP , то именно этого мы и добьемся. Поэтому вернемся немного назад, к уроку, посвященному . Там в качестве примера мы рассматривали класс Bard , который в самом начале был жестко привязан к классу Guitar , представляющему конкретный музыкальный инструмент:

public class Bard { private Guitar guitar; public Bard(Guitar guitar) { this.guitar = guitar; } public void play() { guitar.play(); } }

public class Bard {

private Guitar guitar ;

public Bard (Guitar guitar )

this . guitar = guitar ;

public void play ()

guitar . play () ;

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

Файл Instrument.java :

public interface Instrument { void play(); }

public interface Instrument {

void play () ;

Файл Guitar.java :

class Guitar implements Instrument{ @Override public void play() { System.out.println("Play Guitar!"); } }

class Guitar implements Instrument {

@Override

public void play ()

System . out . println ("Play Guitar!" ) ;

Файл Lute.java :

public class Lute implements Instrument{ @Override public void play() { System.out.println("Play Lute!"); } }

public class Lute implements Instrument {

@Override

public void play ()

System . out . println ("Play Lute!" ) ;

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

public class Bard { private Instrument instrument; public Bard() { } public void play() { instrument.play(); } public void setInstrument(Instrument instrument) { this.instrument = instrument; } }

public class Bard {

private Instrument instrument ;

Последнее обновление: 11.03.2016

Принцип инверсии зависимостей (Dependency Inversion Principle) служит для создания слабосвязанных сущностей, которые легко тестировать, модифицировать и обновлять. Этот принцип можно сформулировать следующим образом:

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

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

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

Class Book { public string Text { get; set; } public ConsolePrinter Printer { get; set; } public void Print() { Printer.Print(Text); } } class ConsolePrinter { public void Print(string text) { Console.WriteLine(text); } }

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

Теперь попробуем привести наши классы в соответствие с принципом инверсии зависимостей, отделив абстракции от низкоуровневой реализации:

Interface IPrinter { void Print(string text); } class Book { public string Text { get; set; } public IPrinter Printer { get; set; } public Book(IPrinter printer) { this.Printer = printer; } public void Print() { Printer.Print(Text); } } class ConsolePrinter: IPrinter { public void Print(string text) { Console.WriteLine("Печать на консоли"); } } class HtmlPrinter: IPrinter { public void Print(string text) { Console.WriteLine("Печать в html"); } }

Теперь абстракция печати книги отделена от конкретных реализаций. В итоге и класс Book и класс ConsolePrinter зависят от абстракции IPrinter. Кроме того, теперь мы также можем создать дополнительные низкоуровневые реализации абстракции IPrinter и динамически применять их в программе:

Book book = new Book(new ConsolePrinter()); book.Print(); book.Printer = new HtmlPrinter(); book.Print();

Понравилась статья? Поделитесь ей