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

НК РФ 26.09.2022
НК РФ

Последнее обновление: 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();

14 ответов

В основном говорится:

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

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

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

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

Книги Agile Software Development, Принципы, Шаблоны и Практики и Agile Принципы, Шаблоны и Практики в С# являются лучшими ресурсами для полного понимания первоначальных целей и мотиваций, лежащих в основе Принципа инверсии зависимости. Статья "Принцип обращения зависимостей" также является хорошим ресурсом, но из-за того, что она является сжатой версией черновика, который в конечном итоге попал в ранее упомянутые книги, она оставляет некоторые важные дискуссии о концепции владение пакетами и интерфейсами, которые являются ключевыми для отличия этого принципа от более общего совета "программировать для интерфейса, а не реализации", который можно найти в книге "Шаблоны проектирования" (Gamma, et al.).

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

Это достигается путем разработки компонентов, чьи внешние зависимости выражаются в виде интерфейса, для которого потребитель компонента должен предоставить реализацию. Другими словами, определенные интерфейсы выражают то, что нужно компоненту, а не то, как вы используете компонент (например, "INeedSomething", а не "IDoSomething").

То, на что не ссылается Принцип обращения зависимостей, - это простая практика абстрагирования зависимостей с помощью интерфейсов (например, MyService → ). Хотя это отделяет компонент от конкретной детали реализации зависимости, оно не инвертирует отношения между потребителем и зависимостью (например, ⇐ Logger.

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

В рамках этой общей цели повторного использования мы можем выделить два подтипа повторного использования:

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

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

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

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

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

Хотя следование принципу инверсии зависимости во втором случае может принести некоторую пользу, следует отметить, что его значение применительно к современным языкам, таким как Java и С#, значительно снижено, возможно, до такой степени, что оно не имеет значения. Как обсуждалось ранее, DIP включает в себя полное разделение деталей реализации на отдельные пакеты. В случае развивающегося приложения, однако, простое использование интерфейсов, определенных в терминах бизнес-области, защитит от необходимости модифицировать компоненты более высокого уровня из-за меняющихся потребностей компонентов детализации реализации, даже если детали реализации в конечном счете будут находиться в одном и том же пакете. Эта часть принципа отражает аспекты, которые имели отношение к языку в момент его кодификации (например, C++), которые не имеют отношения к более новым языкам. Тем не менее, важность Принципа инверсии зависимости прежде всего связана с разработкой повторно используемых программных компонентов/библиотек.

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

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

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

Принцип инверсии зависимостей гласит, что:

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

Этот принцип направлен на "инвертирование" обычного представления о том, что модули высокого уровня в программном обеспечении должны зависеть от модулей нижнего уровня. Здесь модули высокого уровня владеют абстракцией (например, решая методы интерфейса), которые реализуются модулями более низкого уровня. Таким образом, модули нижнего уровня зависят от модулей более высокого уровня.

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

Традиционная многоуровневая архитектура

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

Вы должны понимать слой, пакет или библиотеку. Давайте посмотрим, как будет код.

У нас была бы библиотека или пакет для слоя доступа к данным.

// DataAccessLayer.dll public class ProductDAO { }

// BusinessLogicLayer.dll using DataAccessLayer; public class ProductBO { private ProductDAO productDAO; }

Многоуровневая архитектура с инверсией зависимостей

Инверсия зависимости указывает на следующее:

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

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

Что такое модули высокого уровня и низкого уровня? Мышление модулей, таких как библиотеки или пакеты, высокоуровневыми модулями будут те, которые традиционно имеют зависимости и низкоуровневые, от которых они зависят.

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

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

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

У нас была бы библиотека или пакет для уровня доступа к данным, который определяет абстракцию.

// DataAccessLayer.dll public interface IProductDAO public class ProductDAO: IProductDAO{ }

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

// BusinessLogicLayer.dll using DataAccessLayer; public class ProductBO { private IProductDAO productDAO; }

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

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

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

// Domain.dll public interface IProductRepository; using DataAccessLayer; public class ProductBO { private IProductRepository productRepository; }

После того, как уровень постоянства зависит от домена, теперь можно инвертировать, если определена зависимость.

// Persistence.dll public class ProductDAO: IProductRepository{ }

Углубление принципа

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

Но почему мы инвертируем зависимость? Какова основная цель за пределами конкретных примеров?

Это обычно позволяет наиболее стабильным вещам, которые не зависят от менее стабильных вещей, меняться чаще.

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

Но есть не только этот пример хранилища. Существует много сценариев, в которых применяется этот принцип, и существуют архитектуры, основанные на этом принципе.

архитектуры

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

Чистая Архитектура

Для меня принцип инверсии зависимостей, описанный в официальной статье

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

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

Таким образом, создание отдельной абстракции на том же уровне компонента A, которое зависит от компонента B (которое не зависит от A), может быть выполнено только в том случае, если компонент A действительно будет полезен для повторного использования в разных приложениях или контексты. Если это не так, то применение DIP будет плохой дизайн.

Более ясный способ сформулировать принцип инверсии зависимостей:

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

I.e., вместо того, чтобы реализовать ваш класс Logic , как обычно делают люди:

Class Dependency { ... } class Logic { private Dependency dep; int doSomething() { // Business logic using dep here } }

вы должны сделать что-то вроде:

Class Dependency { ... } interface Data { ... } class DataFromDependency implements Data { private Dependency dep; ... } class Logic { int doSomething(Data data) { // compute something with data } }

Data и DataFromDependency должны жить в том же модуле, что и Logic , а не с Dependency .

Зачем это?

Хорошие ответы и хорошие примеры уже даны другими здесь.

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

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

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

Представьте себе этот псевдокод...

Public class MyClass { public Service myService = ServiceLocator.service; }

MyClass напрямую зависит как от класса Service, так и от класса ServiceLocator. Это необходимо для обоих, если вы хотите использовать его в другом приложении. Теперь представьте это...

Public class MyClass { public IService myService; }

Теперь MyClass использует один интерфейс, интерфейс IService. Мы бы позволили контейнеру IoC фактически установить значение этой переменной.

Пусть будет гостиница, которая попросит у производителя продуктов питания его запасы. Отель дает название еды (скажем, курицу) Генератору еды, и Генератор возвращает запрашиваемую еду в гостиницу. Но отель не заботится о типе пищи, которую он получает и подает. Таким образом, Генератор поставляет продукты с этикеткой "Еда" в отель.

Эта реализация в JAVA

FactoryClass с фабричным методом. Пищевой Генератор

Public class FoodGenerator { Food food; public Food getFood(String name){ if(name.equals("fish")){ food = new Fish(); }else if(name.equals("chicken")){ food = new Chicken(); }else food = null; return food; } }

Класс Аннотация/Интерфейс

Public abstract class Food { //None of the child class will override this method to ensure quality... public void quality(){ String fresh = "This is a fresh " + getName(); String tasty = "This is a tasty " + getName(); System.out.println(fresh); System.out.println(tasty); } public abstract String getName(); }

Курица реализует Еду (Конкретный Класс)

Public class Chicken extends Food { /*All the food types are required to be fresh and tasty so * They won"t be overriding the super class method "property()"*/ public String getName(){ return "Chicken"; } }

Рыба реализует Пищу (Конкретный Класс)

Public class Fish extends Food { /*All the food types are required to be fresh and tasty so * They won"t be overriding the super class method "property()"*/ public String getName(){ return "Fish"; } }

В заключение

Отель

Public class Hotel { public static void main(String args){ //Using a Factory class.... FoodGenerator foodGenerator = new FoodGenerator(); //A factory method to instantiate the foods... Food food = foodGenerator.getFood("chicken"); food.quality(); } }

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

Также вы могли бы заметить, что класс Fish and Chicken реализует класс Food и не связан напрямую с отелем. т.е. курица и рыба также зависит от класса продуктов питания.

Это означает, что компонент высокого уровня (гостиница) и компонент низкого уровня (рыба и курица) зависят от абстракции (еда).

Это называется инверсией зависимости.

Принцип инверсии зависимости (DIP) гласит, что

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

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

Public interface ICustomer { string GetCustomerNameById(int id); } public class Customer: ICustomer { //ctor public Customer(){} public string GetCustomerNameById(int id) { return "Dummy Customer Name"; } } public class CustomerFactory { public static ICustomer GetCustomerData() { return new Customer(); } } public class CustomerBLL { ICustomer _customer; public CustomerBLL() { _customer = CustomerFactory.GetCustomerData(); } public string GetCustomerNameById(int id) { return _customer.GetCustomerNameById(id); } } public class Program { static void Main() { CustomerBLL customerBLL = new CustomerBLL(); int customerId = 25; string customerName = customerBLL.GetCustomerNameById(customerId); Console.WriteLine(customerName); Console.ReadKey(); } }

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

поделиться

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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

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

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

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

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

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

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

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

Какой пример возникает в голове теперь, после того, как мы вспомнили, что же такое абстракция ? Когда абстракция начинает зависеть от деталей? Примером нарушения этого принципа может служить абстрактный класс 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(); } }

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

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

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

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

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

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



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

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

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

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

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

Определение

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

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

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

Инверсия зависимостей — одна из важнейших идиом программирования. В русскоязычном интернете описаний этой идиомы (принципа) на удивление мало. Поэтому я решил попробовать сделать описание. Примеры буду делать на 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. См. .

Рекомендуем почитать

Наверх