Что такое принцип инверсии зависимостей и почему это важно?

StackOverflow https://stackoverflow.com/questions/62539

Вопрос

Что такое принцип инверсии зависимостей и почему это важно?

Это было полезно?

Решение

Ознакомьтесь с этим документом: Принцип Инверсии зависимостей.

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

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

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

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

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

Другие советы

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

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

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

К чему Принцип инверсии зависимостей не относится , так это к простой практике абстрагирования зависимостей с помощью интерфейсов (напримерMyService → [ILogger ⇐ Регистратор]).Хотя это отделяет компонент от конкретной детали реализации зависимости, это не инвертирует взаимосвязь между потребителем и зависимостью (например[MyService → IMyServiceLogger] ⇐ Регистратор.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

http://xurxodev.com/content/images/2016/02/Traditional-Layered.png

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

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

// 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;
}

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

http://xurxodev.com/content/images/2016/02/Traditional-Layered.png

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

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

// Domain.dll
public interface IProductRepository;

using DataAccessLayer;
public class ProductBO { 
    private IProductRepository productRepository;
}

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

// Persistence.dll
public class ProductDAO : IProductRepository{

}

http://xurxodev.com/content/images/2016/02/Dependency-Inversion-Layers.png

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

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

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

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

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

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

Архитектуры

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

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

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

Шестиугольная Архитектура

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

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

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

Гораздо более ясным способом изложения Принципа инверсии зависимостей является:

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

Т.е. вместо того, чтобы реализовывать ваш класс 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.

Зачем это делать?

  1. Два модуля бизнес-логики теперь разделены.Когда Dependency изменения, вам не нужно меняться Logic.
  2. Понимание чего Logic это гораздо более простая задача:он работает только с тем, что выглядит как ADT.
  3. Logic теперь его легче протестировать.Теперь вы можете создавать экземпляры напрямую Data с поддельными данными и передайте их.Нет необходимости в макетах или сложных тестовых подмостках.

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

Причина ПОГРУЖЕНИЕ это важно потому, что обеспечивает принцип OO "слабо связанной конструкции".

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

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

Инверсия управления (IoC) - это шаблон проектирования, в котором объект получает свою зависимость от внешнего фреймворка, вместо того, чтобы запрашивать фреймворк о своей зависимости.

Пример псевдокода с использованием традиционного поиска:

class Service {
    Database database;
    init() {
        database = FrameworkSingleton.getService("database");
    }
}

Аналогичный код с использованием IoC:

class Service {
    Database database;
    init(database) {
        this.database = database;
    }
}

Преимущества МоК заключаются в следующем:

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

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

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

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

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

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

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

public class MyClass
{
  public IService myService;
}

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

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

Еще лучше то, что вам не нужно перетаскивать зависимости MyService, и зависимости этих зависимостей, и...что ж, вы уловили идею.

Инверсия управляющих контейнеров и шаблон внедрения зависимостей Мартина Фаулера тоже хорошо читать.Я нашел Шаблоны проектирования в первую очередь потрясающая книга для моего первого знакомства с DI и другими паттернами.

Инверсия зависимостей:Зависеть от абстракций, а не от конкретизации.

Инверсия управления:Main vs Абстракция, и как Main является связующим звеном систем.

DIP and IoC

Вот несколько хороших постов, рассказывающих об этом:

https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/

https://coderstower.com/2019/04/02/main-and-abstraction-the-decoupled-peers/

https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/

Принцип инверсии зависимостей (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();
        }
    }

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top