Принцип открытости-закрытости

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

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

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

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

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

    В этом примере описан класс ShopManager, который реализует возможность продажи продуктов, с учётом скидки.

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

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

    using System;
    using System.Data;
    using System.Data.SqlClient;
    using System.Configuration;
    
    class ShopManager
    {
       public double Sell(string product, double price, double amount)
       {
           return Math.Round(MakeDiscount(product) * price * amount, 2);
       }
       public double MakeDiscount(string product)
       {
           switch (product)
          {
              case "apples": return 0.95;
              case "potatos": return 0.87;           
              case "strawberry": return 0.99;           
              default: return 0.0;
         }
      }
    }
    
    class Program
    {
       static void Main()
       {
           ShopManager shop = new ShopManager();
           Console.WriteLine("The total i s {0} UAH", shop.Sell("apples", 11.5, 7.0));
           Console.ReadKey();
       }
    }

    Как изменить структуру класса ShopManager, чтобы он был готов к вариациям по поводу алгоритмов начислений скидок?

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

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

    И вот здесь самое время вспомнить про один из принципов ООП – абстракцию.

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

    //interface for all kinds of products
    interface IProduct
    {
       string Name { get; set; }
       double Price { get; set; }
    }
    
    //interface for all types of discounts
    interface IDiscount
    {
       double MakeDiscount(IProduct product, IDiscountRule rule);
    }
    
    //interface for all discount rules
    interface IDiscountRule
    {
       double DiscountAmount { get; set; }
       double SetRule(double coef);
    }

    Теперь создадим конкретные классы, реализующие эти алгоритмы:

    //particular product class
    class Apples : IProduct
    {
       public string Name { get; set; }
       public double Price { get; set; }
    }
    
    //particular type of discount
    class WholesaleDiscount : IDiscount
    {
       public double MakeDiscount(IProduct product, IDiscountRule rule)
       {
             //...
              return product.Price * rule.DiscountAmount;
       }
    }
    
    //particular discount rule class
     class ApplesDiscountRule : IDiscountRule
    {
       public double DiscountAmount { get; set; }
       public double SetRule(double coef)
       {
          //defining the algorithm of wholesale discountt for particular product
          //...
          DiscountAmount = coef;
          return coef;
       }
    }

    Теперь изменим класс ShopManager так, чтобы можно было осуществлять продажи с учётом скидок и без:

    class ShopManager
    {
       public double Sell(IProduct product, double amount)
       {
          //selling the product without discount
          return Math.Round(product.Price * amount, 2);
       }
       public double SellWithDiscount(IProduct product, IDiscount discount, IDiscountRule rule, double amount)
       {
          //selling the product with discount    
           double rs = Math.Round(MakeDiscount(product, discount, rule)* amount, 2);  
      }
       public double MakeDiscount(IProduct product, IDiscount discount, IDiscountRule rule)
       {
          //implementing the discount algorithm
           return discount.MakeDiscount(product, rule);
       }
    }

    И теперь протестируем заново работу программы после внесения изменений:

    class Program
    {
       static void Main()
       {
          var shop = new ShopManager();
          var apples = new Apples() { Name = "Apples", Price = 11.5 };
          Console.WriteLine("The total without discount is {0} UAH", shop.Sell(apples, 7.0));
    
          var rule = new ApplesDiscountRule();
          rule.SetRule(0.95);
    
          Console.WriteLine("The total with discount is {0} UAH",
          shop.SellWithDiscount(apples,new WholesaleDiscount(), rule, 7.0));
         Console.ReadKey();
       }
    }

    Результаты продажи со скидкой и без скидкой очевидны:

    The total without discount is 80,5 UAH
    The total with discount is 76,47 UAH

    Что же касается класса ShopManager, он готов к любым изменениям в логике скидок, любым типам продуктов, и ничего при этом менять в нём не нужно будет. В метод SellWithDiscount() мы можем передать экземпляр любого класса, реализующего интерфейс IProduct, то есть любой продукт, любой класс, реализующий интерфейс IDiscount, то есть любой тип скидки, и любой класс, реализующий интерфейс IDiscountRule, то есть конкретную реализацию определённого типа скидок для конкретного продукта.

    Теперь нас класс ShopManager ОТКРЫТ ДЛЯ РАСШИРЕНИЯ, НО ЗАКРЫТ ДЛЯ МОДИФИКАЦИЙ, что и есть суть принципа SOLID об открытости-закрытости классов.

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