13 minute read

The SOLID Principles using C# - Principles of Object Oriented Design

How to write code that is easy to maintain, extend and understand.

Object-Oriented Programming(OOP) brought a new design to software development.It organizes software design around data, or objects, rather than functions and logic. OOP focuses on the objects that developers want to manipulate rather than the logic required to manipulate them.This enables developers to combine data with the same functionality in one class to deal with the sole purpose there, regardless of the entire application. However, Object-oriented programming can be complex when creating programs based on interaction of objects which results to confusing and unmaintainable programs.

As such, five guidelines were developed by Robert C. Martin (Uncle Bob). These five guidelines/principles made it easy for developers to:

  • Create more maintainable programs - SOLID principles makes programs easy to read and understand thus spending less time figuring what it does and focus on developing the solution
  • Create Testable Programs - Test driven development is required when we design large scale systems which is important in an application life cycle.
  • Flexibility and Extensibilty - Business requirement change often, SOLID principles will enable you to easily extend the system with new functionality without breaking the existing ones
  • Create Loose Coupled Programs - Loose coupling is an approach where we interconnect components. Loosely coupled class objects minimize changes in your code, help in making code more reusable, maintainable, flexible, and stable.

These five principles were called the S.O.L.I.D principles:

  • Single Responsibility Principle
  • Open-Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

1. The Single Responsibility Principle

The Single Responsibility Principle states that:

A class should have one, and only one, reason to change

This simply means that a class should have one responsibility and therefore it should have only a single reason to change. This does not mean that a class should have one method, but instead all the methods should relate to a single purpose(high cohesion). This makes the classes smaller and cleaner.

Classes don’t often start with low cohesion, but often after several releases and different developers adding data and more behaviors in a class, suddenly the class becomes a monster or god class as some people call it. This makes the code lengthy, complex and difficult to maintain.

Let’s look at the code for a simple Person class as an example. This class should not have an Email validation method as it is not related to a Person behavior.

class Person
    {
        public string Name;
        public string Gender;
        public DateTime DOB;
        public string Email;


        public Person(string name, string gender, DateTime dob, string email)
        {
            this.Name = name;
            this.DOB = dob;
            this.Gender = gender;

            if (validateEmail(email))
            {
                this.Email = email;
            }
        }

        public static bool validateEmail(string email)
        {
            var trimmedEmail = email.Trim();

            if (trimmedEmail.EndsWith("."))
            {
                return false;
            }
            try
            {
                var addr = new System.Net.Mail.MailAddress(email);
                return addr.Address == trimmedEmail;
            }
            catch
            {
                return false;
            }
        }

        public void greetPerson()
        {
            Console.WriteLine($"Hello: {this.Name}");
        }
    }

We can purge the validateEmail Method from the Person Class and create an Email class that will have that responsibility.

class Person
    {
        public string Name;
        public string Gender;
        public DateTime DOB;
        public string email;


        public Person(string name, string gender, DateTime dob, string email)
        {
            this.Name = name;
            this.DOB = dob;
            this.Gender = gender;

            if (Email.validateEmail(email))
            {
                this.email = email;
            }
        }

        public void greetPerson()
        {
            Console.WriteLine($"Hello: {this.Name}");
        }
    }
    
    class Email
    {
        public string email;

        public Email(string email)
        {
            if (validateEmail(email))
            {
                this.email = email;
            }
        }

        public static bool validateEmail(string email)
        {
            var trimmedEmail = email.Trim();

            if (trimmedEmail.EndsWith("."))
            {
                return false;
            }
            try
            {
                var addr = new System.Net.Mail.MailAddress(email);
                return addr.Address == trimmedEmail;
            }
            catch
            {
                return false;
            }
        }

    }

Pay extra attention when creating a class to make sure it has one responsibility for easy understanding, maintenance and re usability.

2. The Open Close Principle - extending your entities correctly

The Open close principle is the most misunderstood solid principle mainly because it’s definition is poorly phrased. However, when correctly applied it can save you more development time compared to others. The principle originally stated that:

“classes should be open for extension and closed to modification.”

Later on when Uncle Bob included it in his SOLID principles he put it much better:

“You should be able to extend the behavior of a system without having to modify that system.”

Modification simply means changing the code and extension means adding new functionality.

A class should be designed in such a way that it’s easy to extend it through inheritance without modifying it. If you want to add additional functionality or change the existing class, create a child class instead of changing the original. This makes sure that anyone using the base class does not have to worry about it’s purpose changing later on.

This principle can be applied when writing a software package or library that is used by many third parties. Ideally you would like the library to be used in the widest variety by it’s consumers, that is, it should be open for extension. However, you wouldn’t like to change the existing code functionality since this forces your library consumers to update their versions and also fix the breaking changes. This makes your package more reliable and stable because it ideally get tested over and over in different contexts. The open/close principle is used to mitigate potential bugs when adding new features. When you don’t touch existing code that works, you can be assured that your code will not break which reduces maintenance costs and improves product stability.

Example

Imagine you use an external library which contains a class Car. The Car has a method brake. In its base implementation, this method only slows down the car but you also want to turn on the brake lights. You would create a subclass of Car and override the method brake. After calling the original method of the super class, you can call your own turnOnBrakeLights method. This way you have extended the Car‘s behavior without touching the original class from the library.

3. Liskov Subtitution Principle

The Liskov subtitution principle states that:

“Subtypes must be substitutable for their base types.”

This principle was created by Barbara Liskov and it’s main objective is to avoid throwing exceptions when inheritance is not used in the right way. Inheritance is an OOP paradigm that enables us to reuse the implementation of base classes; one class can inherit all the attributes and behaviors of another class. Inheritance is supposed to be used by classes that are similar. This requires the objects in your parent class to behave the same way as objects in your child class. An overridden method in a child class also needs to accept the same parameters as the parent class.

Example

A good example for illustrating the principle as given by Uncle Bob in a podcast recently is how something can sound right in the natural language but doesn’t work correctly on code.

Typical Violation of the Principle

  class Bird
    {
        public string Species { get; set; }
        public int Weight { get; set; }
        public int Color { get; set; }

        public void Fly()
        {
            Console.WriteLine("Bird Flies");
        }
    }

    class Pigeon : Bird { }

The Pigeon can fly because it’s a bird.

    class Ostrich : Bird { }

An ostrich is a bird but it can’t fly. Ostrich is a child class of class bird but it should not be able to use the fly() method which means it’s breaking the LSK principle.

    static void Main(string[] args)
    {
        Bird b = new Ostrich();
        b.Fly();
        Console.ReadKey();
    }

Compliance with the Principle

class Bird
    {
        public string Species { get; set; }
        public int Weight { get; set; }
        public int Color { get; set; }
    }

    class FlyingBird : Bird
    {
        public void Fly()
        {
            Console.WriteLine("Bird Flies");
        }

    }

    class Pigeon : FlyingBird 
    { 
        
    }

    class Ostrich : Bird 
    {

    }

A common code smell for the violation of this principle is when a parent class property or method is not applicable to a child class. If it behaves like a duck, it’s certainly a bird. If it behaves like a duck but need batteries to fly - you probably have the wrong abstraction.

4. The Interface Segregation Principle

The interface segregation principle was developed by Uncle Bob when he was consulting for Xerox, a company that produces printers. He was assisting them to build new software for printer systems. The principle states that:

“Clients should not be forced to depend upon interfaces that they do not use”

The principle relates to Single Responsibility Principle in that, Interfaces should also have a single purpose.

Classes that should not be forced to implement interface members that they don’t use.

Typical violation of the principle

Let’s imagine you are responsible for developing a new printing software for Xerox. This is a new system developed from scratch. The modern printers should be able to print, scan and fax.

You define an IModernXerox interface that models the basic tasks performed by the printer.


 interface IModernXerox
{
    bool PrintContent(string content);
    bool ScanContent(string content);
    bool FaxContent(string content);
}
    

You define your printer class as follows:

 class Xerox3Printer : IModernXerox
    {
        public bool FaxContent(string content)
        {
            Console.WriteLine("Fax Done");
            return true;
        }

        public bool PrintContent(string content)
        {
            Console.WriteLine("Print Done");
            return true;
        }

        public bool ScanContent(string content)
        {
            Console.WriteLine("Scan Done");
            return true;
        }
    }

The new printing system becomes a big success, Xerox new three in one printer sales are impressive! Your boss asks you to build a new economic printer that can only Print and Scan. Considering the printer falls under the same line of new modern printers you decide to use the same IModernXerox Interface.

class Xerox4Printer : IModernXerox
    {
        

        public bool PrintContent(string content)
        {
            Console.WriteLine("Print Done");
            return true;
        }

        public bool ScanContent(string content)
        {
            Console.WriteLine("Scan Done");
            return true;
        }

        public bool FaxContent(string content)
        {
            throw new NotImplementedException();
        }
    }
    

This is where the violation of the principle happens, since Xerox4Printer is forced to implement faxing method which it does not support.

Compliance with the Principle

The main problem with the IXerox3Printer is that we use one general ‘god’ interface instead of segregating the fat interface into basic tasks of a modern Xerox printer and advanced features such as faxing.

  interface IModernXeroxBasicTasks
    {
        bool PrintContent(string content);
        bool ScanContent(string content);
    }

    interface FaxContent
    {
        bool FaxContent(string content);
    }

We modify the Xerox3Printer so that it can use the specialized interfaces:

 class Xerox3Printer : IModernXeroxBasicTasks, FaxContent
    {
        public bool FaxContent(string content)
        {
            Console.WriteLine("Fax Done");
            return true;
        }

        public bool PrintContent(string content)
        {
            Console.WriteLine("Print Done");
            return true;
        }

        public bool ScanContent(string content)
        {
            Console.WriteLine("Scan Done");
            return true;
        }
    }

Now Xerox4Printer can be defined without depending upon methods that they will never use.

class Xerox4Printer : IModernXeroxBasicTasks
    {

        public bool PrintContent(string content)
        {
            Console.WriteLine("Print Done");
            return true;
        }

        public bool ScanContent(string content)
        {
            Console.WriteLine("Scan Done");
            return true;
        }
    }

Finally our solution complies with Interface Segregation Principle.

When to apply the principle

The principle just like SRP is very difficult in the first iterations to identify parts that will violate the principle. My advice is to wait your code to evolve so that you can identify where to apply it. Do not try to guess future software requirements this may complicate your solutions.

5. The Dependency Inversion Principle

This is the final SOLID principle and a very important one. Dependency inversion Principle forms the foundation of the most useful feature that a lot of modern frameworks use known as Dependency Injection. It gives your architecture the flexibility to achieve separation of concerns between the presentation, business and data layer. Uncle Bob defines DI as follows:

“A. High-level modules should not depend on low-level modules. Both should depend on abstractions.”

High-level modules and Low-level modules

The concept of modules comes from Modular programming which is a software development technique that advocates the breaking down of a system into small programs/components which accomplishes a single purpose and contains everything to accomplish this.

A system is built using one more modules, which can grouped into different layers.For example a calculator system can be broken down into the following modules:

In this example, there is the high and the low level modules. High level modules are modules that are directly used in the presentation layer for example the Calculator class.On the other hand, low-level modules helps the high level modules to accomplish their work. Low level modules are also referred as dependencies.

Dependencies are established when one module needs another module to complete it’s work. For example, the Calculator Modules needs the Add module to achieve it’s goal and therefore a dependency is achieved. The above design can be implemented as follows:

The Calculator class
class Calculator
    {

        public enum Operation
        {
            ADD, SUBTRACT, MULTIPLY, DIVIDE
        }

        //@param numA              First number.
        //@param numB              Second number.
        //@param operation         Type of operation.
        //@return                  Operation's result.

        public double calculate(double numA, double numB, Operation op)
        {
            double result = 0;
            switch (op.ToString())
            {
                case "ADD":
                    AddOperation addOp = new AddOperation();
                    result = addOp.add(numA, numB);
                    break;
                case "SUBTRACT":
                    SubtractOperation subOp = new SubtractOperation();
                    result = subOp.subtract(numA, numB);
                    break;
                case "MULTIPLY":
                    MultiplyOperation multOp = new MultiplyOperation();
                    result = multOp.multiply(numA, numB);
                    break;
                case "DIVIDE":
                    DivideOperation divOp = new DivideOperation();
                    result = divOp.divide(numA, numB);
                    break;
            }

            return result;
        }

    }

The Calculator Dependency classes - low level modules
class AddOperation
    {
        public double add(double numA, double numB)
        {
            return numA + numB;
        }
    }

    class SubtractOperation
    {
        public double subtract(double numA, double numB)
        {
            return numA - numB;
        }
    }

    class MultiplyOperation
    {
        public double multiply(double numA, double numB)
        {
            return numA * numB;
        }
    }

    class DivideOperation
    {
        public double divide(double numA, double numB)
        {
            return numA / numB;
        }
    }

The Calculator class violates the dependency inversion principle. If we want to add a new operation such as finding a modules, we must modify the class, which conflicts with Open/Close Principle.

Application of the Dependency Inversion Principle

In order to comply with dependency inversion and OCP principle, we must add an abstraction and modify the dependencies.

Now use can change one side of the dependency without affecting the other side, you can also add more operations without modifying the Calculator module.

class Calculator
    {

        //@param numA              First number.
        //@param numB              Second number.
        //@param operation         Type of operation.
        //@return                  Operation's result.
        private ICalculatorOperation _calculatorOperation;
        public Calculator(ICalculatorOperation calculatorOperation)
        {
            this._calculatorOperation = calculatorOperation;
        }

        public double calculate(double numA, double numB, ICalculatorOperation operation)
        {
            return operation.calculate(numA, numB);
        }

    }

    interface ICalculatorOperation
    {
        public double calculate(double numbA, double numB);
    }

    class AddOperation : ICalculatorOperation
    {

        public double calculate(double numbA, double numB)
        {
            return numbA + numB;
        }
    }

    class SubtractOperation : ICalculatorOperation
    {
        public double calculate(double numbA, double numB)
        {
            return numbA - numB;
        }

    }

    class MultiplyOperation : ICalculatorOperation
    {
        public double calculate(double numbA, double numB)
        {
            return numbA * numB;
        }
    }

    class DivideOperation : ICalculatorOperation
    {
        public double calculate(double numbA, double numB)
        {
            return numbA / numB;
        }

    }

 static void Main(string[] args)
{
    //create an object
    AddOperation addOperation = new AddOperation();
    //pass the dependancy
    Calculator calculator = new Calculator(addOperation);

    var number = calculator.calculate(2.3, 3.4, addOperation);
    Console.WriteLine(number);
    Console.ReadKey();
}

Final thoughts on the SOLID principles

The SOLID principles are guidelines that helps you create code that is extendable, maintainable and understand. After finally getting some piece of code to work, you’re only done with half the job; you should spend roughly the same amount of time cleaning it. No one writes clean code first because it’s just too hard to get code to work. SOLID principles are not laws. They will however enable you write clean code. Always remember that:

“You are not done when it works, you are done when it’s right.” - Uncle Bob

Thank You!

I’d love to keep in touch! Feel free to follow me on Twitter at @codewithfed. If you enjoyed this article please consider sponsoring me for more. Your feedback is welcome anytime. Thanks again!

Updated:

Leave a comment