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

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 3 мес., 3 нед. назад.

  • Автор
    Сообщения
  • #4029

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

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

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

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

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

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

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

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

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

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

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

    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<IIngredient> Ingredients { get;set;}
       string Description { get;set;}
    }

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

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

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

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

    interface IBakery
    {
      IPastry Bake(IRecipe recipe);
    }

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

    class NiceBakeryOnTheCornerOFMyStreet
    {
       Dictionary<string, IRecipe> menu = new Dictionary<string, IRecipe>();
    
       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[name];
          }
             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. неподвижность (о повторном использовать кода в других системах можно забыть , поскольку модули сильно связаны между собой).

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

Для ответа в этой теме необходимо авторизоваться.