DEV Community

Cover image for Explaining 5+1 SOLID Principles in C# (Learn How To Program Clean Code)
ByteHide
ByteHide

Posted on • Updated on • Originally published at bytehide.com

Explaining 5+1 SOLID Principles in C# (Learn How To Program Clean Code)

Solid principles are patterns, or ways of doing things, which you can use when you're programming to make your code more robust and easier to maintain over time.


What are SOLID principles for?

You might be familiar with the Single Responsibility Principle (SRP) and the Open/Closed Principle (OCP), but there are also four other solid principles that you can use to improve your code and make it more reliable. In this article, we'll look at all five solid principles and how they can help you build better software and, one last principle that is not exactly part of SOLID but is also important and how to apply it.

It's been close to two decades since it was first proposed that a set of tenets or guidelines would aid in simplifying OOP design and developing. Today, many people know these five tenets by heart when it comes to programming in object-oriented languages. Whether they work alone or as part of a team, using these principles has helped make both coding solo easier and collaborative coding more successful. Employers will always prefer candidates who understand the basic concepts behind SOLID design principles because their programs are simply more scalable than others'.


Why follow SOLID principles in C#?

Software developers know how hard it is to build something new and make it perfect. There are always unforeseen consequences, problems arising from the initial design, etc. One issue in particular stands out; because we have put so much work into one product, we don't have time for others. What if this product doesn't do well at all? How will that affect our other products? And every modification or update requires a deep understanding of the entire system we built ( this prevents us from doing anything quickly and efficiently). Let's see Thang Chung example:

Taking care of the design sounds straightforward enough; but it isn't always so easy. For example, it can sometimes be difficult to make sure that every part of a system is designed in such a way that it's clean and scalable - this is where SOLID Design Principles come into play. They help developers find hidden complexities and ensure they're building the right thing before moving forward.


Single Responsibility Principle

The first SOLID principle is known as SRP, or Single Responsibility Principle. According to Martin Fowler's book, Refactoring: Improving the Design of Existing Code; The basic idea is that every class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. In plain English, it means each class should do one thing and one thing only.

Bad way:

class UserSettings
{
    private User User;

    public UserSettings(User user)
    {
        User = user;
    }

    public void UpdateSettings(Settings settings)
    {
        if (ConfirmSMS())
        {
            // ...
        }
    }

    private bool ConfirmSMS()
    {
        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

Good way:

class UserAuth
{
    private User User;

    public UserAuth(User user)
    {
        User = user;
    }

    public bool ConfirmSMS()
    {
        // ...
    }
}

class UserSettings
{
    private User User;
    private UserAuth Auth;

    public UserSettings(User user)
    {
        User = user;
        Auth = new UserAuth(user);
    }

    public void UpdateSettings(Settings settings)
    {
        if (Auth.ConfirmSMS())
        {
            // ...
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

I would like to use the example of a utility knife to cut a steak. Is it possible? Yes, it can. Is it the right thing to do? No. It would be better to use a good knife whose only function is to cut the steak. Here in the code the same thing happens.


Open/Closed Principle

The Open/Closed Principle (OCP) states that a class should be open for extension but closed for modification. It means that you can't modify a class directly to add new functionality, but you can create new classes derived from your original class to add new functionality.

This is usually achieved through inheritance, although there are other ways of implementing OCP.

Bad way:

abstract class AdapterBase
{
    protected string Name;

    public string GetName()
    {
        return Name;
    }
}

class AjaxAdapter : AdapterBase
{
    public AjaxAdapter()
    {
        Name = "ajaxAdapter";
    }
}

class NodeAdapter : AdapterBase
{
    public NodeAdapter()
    {
        Name = "nodeAdapter";
    }
}

class HttpRequester : AdapterBase
{
    private readonly AdapterBase Adapter;

    public HttpRequester(AdapterBase adapter)
    {
        Adapter = adapter;
    }

    public bool Fetch(string url)
    {
        var adapterName = Adapter.GetName();

        if (adapterName == "ajaxAdapter")
        {
            return MakeAjaxCall(url);
        }
        else if (adapterName == "httpNodeAdapter")
        {
            return MakeHttpCall(url);
        }
    }

    private bool MakeAjaxCall(string url)
    {
        // request and return promise
    }

    private bool MakeHttpCall(string url)
    {
        // request and return promise
    }
}
Enter fullscreen mode Exit fullscreen mode

Good way:

interface IAdapter
{
    bool Request(string url);
}

class AjaxAdapter : IAdapter
{
    public bool Request(string url)
    {
        // request and return promise
    }
}

class NodeAdapter : IAdapter
{
    public bool Request(string url)
    {
        // request and return promise
    }
}

class HttpRequester
{
    private readonly IAdapter Adapter;

    public HttpRequester(IAdapter adapter)
    {
        Adapter = adapter;
    }

    public bool Fetch(string url)
    {
        return Adapter.Request(url);
    }
}
Enter fullscreen mode Exit fullscreen mode

It's one of those principles where it's best if you understand its meaning by thinking about what it doesn't mean rather than what it does. So let me explain in a very simple way how not to apply OCP: Don't make methods virtual just so you can replace them with concrete implementations later.


Liskov Substitution Principle

Inherit from an interface not a concrete class. This principle states that inheritance hierarchies should be composed exclusively of interfaces and abstract classes, never concrete classes (e.g., if class B inherits from A and C both, but not D, then D violates LSP). In other words, is-a relationships are stronger than has-an relationships.

Bad way:

class Rectangle
{
    protected double Width = 0;
    protected double Height = 0;

    public Drawable Render(double area)
    {
        // ...
    }

    public void SetWidth(double width)
    {
        Width = width;
    }

    public void SetHeight(double height)
    {
        Height = height;
    }

    public double GetArea()
    {
        return Width * Height;
    }
}

class Square : Rectangle
{
    public double SetWidth(double width)
    {
        Width = Height = width;
    }

    public double SetHeight(double height)
    {
        Width = Height = height;
    }
}

Drawable RenderLargeRectangles(Rectangle rectangles)
{
    foreach (rectangle in rectangles)
    {
        rectangle.SetWidth(4);
        rectangle.SetHeight(5);
        var area = rectangle.GetArea(); // BAD: Will return 25 for Square. Should be 20.
        rectangle.Render(area);
    }
}

var rectangles = new[] { new Rectangle(), new Rectangle(), new Square() };
RenderLargeRectangles(rectangles);
Enter fullscreen mode Exit fullscreen mode

Good way:

abstract class ShapeBase
{
    protected double Width = 0;
    protected double Height = 0;

    abstract public double GetArea();

    public Drawable Render(double area)
    {
        // ...
    }
}

class Rectangle : ShapeBase
{
    public void SetWidth(double width)
    {
        Width = width;
    }

    public void SetHeight(double height)
    {
        Height = height;
    }

    public double GetArea()
    {
        return Width * Height;
    }
}

class Square : ShapeBase
{
    private double Length = 0;

    public double SetLength(double length)
    {
        Length = length;
    }

    public double GetArea()
    {
        return Math.Pow(Length, 2);
    }
}

Drawable RenderLargeRectangles(Rectangle rectangles)
{
    foreach (rectangle in rectangles)
    {
        if (rectangle is Square)
        {
            rectangle.SetLength(5);
        }
        else if (rectangle is Rectangle)
        {
            rectangle.SetWidth(4);
            rectangle.SetHeight(5);
        }

        var area = rectangle.GetArea();
        rectangle.Render(area);
    }
}

var shapes = new[] { new Rectangle(), new Rectangle(), new Square() };
RenderLargeRectangles(shapes);
Enter fullscreen mode Exit fullscreen mode

That is, inheritance chains should be directed acyclic graphs rather than trees; there should be no cycles. Also note that inheritance is not used for code reuse here: inheritance is only used to ensure semantic consistency across distinct types. Code reuse (i.e., code sharing) is achieved by means of interfaces.


Interface Segregation Principle

Every client should be able to use a class by itself, without needing any other classes. If a client needs another class to properly use one of your classes, there's a problem. Most developers don't know it but there is a rule that exists in OOP called the Interface Segregation Principle which states: no client should be forced to depend on methods it does not use. As Thang Chung explains in his example:

Bad way:

public interface IEmployee
{
    void Work();
    void Eat();
}

public class Human : IEmployee
{
    public void Work()
    {
        // ....working
    }

    public void Eat()
    {
        // ...... eating in lunch break
    }
}

public class Robot : IEmployee
{
    public void Work()
    {
        //.... working much more
    }

    public void Eat()
    {
        //.... robot can't eat, but it must implement this method
    }
}
Enter fullscreen mode Exit fullscreen mode

Good way:

public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public interface IEmployee : IFeedable, IWorkable
{
}

public class Human : IEmployee
{
    public void Work()
    {
        // ....working
    }

    public void Eat()
    {
        //.... eating in lunch break
    }
}

// robot can only work
public class Robot : IWorkable
{
    public void Work()
    {
        // ....working
    }
}
Enter fullscreen mode Exit fullscreen mode

In other words, each interface should do only one thing and it should be clear what that one thing is. When we have an interface that contains methods outside of its responsibility, we violate Interfacial Segregation Principle (ISP). To solve these problems with interfaces there are many techniques such as parameterized interfaces and partial classes.


Dependency Inversion Principle

The last of the 5 SOLID principles is called Dependency Inversion Principle (DIP) which was written by Robert C. Martin (commonly known as Uncle Bob). Robert defines the DIP principle as:

It states that high-level modules should not depend on low-level modules. And both should depend on abstractions.

Bad way:

public abstract class EmployeeBase
{
    protected virtual void Work()
    {
        // ....working
    }
}

public class Human : EmployeeBase
{
    public override void Work()
    {
        //.... working much more
    }
}

public class Robot : EmployeeBase
{
    public override void Work()
    {
        //.... working much, much more
    }
}

public class Manager
{
    private readonly Robot _robot;
    private readonly Human _human;

    public Manager(Robot robot, Human human)
    {
        _robot = robot;
        _human = human;
    }

    public void Manage()
    {
        _robot.Work();
        _human.Work();
    }
}
Enter fullscreen mode Exit fullscreen mode

Good way:

public interface IEmployee
{
    void Work();
}

public class Human : IEmployee
{
    public void Work()
    {
        // ....working
    }
}

public class Robot : IEmployee
{
    public void Work()
    {
        //.... working much more
    }
}

public class Manager
{
    private readonly IEnumerable<IEmployee> _employees;

    public Manager(IEnumerable<IEmployee> employees)
    {
        _employees = employees;
    }

    public void Manage()
    {
        foreach (var employee in _employees)
        {
            _employee.Work();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

One reason for DIP is to achieve a separation of concerns, where each module only focuses on one area of concern and does it well without unnecessary dependencies spreading throughout your codebase.

Remember:

  • Abstractions should not depend upon details.
  • Details should depend upon abstractions.

Don't Repeat Yourself

This principle, although as I said at the beginning, it is not really part of the 5 SOLID principles, is a cornerstone of clean coding. It states that every piece of knowledge must have a single, unambiguous, authoritative representation within a system. If you find yourself copying and pasting code or using global variables, you're doing it wrong. DRY code will help you minimize bugs and errors and make your code more flexible and easy to manage.

Bad way:

public List<EmployeeData> ShowDeveloperList(Developers developers)
{
    foreach (var developers in developer)
    {
        var expectedSalary = developer.CalculateExpectedSalary();
        var experience = developer.GetExperience();
        var githubLink = developer.GetGithubLink();
        var data = new[] {
            expectedSalary,
            experience,
            githubLink
        };

        Render(data);
    }
}

public List<ManagerData> ShowManagerList(Manager managers)
{
    foreach (var manager in managers)
    {
        var expectedSalary = manager.CalculateExpectedSalary();
        var experience = manager.GetExperience();
        var githubLink = manager.GetGithubLink();
        var data =
        new[] {
            expectedSalary,
            experience,
            githubLink
        };

        render(data);
    }
}
Enter fullscreen mode Exit fullscreen mode

Good way:

public List<EmployeeData> ShowList(Employee employees)
{
    foreach (var employee in employees)
    {
        render(new[] {
            employee.CalculateExpectedSalary(),
            employee.GetExperience(),
            employee.GetGithubLink()
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Use classes, interfaces and methods when appropriate to ensure that each part of your code only appears once. Think of every class or method as an API: If there's any ambiguity as to how it should be used, you're doing it wrong.


These tips are adapted from Clean Code Javascript. Thanks Ryan McDermott for your great contribution!

Top comments (1)

Collapse
 
adrianbilescu profile image
Adrian-Dieter Bilescu

It's a misinterpretation and dare I say, a false statement to say that Single Responsibility Principle states, "classes should do one thing and one thing only".
It's about reasons to change.
You also referenced the wrong book. Martin Fowler has not mentioned SOLID or Single Responsibility once in his book "Refactoring Improving The Design Of Existing Code".
You will find more at Robert C. Martin and his books.
In the book "Clean Architecture" it is defined as following:
"A module should have one, and only one, reason to change."
"A module should be responsible to one, and only one, user or stakeholder."
"A module should be responsible to one, and only one, actor."
There is a whole capital dedicated to SRP.