The S.O.L.I.D Principles — Explained with Examples in Minutes

Anupriya Pilania
6 min readMay 18, 2021

How to measure the quality of code? Many people say that the code has to function 100% of the time, but I’m not convinced. Code quality is how you express whether the code is reusable, easy to read, easy to maintain, tested and so many other things.

So, Is there a way to recognize good code? Fortunately, there is! They are described in the SOLID design principles and various design patterns.

Photo by Brett Jordan on Unsplash

Often, developers tend to dive into the code without acknowledging the bigger picture of how the overall architecture will look and react as time progresses.

As an outcome of this, code becomes brittle — resistant to changes and inflexible which we call the “code smell”. Doing any minute change could have a ripple effect by creating bugs in other areas of software. As and when a new feature comes, it’ll further add to writing larger chunks of code, which also has the same ripple effect for bugs.

To avoid all these risks, you should always follow SOLID principles. These principles, when combined, make it easy for a programmer to develop software that is easy to maintain and extend. So let’s go ahead and understand what the acronym stands for.

S.O.L.I.D stands for:

Single-responsibility principle (SRP)
Open-closed principle (OCP)
Liskov substitution principle (LSP)
Interface segregation principle (ISP)
Dependency inversion Principle (DIP)

S — Single Responsibility Principle

This means a class or a method should be responsible for doing only one thing, and it should do that one thing really well.

In simple terms, a class should only have 1 purpose.

Now let’s go ahead and see how an example violates this principle and how we can fix it.

class User
{
void CreatePost(Database db, string postMessage)
{
try
{
db.Add(postMessage);
}
catch (Exception ex)
{
db.LogError(“An error occured: “, ex.ToString());
File.WriteAllText(“\LocalErrors.txt”, ex.ToString());
}
}
}

We notice the above CreatePost() method holds too much responsibility that is create a new post, log an error to the database and log an error to the local file. Holding so many responsibilities means violating the SRP principle.

Now let’s see the correct way of doing this.

class Post
{
private LogError logError = new LogError();
void CreatePost(Database db, string postMessage)
{
try
{
db.Add(postMessage);
}
catch (Exception ex)
{
logError.log(ex.ToString())
}
}
}
class LogError
{
void log(string error)
{
db.LogError(“An error occured: “, error);
File.WriteAllText(“\LocalErrors.txt”, error);
}
}

By diverting the functionality of error logging, we no longer violate the single responsibility principle.
Now we have two classes each having single responsibility; to create a post and to log an error, respectively.

O — Open/Closed Principle

It states that your code( class, modules, functions etc.) should be open for extension, but closed to modification.

In simple terms, we should be able to add new functionality without changing the existing code. This can be done by utilizing inheritance and/or implementing interfaces that enable classes to substitute for each other in a polymorphic manner.

Now let’s go ahead and see how an example violates this principle and how we can fix it.

class Post
{
void CreatePost(Database db, string postMessage)
{
if(postMessage.StartsWith(“#”))
{
db.AddAsTag(postMessage);
}
else
{
db.Add(postMessage);
}
}
}

In the above code snippet. We need to perform something specific — adding the post as a tag, if it starts with a character “#”. Now, this violates the OCP principle as what happens if we later want to include mentions starting with “@”, we would have to modify the class by introducing an extra “else if” to the CreatePost() method.

Now let’s see the correct way of doing this.

interface IPost
{
void CreatePost(Database db, string postMessage);
}

public class TagPost: IPost
{
Public void CreatePost(Database db, string postMessage)
{
if(postMessage.StartsWith(“#”))
{
db.AddAsTag(postMessage);
}
else
{
db.Add(postMessage);
}
}
}
public class MentionPost : IPost
{
Public void CreatePost(Database db, string postMessage)
{
if (postMessage.StartsWith(“@”))
{
db.AddAsMention(postMessage);
}
else
{
db.Add(postMessage);
}
}
}

In the above code two new classes are created; TagPost and MentionPost by extending them from IPost. This solves the problem of modification of class and by extending an interface, we can extend functionality.

The above code is implementing both OCP and SRP principle, as each class is doing a single task and we are not modifying the class and only doing an extension.

L — Liskov Substitution Principle

The principle says that any class must be directly replaceable by any of its subclasses without error.

In simple terms, Abstraction should be able to provide all Child Class needs.

Now let’s go ahead and see how an example violates this principle and how we can fix it.

class Post
{
void CreatePost(Database db, string postMessage)
{
db.Add(postMessage);
}
}
class MentionPost : Post
{
override void CreatePost(Database db, string postMessage)
{
db.AddAsMention(postMessage);
}
}
class TagPost : Post
{
void CreateTagPost(Database db, string postMessage)
{
string user = postMessage.parseUser();
db.NotifyUser(user);
db.OverrideExistingTag(user, postMessage);
base.CreatePost(db, postMessage);
}
}
class PostHandler
{
private database = new Database();
void HandleNewPosts() {
List<string> newPosts = database.getUnhandledPostsMessages();
foreach (string postMessage in newPosts)
{
Post post;
if (postMessage.StartsWith(“#”))
{
post = new TagPost();
}
else if (postMessage.StartsWith(“@”))
{
post = new MentionPost();
}
else {
post = new Post();
}
post.CreatePost(database, postMessage);
}
}
}

Observe how the call of CreatePost() in the case of a subtype TagPost won’t do what it is supposed to do; notify the user and override the existing tag.
Since the CreatePost() method is not overridden in TagPost the CreatePost() call will simply be delegated upwards in the class hierarchy and call CreatePost() from its parent class.

Let’s see the correct way of doing this.

class TagPost : Post
{
override void CreatePost(Database db, string postMessage)
{
string user = postMessage.parseUser();
NotifyUser(user);
OverrideExistingTag(user, postMessage)
base.CreatePost(db, postMessage);
}
private void NotifyUser(string user)
{
db.NotifyUser(user);
}
private void OverrideExistingTag(string user, string postMessage)
{
db.OverrideExistingTag(_user, postMessage);
}
}

By refactoring the TagPost class such that we override the CreatePost() method rather than calling it on its base class, we no longer violate the Liskov substitution principle.

I — Interface Segregation Principle

It states that no client should be forced to depend on methods it does not use.

In simple terms, do not burden your existing interface by adding new methods.

Instead, we can create separate interfaces for each operation or requirement rather than having a single class do the same work.

Now let’s go ahead and see how an example violates this principle and how we can fix it.

interface IPost
{
void CreatePost();
}
interface IPostNew
{
void CreatePost();
void DeletePost();
}

In the above snippet, let’s assume that we first have an IPost interface with the signature of a CreatePost() method.
Later on, we modify this interface by adding a new method DeletePost(), so it becomes like the IPostNew interface.

This is where we violate the interface segregation principle.
Now the correct way of doing this is simply to create a new interface.

interface IPostCreate
{
void CreatePost();
}
interface IPostDelete
{
void DeletePost();
}

If any class might need both the CreatePost() method and the DeletePost() method, it will implement both interfaces.

D — Dependency Inversion Principle

This principle states that

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.

In simple words, the principle says that there should not be a tight coupling — strong dependency among components of software and to avoid that, the components should depend on abstraction.

Now let’s go ahead and see how an example violates this principle and how we can fix it.

class Post
{
private LogError logError = new LogError();
void CreatePost(Database db, string postMessage)
{
try
{
db.Add(postMessage);
}
catch (Exception ex)
{
logError.log(ex.ToString())
}
}
}

Observe how we create the LogError instance from within the Post class.
This is a violation of the dependency inversion principle.
If we wanted to use a different kind of logger, we would have to modify the Post class.

Let’s fix this by using dependency injection.

class Post
{
private Logger _logger;
public Post(Logger injectedLogger)
{
_logger = injectedLogger;
}
void CreatePost(Database db, string postMessage)
{
try
{
db.Add(postMessage);
}
catch (Exception ex)
{
_logger.log(ex.ToString());
}
}
}

By using dependency injection we don’t have to look to the Post class to define the specific type of logger.

That’s it!
I hope you get a clear understanding of how these principles, when combined, make it easy for a programmer to develop software that is easy to maintain and extend. If you do have any suggestion please leave them in the comment section.

Finally, be sure to follow me so that you do not miss any new stories I publish.

--

--