SOLID Principles for beginners

Back to Basics |Write Better Code

“Good design adds value faster than it adds cost.”

This article will cover what SOLID principles are in detail and why we should use them. As we all know, code tends to evolve over time, we write it, and then we come back to it to modify it, to tweak it, to fix bugs — and well-designed code is just so much easier to come back to — because over time bad code becomes harder and harder to maintain. Hence spending time on designing good code will save costs and efforts in the future.

SOLID refers to a set of principles of object-oriented programming and design and these principles are intended to help developers to write maintainable extensible code. Let’s understand them one by one.

S in SOLID stands for Single Responsibility Principle, it states:

A class should have one and only one reason to change, meaning that a class should have only one job.

For example, let’s consider a class Dog, it has only one responsibility ie to behave like a dog. It can eat, walk and bark but it can’t fly because that is the behavior of a bird. Note that the Single Responsibility principle makes it super easy to modify the behavior of a class, ie adding a functionality.

class Dog
def initialize(name = "doggo")
@name = name
end
def walk
puts "I walk"
end
def bark
puts "woof woof"
end

def eat
puts "I eat chimken"
end

def tail(attribute = "pretty")
puts "I have a #{attribute} tail"
end
end
Now, if we want to add another method bite(), it would be super easy to add because our class Dog has just one responsibility that is to behave like a Dog. If we want to add a method fly we need to create another class, because fly is the behavior of a bird.class Bird
def initialize(name = "birdie")
@name = name
end
def fly
puts "I fly"
end
end

In addition to classes, the same reasoning can be applied to methods and even to packages and namespaces. This principle applies at multiple levels and it encourages well-designed code with a clear separation of responsibilities.

Moving on to our second principle, Open/Closed Principle, it says:

Software entities — classes, modules, functions, etc. — should be open for extension but closed for modification.

Basically, we want to design our systems in such a way that we can easily add features without having to modify, recompile and redeploy core components.

We can use inheritance/modules to extend classes and add behaviors to them or we can design classes in such a way that bits of logic can be plugged in without a fuss. Let’s take a real-life example, we made a generic Dog class, but as we know different breeds have different behaviors. Now, what if I want to add behaviours of two different dog species, one is a Golden Retriver, and other is a Corgi. What should I do? Should I create two individual classes? What we can do is inherit from Dog class and then extend the behavior as needed.

class GoldenRetriver < Dog
def bark
"I don't bork unnecessarily"
end

def characteristics
"I am large in size and intelligent and playful"
end

def retrieve(things = "toys")
"I retrieve #{things}"
end
end
class Corgi < Dog
def bark
"I bork pretty lot"
end

def characteristics
"I am small and Cheerful and lively"
end

def herd(thing = "sheep")
"I herd #{thing}"
end
end
Here, both classes are derived from Dog and would behave as a Dog albeit with additional behaviors. Hence the Dog class is closed for modification(We didn't change any methods of the Dog class), and is open for modification(We extended Dog class via inheritance).

The third principle, Liskov Substitution Principle states:

Subclasses should add to a base class’s behavior, not replace it.

Better way to look at it is you should be able to replace any instances of a parent class with an instance of one of its children without creating any unexpected or incorrect behaviors.

For example consider our class Dog having a method tail, but corgi’s don’t have tails, so while calling tail method we always have to check if instance is corgi because we need to handle the exception in that case. This code hence violets the principle of Liskov Substitution.

class Dog 
def tail(attribute = "pretty")
puts "I have a #{attribute} tail"
end
end
class Corgi < Dog
def tail
raise "I don't have a tail"
end
end
Note: You should be able to use subtypes of class interchangeably, with the instance of parent class, this will allow you to easily swap out different implementations without affecting existing code.
Also, if you have to check instance_of? or something similar before calling a method, it's an indication that you are violating the Liskov substitution.

FYI: Ruby being dynamic language and using duck typing design, Interface Segregation and Dependency Inversion principle are not applicable, and hence can’t be violated.

Interface segregation principle, states that:

No client should be forced to depend on methods it does not use.

ISP splits interfaces which are very large into smaller and more specific ones so that clients will only have to know about the methods that are of interest to them. Such shrunken interfaces are also called role interfaces. ISP is intended to keep a system decoupled and thus easier to refactor, change, and redeploy. This principle doesn’t really apply to Ruby, which rely on methods and properties of objects, not strictly their type, to determine suitability. No code example here, sorry.

Last one is Dependency Inversion principle,

High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.

The Dependency Inversion Principle has to do with high-level (think business logic) objects not depending on low-level (think database querying and IO) implementation details.

In conclusion, remember that SOLID principles themselves don’t guarantee great object-oriented design. Apply SOLID principles smartly. To do so, you need to know exactly what problem you’re trying to solve and if the problem is truly a risk for your system. For example, excessively segregating classes to conform with the Single Responsibility Principle may lead to low cohesion and even performance losses.

Programmer, avid Reader, artist, exploring different trades, fun-loving, adventurous and most of the time a hell-yes person!