[C#] Easy-to-understand SOLID principles with examples

Eden Park
6 min readMay 14, 2022

Introduction

Why I am writing this? Many of the resources are explaining SOLID principles in such a hard way. You just started learning programming and you just wanted to know the core concepts of SOLID principles without being intimidated by a bunch of complex theories.

Single Responsibility

Every module, class or function in a computer program should have responsibility over a single part of that program’s functionality [1]

I believe that not many people would mess up with Single Responsibility. Think about we have a method called Subtraction and Addition in a class called Calculator. We can’t really mix Addition with Subtraction logic. The same way each function and class should have one and only one responsibility. It’s always better to create another class or method if topics, subjects, concepts are different.

The same way in Calculator class, you really don’t want to put a SendEmail() function to it, do you? That’s simply called Single Responsibility. Do not surprise other developers. Normal developers might expect that there might be some kind of an EmailService dealing with SendEmail() method instead of in the Calculator class. That way, the code is easy to maintain, read and extend.

Open Close Principle

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”; that is, such an entity can allow its behavior to be extended without modifying its source code. [2]w

It basically means that you should code or design in a way that existing code should not be touched to add more logic.

You are creating a salary calculator; one for graduate developers and the other for senior developers. Their salaries are calculated differently as senior developer might have bonus (😃) and graduate developers not (😥). You have a method called CalculateSalary(). You might come up with adding multiple if statements to make it work such as below. But that will break the open for extension and close for modification principle because when intermediate developers come on board whose salary is calculated differently, then, we need to touch the existing code by adding another if statement.

How can we solve this problem? We probably use some contract such as Abstract class or Interface then inherit them as below. With the design below, you can simply add more levels of experienced-developers’ calculators without touching the existing code. What’s the beauty of it? Well, first you are not touching the existing logic that you might inadvertently break the existing logic by doing so. Maintainability? Yes, if you want to change the logic of GraduateCalculator, then just go in one place and change the logic without affecting other Calculators. What about scalability? Yes, it’s easy to scale as you can add as many calculators you want.

Liskov Substitution Principle (LSP)

Object (such as a class) and a sub-object (such as a class that extends the first class) must be interchangeable without breaking the program [3]

This simply means that your child class’s object should be replaceable of the parent class object without breaking the program or logic.

The most famous example might be from Robert C. Martin (you may have heard of him already) Square and Rectangle. [4] Square is a rectangle as it possesses all properties of rectangle. Then you might come up with an idea inheriting Rectangle from Square just like the code below.

The problem

Square’s width and height should be identical, but you are allowing users to set different height and width. What did I say? Parent object and a child object should be interchangeable without breaking the logic. Then, now the derived class object (square) can’t replace the parent class object (rectangle) due to a bad design.

The Solution

There could be many different solutions for that such as not inheriting Rectangle class and manage both classes differently, creating different Area() methods for Rectangle and Square in a new base class, Shape, or making the Area() method as overridable in a new base class, Shape, to satisfy LSP. I will go through the last solution as an example.

This way we are coding adhering to LSP. What are the benefits of it? First, we are not breaking any logic of the parent class object (shape in this case). Obviously, we reduced coupling between Square and Rectangle, which would give you a headache when you try to modify the Sqaure code, because you need to also think about how the Rectangle logic is affected. We can also reuse Shape quite often when a new shape comes in as well as easily maintain the code as you can simply go to Square.Area() to fix the issue when raised.

Interface Segregation

Client should not be forced to depend upon interfaces that they do not use.

This is the quote from Robert C. Martin when he consulted Xerox when they are trying to create a printing system. It’s kind of obvious, isn’t it? Simply, create another interface (contract) to separate the concern.

Look at the example below.

The Problem

We create an interface calls IVehicle and forcing a child class to inherit Fly() and Drive(). As you logically see, at the moment, cars can’t fly properly I mean.

The Solution

As you expected, create separate interfaces for each Car and Airplane. Now everybody’s happy. Car doesn’t need to worry about Fly() and so does Airplane Drive(). Yeah, simple as that. I have seen many senior developers being afraid of creating extra files or folders. But, it’s normally good to have a separate folders, files, interfaces if you think you can implement separation of concern.

Dependency Inversion (DI)

High level modules should not depend on low level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions

The first thing that came up to my mind was what the hell are you talking about? See the three important terms that might alleviate your headache.

High level modules: It’s more of a business context rather than technical. It could be explained as client or user’s facing module. A client can be anything that calls the high-level module. If I (client) click Order Button, then IOrderService which will be facing first, then IOrderService would be a high level module. The same way, if a Factory class calls ICandyFactory class to manufacture candies, then Factory class is a client and ICandyFactory could be a high level module.

Low level modules: any sub modules helping the high level module to make it work. For example, to make an order properly, you need to have OrderModelValidator to check if all the information is coming correctly. OrderModelValidator can be a low level module as it’s helping IOrderService. To make candies, ICandyFactory needs to use WrapClass that wraps candies properly. WrapClass (low-level module) is helping ICandyFactory (high level module) to make it work.

Abstraction: As you know, interfaces or abstract

Simply saying. high level module and low level module should depend on abstraction (interface or abstract class) for decoupling. Why? you know the problem of tight coupling; not easy to maintain and extend.

Look at the example. Program (Console App), which runs the CandyFactory, is the client. CandyFactory is the highest module as it’s facing first when Program runs. CandyWrap class is for wrapping candies to finish off Candy-making thus low-level module.

This will result in: Shape Candy, Put Flavors, Candies have been tied.

The Problem

Now, you need to wrap marshmallows because boss wants to sell them. Hey I am not ready for that because this design is not extendable or easy to fix. Also, higher module (CandyFactory) is depending on low-module (CandyWrap) which violates Dependency Inversion principle. Because you need to touch the existing code to add Marshmallow Wrap, thus you are also violating Open Closed Principle.

The Solution

As the DI suggests already, high-level and low-level module should depend on abstractions. We had dependencies between CandyFactory and Wrap so we are changing the code so that CandyFactory depends on Abstraction. As suggested, I am going to make an interface called WrapService for WrapClass and make high-level CandyFactory depend on WrapService instead of CandyWrap. As you see the code below, MarshmallowWrap, CandyWrap both inherit from WrapService and CandyFactory now depends on WrapService. What are the benefits? Well, you can now add ChocolateWrap without touching existing code! (Don’t lecture on marshmallow and chocolates are not candies 🤪🤪)which is awesome. As well as that, it will be easy to maintain, reusable and most of all, it’s clean!

Summary

I hope I didn’t make you sick for reading quite a lengthy article. But I tried to explain with simplest examples so you guys can grab core concepts and beauty of SOLID principles and why you need to keep SOLID principles in mind when coding.

References

[1] https://en.wikipedia.org/wiki/Single-responsibility_principle

[2] Open–closed principle — Wikipedia

[3] Liskov substitution principle — Wikipedia

[4] Wayback Machine (archive.org)

--

--