воскресенье, 29 января 2012 г.

Принцип инверсии зависимости

Принцип инверсии зависимости (DIP) впервые был определен Робертом Мартином около 15ти лет назад и содержит два утверждения:
  1. Модули верхних уровней не должны зависеть от модулей нижних уровней. Модули обоих уровней должны зависеть от абстракций.
  2. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Что интересно, в отличие от других принципов SOLID Роберт Мартин не рассматривает DIP как вполне обособленный. В "The C++ Report" за май 96го, где впервые и прозвучало полное определение и описание принципа, автор указывает, что DIP - это следствие строгого следования Принципу открытости/закрытости (OCP) и Принципу подстановки Лисков (LSP). Те изменения в архитектуре приложения, которые происходят в результате применения OCP и LSP, Мартин называет Принципом инверсии зависимости.
Так как применение DIP лежит в плоскости архитектуры, Мартин определил показатели качества дизайна, которые устраняются с применением Принципа инверсии зависимости.
  1. Жесткость. Изменение одного модуля ведет к изменению других модулей.
  2. Хрупкость. Изменения в приводят к неконтролируемым ошибкам в других частях программы.
  3. Неподвижность. Модуль сложно отделить от остальной части приложения для повторного использования.
DIP не очень прост в понимании, как кажется. Суть принципа лучше сможет пояснить следующий пример.
Требуется составить программу для расчета суммарной скидки товара, который хранится на складе, по определенной карте скидок. Черновой вариант будет выглядеть так

  1. namespace DIP
  2. {
  3.   public class Product
  4.   {
  5.     public double Cost { get; set; }
  6.     public String Name { get; set; }
  7.     public uint Count { get; set; }
  8.   }
  9.  
  10.   public class Warehouse
  11.   {
  12.     public IEnumerable<Product> GetProducts()
  13.     {
  14.       return new List<Product>
  15.    {
  16.     new Product {Cost = 100, Name = "Tyres", Count = 1000},
  17.     new Product {Cost = 120, Name = "Disks", Count = 200},
  18.     new Product {Cost = 90, Name = "Alarms", Count = 500},
  19.     new Product {Cost = 150, Name = "Batteries", Count = 200},
  20.     new Product {Cost = 60, Name = "Tools", Count = 50}
  21.    };
  22.     }
  23.   }
  24.  
  25.   public class DiscountScheme
  26.   {
  27.     public double GetDiscount(Product p)
  28.     {
  29.       if (p.Name == "Tyres")
  30.         return 0.01;
  31.       else if (p.Name == "Disks")
  32.         return 0.05;
  33.       else if (p.Name == "Alarms")
  34.         return 0.1;
  35.       else if (p.Name == "Batteries")
  36.         return 0.15;
  37.       else if (p.Name == "Tools")
  38.         return 0.1;
  39.       else
  40.         return 0;
  41.     }
  42.   }
  43.  
  44.   public class ProductService
  45.   {
  46.     public double GetAllDiscount()
  47.     {
  48.       double sum = 0;
  49.  
  50.       Warehouse wh = new Warehouse();
  51.  
  52.       IEnumerable<Product> products = wh.GetProducts();
  53.  
  54.       DiscountScheme ds = new DiscountScheme();
  55.  
  56.       foreach(var p in products)
  57.         sum += p.Cost * p.Count * ds.GetDiscount(p);
  58.       
  59.       return sum;
  60.     }
  61.   }
  62.  
  63.   class Program
  64.   {
  65.     static void Main(string[] args)
  66.     {
  67.       ProductService ps = new ProductService();
  68.       Console.WriteLine("Discount for all products = " + ps.GetAllDiscount());
  69.  
  70.       Console.ReadKey();
  71.     }
  72.   }
  73. }
* This source code was highlighted with Source Code Highlighter.
Обратите внимание на класс ProductService. Структура класса такова
Пунктирными линиями указаны вызовы. ProductService зависит от реализации Warehouse и DiscountScheme.
Является ли такой дизайн гибким? По факту мы не можем без изменения ProductService рассчитать скидку на товары, которые могут быть не только на складе. Так же нет возможности подсчитать скидку по другой карте скидок (с другим Disctount Scheme ). Как верно подметил Александр Бындю, такой дизайн не позволяет в полной мере тестировать используемые классы.
Согласно OCP и LSP нужно выделить использование реализаций Warehouse и Discount Scheme  из ProductService при помощи абстракций.

  1. namespace DIP
  2. {
  3.   public interface IProductStorage
  4.   {
  5.     IEnumerable<Product> GetProducts();
  6.   }
  7.  
  8.   public interface IDiscountCalculator
  9.   {
  10.     double GetDiscount(Product products);
  11.   }
  12.  
  13.   public class Product
  14.   {
  15.     public double Cost { get; set; }
  16.     public String Name { get; set; }
  17.     public uint Count { get; set; }
  18.   }
  19.  
  20.   public class Warehouse : IProductStorage
  21.   {
  22.     public IEnumerable<Product> GetProducts()
  23.     {
  24.       return new List<Product>
  25.       {
  26.         new Product {Cost = 100, Name = "Tyres", Count = 1000},
  27.         new Product {Cost = 120, Name = "Disks", Count = 200},
  28.         new Product {Cost = 90, Name = "Alarms", Count = 500},
  29.         new Product {Cost = 150, Name = "Batteries", Count = 200},
  30.         new Product {Cost = 60, Name = "Tools", Count = 50}
  31.       };
  32.     }
  33.   }
  34.  
  35.   public class SimpleScheme : IDiscountCalculator
  36.   {
  37.     public double GetDiscount(Product p)
  38.     {
  39.       if (p.Name == "Tyres")
  40.         return 0.01;
  41.       else if (p.Name == "Disks")
  42.         return 0.05;
  43.       else if (p.Name == "Alarms")
  44.         return 0.1;
  45.       else if (p.Name == "Batteries")
  46.         return 0.15;
  47.       else if (p.Name == "Tools")
  48.         return 0.1;
  49.       else
  50.         return 0;
  51.     }
  52.   }
  53.  
  54.   public class ProductService
  55.   {
  56.     public double GetAllDiscount(IProductStorage storage,
  57.                    IDiscountCalculator discountCalculator)
  58.     {
  59.         double sum = 0;
  60.      
  61.         foreach(var p in storage.GetProducts())
  62.           sum += p.Cost * p.Count * discountCalculator.GetDiscount(p);
  63.       
  64.         return sum;
  65.     }
  66.   }
  67.  
  68.   class Program
  69.   {
  70.     static void Main(string[] args)
  71.     {
  72.       ProductService ps = new ProductService();
  73.       Console.WriteLine("Discount for all products = " +
  74.                 ps.GetAllDiscount(new Warehouse(), new SimpleScheme()));
  75.  
  76.       Console.ReadKey();
  77.     }
  78.   }
  79. }
* 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

13 комментариев:

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

    ОтветитьУдалить
    Ответы
    1. Ну вот, значит не зря пост написал :-)

      Удалить
    2. Да таких примеров полно.

      Удалить
    3. Конечно, не зря! Теперь стало понятно где происходит инверсия. Это хорошее дополнение к твоей презентации.

      Удалить
  2. Ах вот оно что...
    Спасибо, очень доходчиво объяснили.
    Только я бы схемы разместил ПЕРЕД кодом, на мой взгляд это удобнее: при изучении кода в голове приходится строить диаграмму классов, а если диаграмма дана сразу, то можно сосредоточиться уже на сути.

    ОтветитьУдалить
    Ответы
    1. Пожалуйста.
      Я думаю, что построить диаграмму классов из кода с пятью типами, это несложная задача.
      И тут кстати такой еще момент есть. Картинки лучше использовать для закрепления материала. Если использовать диаграммы как у меня до кода, то можно посеять непонимание.

      Удалить
  3. Вот было горе: ProductService зависит от реализации Warehouse и DiscountScheme.
    Вот стало горе: Program зависит от реализации Warehouse и SimpleScheme.
    Для чего выводить "наружу" всё "грязное бельё"? Оно всегда пряталось по давно понятным и незыблемым причинам. Если проблема в модульном тестировании, то эти "огороды" нужно делать, конечно, но использовать только в тестах. А в Program вызывать ps.GetAllDiscount(), в которой уже и создавать Warehouse и SimpleScheme для метода ps.GetAllDiscount(IProductStorage storage, IDiscountCalculator discountCalculator). ИМХО.

    ОтветитьУдалить
    Ответы
    1. А что если все это (кроме класса Program) кроется в готовой неприкасаемой сборке, а логику классов Warehouse и SimpleScheme нужно сменить? Писать подобное с нуля? Или разрабам этой сборки изначально следует позаботиться о супер-пупер кастомизации, на которую они убьют кучу времени, еще дольше будут тестировать и все равно не смогут охватить все возможные варианты? Если же Warehouse и SimpleScheme будут использовать абстракции, то пользователи сборки смогут сами реализовать IProductStorage и IDiscountCalculator и накрутить там то, что им действительно нужно, и в случае чего, исправить. Лучшей кастомизации не придумать. ИМХО.

      Удалить
    2. Поправочка: если ProductService будет использовать абстракции...

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

      Удалить
  4. После твоей статьи наконец-то разобрался, два дня на понимание ушло... Спасибо! Если совсем по простому, то мы тупо заменяем New ClassName() на связку Интерфейс + Реализация во всех местах нашего проекта, кроме начальной точки.

    ОтветитьУдалить
    Ответы
    1. Только не стоит перегибать с упрощением, а то прогресс остановится на куче кода без архитектуры

      Удалить