Принцип инверсии зависимости (DIP) впервые был определен Робертом Мартином около 15ти лет назад и содержит два утверждения:
Так как применение DIP лежит в плоскости архитектуры, Мартин определил показатели качества дизайна, которые устраняются с применением Принципа инверсии зависимости.
Требуется составить программу для расчета суммарной скидки товара, который хранится на складе, по определенной карте скидок. Черновой вариант будет выглядеть так
Пунктирными линиями указаны вызовы. ProductService зависит от реализации Warehouse и DiscountScheme.
Является ли такой дизайн гибким? По факту мы не можем без изменения ProductService рассчитать скидку на товары, которые могут быть не только на складе. Так же нет возможности подсчитать скидку по другой карте скидок (с другим Disctount Scheme ). Как верно подметил Александр Бындю, такой дизайн не позволяет в полной мере тестировать используемые классы.
Согласно OCP и LSP нужно выделить использование реализаций Warehouse и Discount Scheme из ProductService при помощи абстракций.
Сплошные стрелки означают наследование.
Обратите внимание на стрелки от Warehouse и SimpleScheme - они поменяли направление. Теперь от Warehouse и SimpleScheme (DiscounterScheme) ничего не зависит. Наоборот - они зависят от абстракций. Поэтому принцип так и назван - инверсии зависимости.
Ссылки:
Принцип инверсии зависимости - блог Александра Бындю
Хороший дизайн должен быть SOLID: TOP-5 архитектурных принципов
The Dependency Inversion Principle, Robert C. Martin, C++ Report, May 1996
- Модули верхних уровней не должны зависеть от модулей нижних уровней. Модули обоих уровней должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Так как применение DIP лежит в плоскости архитектуры, Мартин определил показатели качества дизайна, которые устраняются с применением Принципа инверсии зависимости.
- Жесткость. Изменение одного модуля ведет к изменению других модулей.
- Хрупкость. Изменения в приводят к неконтролируемым ошибкам в других частях программы.
- Неподвижность. Модуль сложно отделить от остальной части приложения для повторного использования.
Требуется составить программу для расчета суммарной скидки товара, который хранится на складе, по определенной карте скидок. Черновой вариант будет выглядеть так
Обратите внимание на класс ProductService. Структура класса такова
- namespace DIP
- {
- public class Product
- {
- public double Cost { get; set; }
- public String Name { get; set; }
- public uint Count { get; set; }
- }
- public class Warehouse
- {
- public IEnumerable<Product> GetProducts()
- {
- return new List<Product>
- {
- new Product {Cost = 100, Name = "Tyres", Count = 1000},
- new Product {Cost = 120, Name = "Disks", Count = 200},
- new Product {Cost = 90, Name = "Alarms", Count = 500},
- new Product {Cost = 150, Name = "Batteries", Count = 200},
- new Product {Cost = 60, Name = "Tools", Count = 50}
- };
- }
- }
- public class DiscountScheme
- {
- public double GetDiscount(Product p)
- {
- if (p.Name == "Tyres")
- return 0.01;
- else if (p.Name == "Disks")
- return 0.05;
- else if (p.Name == "Alarms")
- return 0.1;
- else if (p.Name == "Batteries")
- return 0.15;
- else if (p.Name == "Tools")
- return 0.1;
- else
- return 0;
- }
- }
- public class ProductService
- {
- public double GetAllDiscount()
- {
- double sum = 0;
- Warehouse wh = new Warehouse();
- IEnumerable<Product> products = wh.GetProducts();
- DiscountScheme ds = new DiscountScheme();
- foreach(var p in products)
- sum += p.Cost * p.Count * ds.GetDiscount(p);
- return sum;
- }
- }
- class Program
- {
- static void Main(string[] args)
- {
- ProductService ps = new ProductService();
- Console.WriteLine("Discount for all products = " + ps.GetAllDiscount());
- Console.ReadKey();
- }
- }
- }
* This source code was highlighted with Source Code Highlighter.
Пунктирными линиями указаны вызовы. ProductService зависит от реализации Warehouse и DiscountScheme.
Является ли такой дизайн гибким? По факту мы не можем без изменения ProductService рассчитать скидку на товары, которые могут быть не только на складе. Так же нет возможности подсчитать скидку по другой карте скидок (с другим Disctount Scheme ). Как верно подметил Александр Бындю, такой дизайн не позволяет в полной мере тестировать используемые классы.
Согласно OCP и LSP нужно выделить использование реализаций Warehouse и Discount Scheme из ProductService при помощи абстракций.
Если представить этот код в виде схемы
- namespace DIP
- {
- public interface IProductStorage
- {
- IEnumerable<Product> GetProducts();
- }
- public interface IDiscountCalculator
- {
- double GetDiscount(Product products);
- }
- public class Product
- {
- public double Cost { get; set; }
- public String Name { get; set; }
- public uint Count { get; set; }
- }
- public class Warehouse : IProductStorage
- {
- public IEnumerable<Product> GetProducts()
- {
- return new List<Product>
- {
- new Product {Cost = 100, Name = "Tyres", Count = 1000},
- new Product {Cost = 120, Name = "Disks", Count = 200},
- new Product {Cost = 90, Name = "Alarms", Count = 500},
- new Product {Cost = 150, Name = "Batteries", Count = 200},
- new Product {Cost = 60, Name = "Tools", Count = 50}
- };
- }
- }
- public class SimpleScheme : IDiscountCalculator
- {
- public double GetDiscount(Product p)
- {
- if (p.Name == "Tyres")
- return 0.01;
- else if (p.Name == "Disks")
- return 0.05;
- else if (p.Name == "Alarms")
- return 0.1;
- else if (p.Name == "Batteries")
- return 0.15;
- else if (p.Name == "Tools")
- return 0.1;
- else
- return 0;
- }
- }
- public class ProductService
- {
- public double GetAllDiscount(IProductStorage storage,
- IDiscountCalculator discountCalculator)
- {
- double sum = 0;
- foreach(var p in storage.GetProducts())
- sum += p.Cost * p.Count * discountCalculator.GetDiscount(p);
- return sum;
- }
- }
- class Program
- {
- static void Main(string[] args)
- {
- ProductService ps = new ProductService();
- Console.WriteLine("Discount for all products = " +
- ps.GetAllDiscount(new Warehouse(), new SimpleScheme()));
- Console.ReadKey();
- }
- }
- }
* This source code was highlighted with Source Code Highlighter.
Сплошные стрелки означают наследование.
Обратите внимание на стрелки от Warehouse и SimpleScheme - они поменяли направление. Теперь от Warehouse и SimpleScheme (DiscounterScheme) ничего не зависит. Наоборот - они зависят от абстракций. Поэтому принцип так и назван - инверсии зависимости.
Ссылки:
Принцип инверсии зависимости - блог Александра Бындю
Хороший дизайн должен быть SOLID: TOP-5 архитектурных принципов
The Dependency Inversion Principle, Robert C. Martin, C++ Report, May 1996
Интересно. Никогда не задумывался, что один из принципов SOLID может быть следствием применения двух других.
ОтветитьУдалитьНу вот, значит не зря пост написал :-)
УдалитьДа таких примеров полно.
УдалитьКонечно, не зря! Теперь стало понятно где происходит инверсия. Это хорошее дополнение к твоей презентации.
УдалитьАх вот оно что...
ОтветитьУдалитьСпасибо, очень доходчиво объяснили.
Только я бы схемы разместил ПЕРЕД кодом, на мой взгляд это удобнее: при изучении кода в голове приходится строить диаграмму классов, а если диаграмма дана сразу, то можно сосредоточиться уже на сути.
Пожалуйста.
УдалитьЯ думаю, что построить диаграмму классов из кода с пятью типами, это несложная задача.
И тут кстати такой еще момент есть. Картинки лучше использовать для закрепления материала. Если использовать диаграммы как у меня до кода, то можно посеять непонимание.
Вот было горе: ProductService зависит от реализации Warehouse и DiscountScheme.
ОтветитьУдалитьВот стало горе: Program зависит от реализации Warehouse и SimpleScheme.
Для чего выводить "наружу" всё "грязное бельё"? Оно всегда пряталось по давно понятным и незыблемым причинам. Если проблема в модульном тестировании, то эти "огороды" нужно делать, конечно, но использовать только в тестах. А в Program вызывать ps.GetAllDiscount(), в которой уже и создавать Warehouse и SimpleScheme для метода ps.GetAllDiscount(IProductStorage storage, IDiscountCalculator discountCalculator). ИМХО.
А что если все это (кроме класса Program) кроется в готовой неприкасаемой сборке, а логику классов Warehouse и SimpleScheme нужно сменить? Писать подобное с нуля? Или разрабам этой сборки изначально следует позаботиться о супер-пупер кастомизации, на которую они убьют кучу времени, еще дольше будут тестировать и все равно не смогут охватить все возможные варианты? Если же Warehouse и SimpleScheme будут использовать абстракции, то пользователи сборки смогут сами реализовать IProductStorage и IDiscountCalculator и накрутить там то, что им действительно нужно, и в случае чего, исправить. Лучшей кастомизации не придумать. ИМХО.
УдалитьПоправочка: если ProductService будет использовать абстракции...
УдалитьОльга, в этом подходе на поверхности лежит простая, но действенная идея: использование посредника. Посредника легко заменить, заменить же вросшие спагетти кодом модули намного сложнее. Опять же, вынося зависимости как можно выше в иерархии, получаем возможность управлять циклом их их жизни в одной точке, как кукловод марионетками, легким движением пальцев
УдалитьПосле твоей статьи наконец-то разобрался, два дня на понимание ушло... Спасибо! Если совсем по простому, то мы тупо заменяем New ClassName() на связку Интерфейс + Реализация во всех местах нашего проекта, кроме начальной точки.
ОтветитьУдалитьПожалуйста!
УдалитьТолько не стоит перегибать с упрощением, а то прогресс остановится на куче кода без архитектуры
Удалить