SOLID Made Easy – Open-Closed Principle Posted on October 20, 2015 by Benjamin Medina III After learning about the Single Responsibility Principle, we have the Open-Closed Principle which is the second principle in SOLID. A class should be open for extension but closed for modification. The open-closed principle suggests that when a new functionality is to be implemented we should create another class or object rather than modifying existing code and tests to comply with the new requirements. In a restaurant POS, the computation of the total invoice for a transaction can be different because of discounts. Let’s say that the restaurant has two ways to compute the invoice of a transaction i.e without any discount and with a senior citizen’s discount. The owner now said that he will now distribute promo codes for his customers. A new computation has to be created for this feature. //// Incorrect way //// The invoice model that contains the properties of the invoice public class Invoice { public int Id { get; set; } public int TransactionId {get; set;} public InvoiceType Type { get; set; } public double FinalAmount {get; set;} } //// The different types of discount types supported public enum InvoiceType { None, SeniorCitizen, //// we added a new one for the promo discount Promo } //// The class that is responsible for computing for the invoice public class InvoiceBuilder { public InvoiceBuilder(InvoiceType type) { this.type = type; } public Invoice Compute() { Invoice result = new Invoice(); if (this.type == InvoiceType.SeniorCitizen) { //// compute for the invoice with a discount because the customer is a/accompanied by senior citizen/s } else if (this.type == InvoiceType.Promo) { //// compute for the invoice with a discount because the customer used a promo code } else { //// compute for the invoice normally } return result; } } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 //// Incorrect way //// The invoice model that contains the properties of the invoicepublic class Invoice { public int Id { get; set; } public int TransactionId {get; set;} public InvoiceType Type { get; set; } public double FinalAmount {get; set;}} //// The different types of discount types supportedpublic enum InvoiceType{ None, SeniorCitizen, //// we added a new one for the promo discount Promo} //// The class that is responsible for computing for the invoicepublic class InvoiceBuilder{ public InvoiceBuilder(InvoiceType type) { this.type = type; } public Invoice Compute() { Invoice result = new Invoice(); if (this.type == InvoiceType.SeniorCitizen) { //// compute for the invoice with a discount because the customer is a/accompanied by senior citizen/s } else if (this.type == InvoiceType.Promo) { //// compute for the invoice with a discount because the customer used a promo code } else { //// compute for the invoice normally } return result; }} In the sample above, we added a new discount type and modified the invoice builder class in order to have a section in the code where the computation of the invoice with a promo code is done. This would work but whenever we need to change something we need to refer to the same class. Also, whenever we need to add a new type of discount we will have to modify the same class/method thus adding more lines and complexity to the class. //// Correct way //// The interface that computes for the invoice of a transaction public interface IInvoiceStrategy { Invoice Compute(); } //// The class where the normal invoice is processed public class NormalInvoiceStrategy: IInvoiceStrategy { private int transactionID; public NormalInvoiceStrategyint transactionID) { this.transactionID = transactionID; } public Invoice Compute() { Invoice result = new Invoice (); ///// compute for the invoice of the transaction that does not have any discount. return result; } } //// The class for an invoice with a senior citizen's discount public class SeniorCitizenInvoiceStrategy: IInvoiceStrategy { private int transactionID; public SeniorCitizenInvoiceStrategy(int transactionID) { this.transactionID = transactionID; } public Invoice Compute() { Invoice result = new Invoice(); ///// compute for the invoice of the transaction that has a discount due to a senior citizen return result; } } //// The model for the promo that contains all its properties public class Promo { public int Id { get; set; } public int Discount {get; set;} public DateTime ExpirationDate {get; set;} } //// The class for an invoice with a promo discount public class PromoInvoiceStrategy: IInvoiceStrategy { private int transactionID; public PromoInvoiceStrategy(int transactionID, Promo promo) { this.transactionID = transactionID; } public Invoice Compute() { Invoice result = new Invoice(); ///// compute for the invoice of the transaction that has a discount due to a promo return result; } } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869 //// Correct way //// The interface that computes for the invoice of a transactionpublic interface IInvoiceStrategy{ Invoice Compute();} //// The class where the normal invoice is processedpublic class NormalInvoiceStrategy: IInvoiceStrategy{ private int transactionID; public NormalInvoiceStrategyint transactionID) { this.transactionID = transactionID; } public Invoice Compute() { Invoice result = new Invoice (); ///// compute for the invoice of the transaction that does not have any discount. return result; }} //// The class for an invoice with a senior citizen's discountpublic class SeniorCitizenInvoiceStrategy: IInvoiceStrategy{ private int transactionID; public SeniorCitizenInvoiceStrategy(int transactionID) { this.transactionID = transactionID; } public Invoice Compute() { Invoice result = new Invoice(); ///// compute for the invoice of the transaction that has a discount due to a senior citizen return result; }} //// The model for the promo that contains all its propertiespublic class Promo{ public int Id { get; set; } public int Discount {get; set;} public DateTime ExpirationDate {get; set;}} //// The class for an invoice with a promo discountpublic class PromoInvoiceStrategy: IInvoiceStrategy{ private int transactionID; public PromoInvoiceStrategy(int transactionID, Promo promo) { this.transactionID = transactionID; } public Invoice Compute() { Invoice result = new Invoice(); ///// compute for the invoice of the transaction that has a discount due to a promo return result; }} By creating an interface for the computation of the invoice, we can easily implement new invoice computations without changing existing codes. Extending rather than continuously modifying grants us a cleaner and more maintainable code. If the computation for the invoice with a promo needs to be changed because the owner said that it has to be computed differently from now on, then we can easily access that class to change it. If you noticed, the open-closed principle and the single responsibility principle works together. It may not be always the case but if we practice the single responsibility principle, we can easily comply with the open-closed principle and vice versa. In the sample, what we did was to have separate responsibilities in the computation of the bill based on the different discounts offered by the restaurant. In the sample, we were able to practice the open-closed principle by implementing an interface. Another way to achieve the open-closed principle is through polymorphism. By implementing the open-closed principle we become more confident that we can easily deal with new requirements, we can say that our code is maintainable and we can test our code easier. Whenever a new feature is required, don’t modify existing code, extend it.