Open-closed principle

      Comments Off on Open-closed principle

Home Forums Programming Software engineering Object-oriented programming Open-closed principle

This topic contains 0 replies, has 1 voice, and was last updated by  Васильев Владимир Сергеевич 2 weeks, 3 days ago.

  • Author
    Posts
  • #4093

    This article provides a short and clear description for one of the main code design rules (SOLID) – an Open-Closed principle. It is demonstrating examples in C# programming language.

    Open-closed principle is the easiest and the most obvious. It states: any program units (classes, structures, modules) should be open for extension and closed for modification.

    If the class has already been written, approved, tested, possibly included to the library and then to the project, any attempt to modify it later is a bad idea. But other thing is the possibility to extend it through other available means. In fact, this principle simply involves the clever use of two principles of OOP: abstraction, and polymorphism.

    Of course, the need to change the existing requirements to the code is not something rare, this is a normal situation. Another question is how the class itself can be prepared for its functionality to be extended in time. Therefore, data types have to be designed according to the possible need for extension in future.

    Lets see an example of a class whose authors were too optimistic about its design, assuming that it is flexible enough to be ever modified.

    This example describes a class ShopManager, which implements the possibility of selling products with the discount.

    What catches the eye in this code – a limited list of goods, and it is unclear what to do if suddenly the store owners want to extend or change the range of those goods. From this perspective, the code is completely “wooden”, inflexible.

    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();
       } 
    }

    The second thing that is puzzling – accrual of discounts. In this case, it just depends on what kind of product we buy. Neither the amount of the product or of the sales period, or from any other reasons. But in reality the principles of accrual discounts may depend on many factors. It may be promotions and wholesale prices, and discount cards and plenty of other options.

    How to change the design of the ShopManager class to make it open for any extensions we might want to make in future?

    First, it makes sense to immediately get rid of the hard binding to a particular list of products. Second, add some flexibility about different types of discounts (holiday, savings, promotions, etc.). Third, even if we distinguish reasons for discounts, you need to understand that for different types of products they can vary.

    And here it becomes clear that it wont take just a couple of minutes to fix this code design. It requires significant improvements in three different directions: product types, types of discounts, different types of discounts for different goods.

    And here it is the time to think about one of the OOP principles – abstraction.

    We define the base interfaces for any type of products, type of discounts and rules for calculating the discount.

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

    Now we define specific classes that implement these algorithms:

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

    Now we change ShopManager class so that you could make sales with and without discounts:

    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            
          return  Math.Round(MakeDiscount(product, discount, rule) * amount, 2);
        }
       public double MakeDiscount(IProduct product, IDiscount discount, IDiscountRule rule)
       {
          //implementing the discount algorithm       
           return double rs = discount.MakeDiscount(product, rule);  
      }
    }

    And now let’s test the program again after making changes:

    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 results of selling with a discount and without it are obvious:

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

    As for ShopManager class , it’s ready for any changes in the logic of discounts, all types of products, and we wont need to change anything for that. We can pass an instance of any class that implements the interface IProduct to the method SellWithDiscount () , any product, any class that implements the interface IDiscount, as well as any type of discount or an instance of any class that implements the interface IDiscountRule, as well as the particular implementation of a certain type of discounts for a particular product.

    Now we have a class ShopManager which is OPEN FOR EXTENSION, BUT CLOSE FOR MODIFICATIONS, which is the essence of the open-closed principle of SOLID .

You must be logged in to reply to this topic.