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

На самом деле все принципы 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 ;

2 ответов

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

поделиться

Формулировка принципа инверсии зависимости состоит из двух правил, соблюдение которых чрезвычайно позитивно отражаются на структуре кода:

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

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

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

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

Скажем, мы решили заказать торт ко дню рождения. Для этого мы отправились в замечательную пекарню на углу улицы. Узнали, могут ли они для нас испечь торт под громким названием “Роскошь”, и получив позитивный ответ, заказали. Всё просто.

А теперь определимся с тем, какие абстракции нужно предусмотреть в этом коде. Для этого просто зададим себе несколько вопросов:

  • Почему именно торт? Можно было заказать и пирог или кексы
  • Почему именно ко дню рождения? А если бы это была свадьба или выпускной?
  • Почему именно “Роскошь”, а вдруг мне больше нравится “Муравейник” или “Пражский”?
  • Почему именно в эту пекарню, а не в кондитерскую мастерскую в центре города или куда-нибудь ещё?

И вот каждое из этих “а если” и “а вдруг” и есть точки, в которых требуется расширяемость кода, а соответственно – слабая связанность и определение типов через абстракции.

Теперь займёмся конструированием самих типов.

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

Interface IPastry { string Name { get; set; } }

А вот и конкретная реализация, под наш случай – торт ко дню рождения. Как видим, традиционно торт ко дню рождения предполагает свечки)))

Class BirthdayCake: IPastry { public int NumberOfCandles { get; set; } public string Name { get; set; } public override string ToString() { return String.Format("{0} with {1} nice candles", Name, NumberOfCandles) ; } }

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

Следующий вопрос – это чем отличаются друг от друга кондитерские изделия (кроме названия, конечно). Конечно же, рецептом!

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

Interface IIngredient { string IngredientName { get;set;} double Quantity { get; set; } string Units { get; set; } }

А вот и сами ингредиенты для нашего торта: мука, масло, сахар и сливки:

Class Flour:IIngredient { public string IngredientName { get; set; } public double Quantity { get; set; } public string Units { get; set; } public string Quality { get; set; } } class Butter: IIngredient { public string IngredientName { get; set; } public double Quantity { get; set; } public string Units { get; set; } } class Sugar: IIngredient { public string IngredientName { get; set; } public double Quantity { get; set; } public string Units { get; set; } public string Kind { get; set; } } class Creme: IIngredient { public string IngredientName { get; set; } public double Quantity { get; set; } public string Units { get; set; } public double Fat { get; set; } }

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

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

Interface IRecipe { Type PastryType { get; set; } string Name { get;set;} IList Ingredients { get;set;} string Description { get;set;} }

А конкретно класс, представляющий рецепт торта ко дню рождения, выглядит так:

Class BirthdayCakeRecipe: IRecipe { public Type PastryType { get; set; } public string Name { get;set;} public IList Ingredients { get; set; } public string Description { get; set; } public BirthdayCakeReipe() { Ingredients = new List(); } }

Теперь перейдём к нашей замечательной пекарне на углу улицы.

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

Interface IBakery { IPastry Bake(IRecipe recipe); }

А вот и класс, представляющий нашу пекарню:

Class NiceBakeryOnTheCornerOFMyStreet { Dictionary menu = new Dictionary(); public void AddToMenu(IRecipe recipe) { if (!menu.ContainsKey(recipe.Name)) { menu.Add(recipe.Name, recipe); } else { Console.WriteLine("It is already on the menu"); } } public IRecipe FindInMenu(string name) { if (menu.ContainsKey(name)) { return menu; } Console.WriteLine("Sorry...currently we don"t have " + name); return null; } public IPastry Bake(IRecipe recipe) { if (recipe != null) { IPastry pastry = Activator.CreateInstance(recipe.PastryType) as IPastry; if (pastry != null) { pastry.Name = recipe.Name; return pastry as IPastry; } } return null; } }

Осталось только протестировать работу кода:

Class Program { static void Main() { //creating an inctance of the bakery class var bakery = new NiceBakeryOnTheCornerOFMyStreet(); //preparing ingredients for the recipe var flour = new Flour() { IngredientName = "Flour", Quantity = 1.5, Units = "kg" }; var butter = new Butter() { IngredientName = "Butter", Quantity = 0.5, Units = "kg" }; var sugar = new Sugar() { IngredientName = "Sugar", Quantity = 0.7, Units = "kg" }; var creme = new Creme() { IngredientName= "Creme", Quantity = 1.0, Units = "liters" }; //and this is the recipe itself var weddingCakeRecipe = new BirthdayCakeRecipe() { PastryType = typeof(BirthdayCake), Name = "Birthday Cake Luxury", Description = "description on how to make a beautiful birthday cake" }; weddingCakeRecipe.Ingredients.Add(flour); weddingCakeRecipe.Ingredients.Add(butter); weddingCakeRecipe.Ingredients.Add(sugar); weddingCakeRecipe.Ingredients.Add(creme); //adding our cake recipe to the bakery"s menu bakery.AddToMenu(weddingCakeRecipe); //now let"s order it!! BirthdayCake cake = bakery.Bake(bakery.FindInMenu("Birthday Cake Luxury")) as BirthdayCake; //adding some candles ;) cake.NumberOfCandles = 10; //and here we are!!! Console.WriteLine(cake); } }

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

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

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

А теперь подумаем над последствиями нарушения принципа инверсии зависимости:

  1. жесткость (в систему было бы очень тяжело внести какие-то изменения, потому что каждое изменение затрагивало много различных ее частей).
  2. хрупкость (при внесении каких-либо изменения в одну часть системы, уязвимыми становятся другие её части, и порой это не слишком очевидно на первый взгляд).
  3. неподвижность (о повторном использовать кода в других системах можно забыть, поскольку модули сильно связаны между собой).

Ну а теперь делайте выводы сами, насколько принцип инверсии зависимости полезен в коде. Думаю, ответ очевиден.