SOLID architecture principles using C# with examples


SOLID architecture principles help to create good software architecture; basically, it has five principles known as SOLID.  SOLID is a short form you can see below the full form of SOLID:

S:  stands for SRP (Single responsibility principle)
O: stands for OCP (Open closed principle)
L:  stands for LSP (Liskov substitution principle)
I:   stands for ISP (Interface segregation principle)
D: stands for DIP (Dependency inversion principle)
Below I have started to describe each principle with c# examples

 “S”- SRP (Single responsibility principle)

It would be better if first we understand the problem and try to resolve it using the SOLID architecture principle.
See below code and think about what may be the exact problem here.
    public class Orders
    {
        public void SaveOrders()
        {
            try
            {
                // Do your database code to save order
                    
            }
            catch (Exception ex)
            {
                System.IO.File.WriteAllText(@"D:\Elog.txt", ex.ToString());
            }
        }
    }
The above Order class should do customer data validations, call the Order data access layer, etc., but if you notice the catch block, it is maintaining log activity. It means Order class overloaded with a lot of responsibility.

If in catch the blog we need to add a new logger like event viewer I need to go and change the “Order” class, that’s not a good practice.

If we make a separate class for the logging “System.IO.File.WriteAllText(@"D:\Elog.txt", ex.ToString());” it would be better for future changes in the log, It will not disturb to Order class.

SRP says Class should be responsible for only one activity.
    class Log
    {
        public void ErrorHandle(string ex)
        {
            File.WriteAllText(@"D:\Elog.txt", ex.ToString());
        }
    }
Now Order Class can easily use the logging activity to the “Log” class, and he can give attention to Order related activities.

    public abstract class Order
    {
        Log oLog = new Log();
        public virtual void SaveOrder(decimal price)
        {
            try
            {
                //Save Order Goes Here
            }
            catch(Exception ex)
            {
                oLog.ErrorHandle(ex.ToString());
            }
        }
    }

 “O” - Open closed principle
I will continue with Order Example I have added a simple code regarding discount voucher, add voucher property to order class. This property decided user has “GoldCoupon” or “SilverCoupon” regarding this property we have to decide we give discount or not.


class Order
    {
        private string _voucherType;

        public string VoucherType
        {
            get { return _voucherType; }
            set { _voucherType = value; }
        }

        public double Discount(decimal totalPrice)
        {
            if (VoucherType == “GoldCoupon”)
            {
                return totalPrice - (totalPrice*10)/100;
            }
            else
            {
                return totalPrice - (totalPrice*5)/100;
            }
        }
       public double SaveOrder(decimal totalPrice)
{
           //Database Code here
}
    }
The problem is if we need to add a new Voucher type we need to go and add one more “IF” condition in the “Discount” function, in other words, we need to change the Order class.

If we are changing the Order class, again and again, we need to ensure that the previous conditions with new ones are working fine or not and test again, also need to ensure existing user’s or client’s which are referencing this class are working properly as before.
Now we can see the modification of current “Order” class code for every change and every time and also ensure that all previous functionalities are working as before.
Question is arise here,
How can we go for extension except for modification of Order class?
The answer is we can add a new class for a voucher type; see below like this current code will untouched and in the test, we just analyze the new class only.
public abstract class Order
    {
       
        public virtual decimal Discount(decimal totalPrice)
        {
            return totalPrice;
        }
        public virtual decimal VoucherEnquiry(decimal totalPrice, string VoucherType)
        {
            return totalPrice;
        }
        public abstract void SaveOrder(decimal price);
    }

    class GoldCoupon : Order
    {
        public override decimal Discount(decimal totalPrice)
        {
            return base.Discount((totalPrice * 10) / 100);
        }
        public override void SaveOrder(decimal price)
        {
            decimal totalPrice = Discount(price);
            //Database Code here
        }
    }
    class SilverCoupon : Order
    {
        public override decimal Discount(decimal totalPrice)
        {
            return base.Discount((totalPrice * 5) / 100);
        }
        public override void SaveOrder(decimal price)
        {
            decimal totalPrice = Discount(price);
            //Database Code here
        }
    }
Now one new Voucher type “BronzeCoupon ” needs to add here; without any code change in existing classes, we can add easily. See below.
    class BronzeCoupon : Order
    {
        public override decimal Discount(decimal totalPrice)
        {
            return base.Discount((totalPrice * 2) / 100);
        }
        public override void SaveOrder(decimal price)
        {
            decimal totalPrice = Discount(price);
            //Database Code here
        }
    }


 L - LSP (Liskov substitution principle)
I continue with the same example, Suppose discount Enquiries need to add in our system. If any user demanded discount inquiry so we will not save it to our database we just give him info about the discount.
Now we will create a new class named “VchrEnquiry” which inherits from the “Order” class.
See bellow
class VchrEnquiry : Order
    {
        public override decimal VoucherEnquiry(decimal totalPrice, string VoucherType)
        {
            //code here for a discount inquiry like GoldCoupon, SilverCoupon, etc.

            return base.Discount((totalPrice * 2) / 100);
        }
        public override void SaveOrder(decimal price)
        {
            throw new Exception("This is not allowed");
        }
    }
Above code is for new class VchrEnquiry one the problem we are facing here, we don’t need to save order in the database but as per abstract method rule, we have to implement SaveOrder a method so we will code like the same.



So as per polymorphism rule my parent “Order” class object can point to any of it child class objects i.e. “GoldCoupon”, “SilverCoupon” or “VchrEnquiry” during runtime without any issues.

So for instance in the below code you can see I have created a list collection of “Customer” and thanks to polymorphism I can add “GoldCoupon”, “SilverCoupon” and “VchrEnquiry” user to the “Order” collection without any issues.

List<Order> Orders = new List<Order>();
            Orders.Add(new GoldCoupon());
            Orders.Add(new SilverCoupon());
            Orders.Add(new VchrEnquiry());
            Orders.Add(new BronzeCoupon());

            foreach (Order o in Orders)
            {
                o.SaveOrder(10);
            }
When you will run the code you will find an error at SaveOrder method in VchrEnquiry class.

In other words the VchrEnquiry has discount calculation; It looks like an Order class, but it is not an Order. So the parent cannot replace the child object seamlessly. In other words, the Order class is not the actual parent for the VchrEnquiry class. VchrEnquiry is a different entity altogether. So the LISKOV principle says the parent should easily replace the child object, so we need to create two interfaces one is for discount and other for save order into the database, see below.

public interface ISaveOrder
    {
        void SaveOrder(decimal price);
    } 

public interface IDiscount
    {
        decimal Discount(decimal totalPrice);
    }
Now the VchrEnquiry class will only implement IDiscount.

class VchrEnquiry : IDiscount
    {
        public override decimal VoucherEnquiry(decimal totalPrice)
        {
            //your discount code here
            return 10;
        }
    }

Order class will implement both IDiscount as well as ISaveOrder as it also saves the order to the database.

public class Order:IDiscount,ISaveOrder   
    {
       
        public virtual decimal Discount(decimal totalPrice)
        {
            return totalPrice;
        }
        Log oLog = new Log();
        public virtual void SaveOrder(decimal price)
        {
            try
            {
                //Save Order Goes Here
            }
            catch(Exception ex)
            {
                oLog.ErrorHandle(ex.ToString());
            }
        }
    }

And other the class will be the same as below

class GoldCoupon : Order
    {
        public override decimal Discount(decimal totalPrice)
        {
            return base.Discount((totalPrice * 10) / 100);
        }
        public override void SaveOrder(decimal price)
        {
            decimal totalPrice = Discount(price);
            //Database Code here
        }
    }
    class SilverCoupon : Order
    {
        public override decimal Discount(decimal totalPrice)
        {
            return base.Discount((totalPrice * 5) / 100);
        }
        public override void SaveOrder(decimal price)
        {
            decimal totalPrice = Discount(price);
            //Database Code here
        }
    }
    class BronzeCoupon : Order
    {
        public override decimal Discount(decimal totalPrice)
        {
            return base.Discount((totalPrice * 2) / 100);
        }
        public override void SaveOrder(decimal price)
        {
            decimal totalPrice = Discount(price);
            //Database Code here
        }
    }

Now without the confusion, we can create a list of SaveOrder .
List<Order> Orders = new List<Order>();
            Orders.Add(new GoldCoupon());
            Orders.Add(new SilverCoupon());
            //Orders.Add(new VchrEnquiry()); this line will show error
            Orders.Add(new BronzeCoupon());

            foreach (Order o in Orders)
            {
                o.SaveOrder(10);
            }
I - ISP (Interface Segregation principle)
Now your application going well and you have more clients, some new client’s demanded view of Orders after Order Make.
Now what will you do, if you decided to change existing code may be existing client don’t want this facility, now what you will do, see below
public interface IOrderViewV1
    {
        void ViewOrder();
    }

Add a interface with name IOrderViewV1 implement it in new class OrderView see below
public class OrderView : ISaveOrder,IOrderViewV1
    {
        Log oLog = new Log();
        public virtual void SaveOrder(decimal price)
        {
            try
            {
                //Save Order Goes Here
            }
            catch (Exception ex)
            {
                oLog.ErrorHandle(ex.ToString());
            }
        }

        public void ViewOrder()
        {
            //your Order view logic will go here
        }
    }

So here old client will use ISaveOrder Interface, and new will use IOrderView Interface.
     ISaveOrder oI = new Order();
            oI.SaveOrder(10);
            IOrderViewV1 oIV1 = new OrderView();
            oIV1.ViewOrder();


D- Dependency inversion principle
Guys this a very important topic for the interview and ask me many times to me here I tried to explain about Dependency inversion principle.
See below code, in SRP we just discussed this code make a separate class for the error log. 
public class Order
    {
        Log oLog = new Log();
        public virtual void SaveOrder(decimal price)
        {
            try
            {
                //Save Order Goes Here
            }
            catch (Exception ex)
            {
                oLog.ErrorHandle(ex.ToString());
            }
        }
    }
    class Log
    {
        public void ErrorHandle(string ex)
        {
            File.WriteAllText(@"D:\Elog.txt", ex.ToString());
        }
    }

Now we have the requirement to error should be sent on user mail or SMS also and it will be choose by the user if he wants to mail or SMS now what we will do
We implement an interface for log and put code in catch blog, see below.

class EmailLog:ILog
    {
        public void ErrorHandle(string ex)
        {

            //Email code here
        }

    }

    class smsLog:ILog
    {

        public void ErrorHandle(string ex)
        {
            //Sms code here
        }
    }
    class FileLog : ILog
    {
        public void ErrorHandle(string ex)
        {

            //Email code here
        }

    }

public class Order
    {
        private ILog oLog;
        public virtual void SaveOrder(decimal price,string exType)
        {
            try
            {
                //Save Order Goes Here
            }
            catch (Exception ex)
            {
                if (exType == "File")
                {
                    oLog = new FileLog();
                    oLog.ErrorHandle(ex.ToString());
                }
                if (exType == "Email")
                {
                    oLog = new EmailLog();
                    oLog.ErrorHandle(ex.ToString());
                }
                if (exType == "Sms")
                {
                    oLog = new smsLog();
                    oLog.ErrorHandle(ex.ToString());
                }
            }
        }
    }

The See catch block here you find SRP violate, but the aspect is different, it’s about deciding which object should be instantiated.  
Now it’s not the work of Order object to decide which instances to be created, he should be concentrating only on Order class-related functionalities.
The biggest problem is the new keyword. He is taking extra responsibilities of which object needs to be created.
If we give these responsibilities to other someone else except Order class our problem may be solved.
See below code
public class Order:ISaveOrder
    {
        private ILog oLog;
        public Order(ILog iLog)
        {
            oLog = iLog;
        }
        public Order()
        {
        
        }
        public virtual void SaveOrder(decimal price)
        {
            try
            {
                int i = 10, j = 0;
                i = i / j;
                //Save Order code Goes Here
            }
            catch (Exception ex)
            {
                oLog.ErrorHandle(ex.ToString());
            }
        }
    }
    class EmailLog:ILog
    {
        public void ErrorHandle(string ex)
        {

            //Email code here
        }

    }

    class smsLog:ILog
    {

        public void ErrorHandle(string ex)
        {
            //Sms code here
        }
    }
    class FileLog : ILog
    {
        public void ErrorHandle(string ex)
        {

            //Email code here
        }

    }

public class MakeOrder
    {
        public void MyOrder()
        {
            List<Order> Orders = new List<Order>();
            ISaveOrder oI = new Order(new smsLog());
            oI.SaveOrder(10);
           
        }

    }
When you call MyOrder Method, you will get an error, and catch block in Order class call the smsLog class method.

2 comments:

Please do not enter any spam link in the comment box.

Related Posts

What is the Use of isNaN Function in JavaScript? A Comprehensive Explanation for Effective Input Validation

In the world of JavaScript, input validation is a critical aspect of ensuring that user-provided data is processed correctly. One indispensa...