S.O.L.I.D. Principles: Simplified Explanation & Example
You guys might have heard about S.O.L.I.D. before, for those of you who still have something missing about this concept maybe because the explanations aren’t telling the point or using a complicated wording. Or for those of you who are totally new and never heard of this before. Now you’re on the right page because I’m going to give you the Simplified Version of this concept. Of course with the violation & correct example. Let’s Start!
First of all, before jumping into each Principle’s implementation, we need to understand what S.O.L.I.D. Principles is, why it is being used & when you are going to implement this. Also, one thing to note is that I’ll consider you are familiar with OOP Concept because S.O.L.I.D. Principles come after OOP. Otherwise, please visit this link before going any further.
Prerequisite
Object-Oriented Programming (OOP)
What S.O.L.I.D. Principles is?
S.O.L.I.D. Principles is a Software Development Principle for programming with OOP Paradigm. These Principles were introduced by Robert C. Martin (Uncle Bob) in his paper Design Principles and Design Patterns (2000).
Yes, you’re not reading it wrongly nor I have a typo. It was written in 2000, 20 years ago since this article was published. But these Principles are still quite popular and are still being used widely in the world of OOP Paradigm.
Why S.O.L.I.D. Principles?
Because it makes your software :
- More Understandable
When you come back to your code six months later, you still understand what you wrote back then. - More Flexible
It’s quite easy when you need to add features in your code because all codes are loosely coupled. - More Maintainable
With the help of points 1 & 2, you will be easier in maintaining your code.
When S.O.L.I.D. Principles are needed?
Everytime you code with OOP Paradigm
Meaning, these Principles are not tied to specific Programming Languages like Kotlin, Swift, Golang, etc. This is a Principles that you’re going to have in your mind when you’re writing codes that support OOP Paradigm.
Up until here, you should have enough knowledge about the theory of using S.O.L.I.D. Principles. Now it’s about the time to dive into each Principle! Here I’ll provide you the Simplified Version of each Principle & 2 Examples which consist of Violation and Correct Example.
The examples are in Kotlin. For those of you who have no experience in Kotlin, bear with me because I’m providing a general description & the most simple code as I can. Also, this is not about the language, it’s more about the concept.
S. in S.O.L.I.D. Principles
First, let’s start with what does this S. stands for :
S. = Single Responsibility Principle
From Wikipedia :
The single-responsibility principle (SRP) is a computer-programming principle that states that every module or 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, module or function.
Don’t worry I’ll give you the Simplified Version :
Simplified Version :
A Class should only have 1 purpose.
Yeah, that’s all the point of the Single Responsibility Principle. Now I’ll provide you the Example :
Violation Example :
class AgeCalculator { private val currentYear = Calendar.getInstance().get(Calendar.YEAR)
fun calculate(birthYear: Int): String {
return (currentYear - birthYear).toString()
}
fun isValid(birthYear: Int): Boolean {
return currentYear > birthYear
}
}
From the highlighted code, we can see that in AgeCalculator
class contains isValid()
function which has different responsibility as what stated in the class name.
This is what violates the Single Responsibility Principle. Meaning, this class contains 2 functions with 2 responsibilities which are :
calculate()
for calculating ageisValid()
for validating age
So, what’s the impact if you leave this code as it is?
Imagine this class grows, and have 10 functions which have 10 responsibility each. When you have got something to modify, you would go to that class. First, you will face 10 functions with their own responsibility which going to make you spend more time understanding the code. Second, you will violate the next principle which is Open-Closed Principle because you’re modifying the existing class. Open-Closed Principle will be discussed after this principle.
So how do we refactor previous code to the correct one? Let’s see
Correct Example :
- First, we left
calculate()
function inAgeCalculator
class.
class AgeCalculator {
fun calculate(birthYear: Int): String {
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
return (currentYear - birthYear).toString()
}
}
- Then, we move
isValid()
function to separateAgeValidator
class.
class AgeValidator {
fun isValid(birthYear: Int): Boolean {
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
return currentYear > birthYear
}
}
Here, we can see that each class has only 1 responsibility which would make us easier to understand the code and won’t violate the next Open-Closed Principle.
O. in S.O.L.I.D. Principles
Let’s start with what O. stands for :
O. = Open — Closed Principle
From Wikipedia :
In object-oriented programming, the open/closed principle states “software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification”; that is, such an entity can allow its behaviour to be extended without modifying its source code.
Here’s the Simplified Version :
Simplified Version :
A Class should be Open for Extension but Closed for Modification.
Maybe some of you might be wondering why Extension is preferred rather than Modification. Actually, this Principle is trying to prevent us from seeing too much code in a class.
Imagine you have a class with 1000 lines of code, and you need to edit something from there. You might have felt overwhelmed the moment you think of it and haven’t even started editing the stuff that you’re going to edit.
Please note that Closed for Modification here doesn’t mean we totally may not edit stuff in classes, but the real meaning is, we can still edit stuff but with minimum changes. Here’s the example :
Violation Example :
- First, we have
ShoesBrand
enum class.
enum class ShoesBrand {
NIKE, ADIDAS
}
- Then, we have ShoesFactory class which have
getShoesPrice
function that acceptsShoesBrand
as parameter.
class ShoesFactory {
// How if there's a new brand of shoes?
// If there are 10 ShoesBrand with Complex Calculation in it, this class will be very Complexfun getShoesPrice(shoesBrand: ShoesBrand): Int {
return when (shoesBrand) {
ShoesBrand.NIKE -> 2000
ShoesBrand.ADIDAS -> 1500
}
}
}
- At last, we have another class which uses
ShoesFactory
as an Object. Then call thegetShoesPrice
method withShoesBrand.NIKE
as argument.
val shoesFactory = ShoesFactory()
tvViolationShoesType.text = shoesFactory.getShoesPrice(ShoesBrand.NIKE).toString()
The highlighted code is prone to be edited. Imagine if your Project Manager comes and says, please update Nike price to 2500 and add another Puma brand with a price of 3000.
With the above code, you will modify the highlighted code and that’s what violates this principle.
So how do we refactor previous code to the correct one? Let’s see
Correct Example :
- First, We have
ShoesBrand
abstract class withgetShoesPrice
abstract function.
abstract class ShoesBrand {
abstract fun getShoesPrice(): Int
}
- Then, we have
NikeShoes
class which extends fromShoesBrand
.
class NikeShoes : ShoesBrand() {
override fun getShoesPrice() = 2500
}
- Then, we have
AdidasShoes
class which also extends fromShoesBrand
.
class AdidasShoes : ShoesBrand() {
override fun getShoesPrice() = 1500
}
- Then, we have
PumaShoes
class which also extends fromShoesBrand
.
class PumaShoes : ShoesBrand() {
override fun getShoesPrice() = 3000
}
- At last, we have another class which uses
NikeShoes
as an Object with the type ofShoesBrand
. Then we callgetShoesPrice
method to show the shoes price into TextView.
val shoes: ShoesBrand = NikeShoes()
tvShoesType.text = shoes.getShoesPrice().toString()
Now, with this code, we can easily update Nike Price without bothering to see other brands of Shoes, and here is what I mean we still need to edit the class but with minimum changes.
Lastly, we can easily extend from ShoesBrand
if there’s a new Brand. Just like what we did in PumaShoes
class.
L. in S.O.L.I.D. Principles
Here, we’ve arrived at Principle that causes brain explosion to most of us. But don’t worry I’ll give you the best-simplified version of this Principle. Let’s start with what L. stands for
L. = Liskov Substitution Principle
From Wikipedia :
In object-oriented programming, behavioral subtyping is the principle that subclasses should satisfy the expectations of clients accessing subclass objects through references of superclass type, not just as regards syntactic safety (absence of method-not-found errors and such) but also as regards behavioral correctness.
I know a brain explosion there. Here’s the Simplified Version :
Simplified Version :
Abstraction should be able to provide all Child Class needs.
If you are still confused with my simplified version definition, try to look at the example below first, then come back to this definition and hopefully things will start making sense.
Violation Example :
- We have
Jobs
interface withdoWork()
function.
interface Jobs {
fun doWork()
}
- Then, we have
Programmer
class which implementsJobs
interface.
class Programmer : Jobs {
override fun doWork() {
println("Working on Software Development Project")
}
}
- Then, we have
Salesman
class which also implementsJobs
interface.
class Salesman : Jobs {
override fun doWork() {
println("Working on Selling Goods")
}
fun talkToCustomer() {
println("I talk to Customer Everyday")
}
}
- Lastly, we have another class which uses
Programmer
as an Object with the type ofJobs
. Then call thedoWork()
method.
val jobs: Jobs = Programmer()
jobs.doWork()
This is okay because doWork()
method is supported by Jobs
interface. So this does not violate the Principle yet.
So where’s the Violating one? Please be patient, the violating part is about to come :
- Lastly, really the last one, the code is pretty much the same as before. We just replace
Salesman
class intoProgrammer
class.
val jobs: Jobs = Salesman()
jobs.doWork()
if (jobs is Salesman) {
jobs.talkToCustomer()
}
Here comes the violation part, pay attention to the highlighted code we check if jobs
is Salesman
then we cast it to Salesman
and then call the talkToCustomer()
method.
This is what Liskov Substitution Principle told us not to do. Jobs
interface should be able to provide what is needed by the child class.
But there is something to note here, all things that are defined in an interface should be implemented correctly by the child class otherwise we’ll violate the next principle which is Interface Segregation Principle. Interface Segregation Principle will be discussed after this principle.
Let’s see how we refactor previous code into the correct one :
Correct Example :
- First, we have
Jobs
interface withdoWork()
andtalkToCustomer()
functions.
interface Jobs {
fun doWork()
fun talkToCustomer()
}
- Then, we have
Programmer
class which implementsJobs
interface.
class Programmer : Jobs {
override fun doWork() {
println("Working on Software Development Project")
}
override fun talkToCustomer() {
println("I seldom talk to Customer")
}
}
- Then, we have
Salesman
class which also implementsJobs
interface.
class Salesman : Jobs {
override fun doWork() {
println("Working on Selling Goods")
}
override fun talkToCustomer() {
println("I talk to Customer Everyday")
}
}
- Lastly, we have another class which uses
Salesman
as an Object with the type ofJobs
. Then call thedoWork()
andtalkToCustomer()
methods.
val jobs: Jobs = Salesman()
jobs.doWork()
jobs.talkToCustomer()
Here you can see that Jobs
interface has got talkToCustomer()
function. So, in order to use talkToCustomer()
method, there is no need for us to check if jobs
is Salesman
or not anymore.
Hopefully reaching this part, your brain is not exploding anymore.
I. in S.O.L.I.D. Principles
We’ve reached the fourth Principle, the second last one. Let’s start with what I. stands for
I. = Interface Segregation Principle
From Wikipedia :
In the field of software engineering, the interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.
The most simple Definition of Wikipedia so far. But here’s the Simplified Version :
Simplified Version :
Small Interface is better than Big Interface.
This principle is the next stage after the Liskov Substitution Principle where functions that we define in abstraction should be implemented correctly. Meaning, we should not force child class to implement things that they can’t do.
As we know, everything that is defined in Interface or abstract function should be implemented by the child class, otherwise, it would output a compiler error. So, we’re encouraged to separate interface into a smaller one. Here’s the example :
Violation Example :
- First, we have
OnClickListener
interface withonClick()
andonLongClick()
functions.
interface OnClickListener {
fun onClick()
fun onLongClick()
}
- Then, we have
ISViolationActivity
class which implementsOnClickListener
interface.
class ISViolationActivity : AppCompatActivity(), OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_interface_segregation)
btnChangeName.setOnClickListener {
onClick()
}
}
@SuppressLint("SetTextI18n")
override fun onClick() {
tvName.text = "Franz Andel"
}
override fun onLongClick() {
// This function is not used, but is forced to be implemented
}
}
The interface consists of 2 functions in which if we only need one of the function inside that interface which is onClick()
function, but we are forced to implement both of the functions. And this is what violates the principle. We should separate Interface into a smaller one so we can choose which to be used based on what we need.
Here’s how we refactor previous code into the correct one :
Correct Example :
- First, we have
OnClickListener
interface withonClick()
function.
interface OnClickListener {
fun onClick()
}
- Then, we have
OnLongClickListener
interface withonLongClick()
function.
interface OnLongClickListener {
fun onLongClick()
}
- Lastly, we have
ISBestPracticeActivity
class which implementsOnClickListener
interface.
// Make a Smaller Interface, so Child Class can choose which Interface to be Implementedclass ISBestPracticeActivity : AppCompatActivity(), OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_interface_segregation)
btnChangeName.setOnClickListener {
onClick()
}
}
@SuppressLint("SetTextI18n")
override fun onClick() {
tvName.text = "Franz Andel"
}
}
Now with this code, we can easily choose which interface that we need and then implement it.
In the example, we only need onClick()
function so we only implement OnClickListener
interface, but in the future, there might be a chance for you to need onLongClick()
function too. This is simply solved by implementing both OnClickListener
& OnLongClickListener
interface. As you may know, in Java or Kotlin we can implement more than 1 interface.
D. in S.O.L.I.D. Principles
Finally, we’ve reached the last Principle. Let’s start with what D. stands for
D. = Dependency Inversion Principle
From Wikipedia :
In object-oriented design, the dependency inversion principle is a specific form of decoupling software modules.
The shortest definition of Wikipedia at last. Here’s the Simplified Version :
Simplified Version :
A Class should depend on Abstraction, not Implementation.
Let’s get into the example to make things clearer :
Violation Example :
- First, we have
Dollar
class withcurrency()
function.
class Dollar {
fun currency() = "$"
}
- Then, we have
Rupiah
class withmoneyCurrency()
function.
class Rupiah {
fun moneyCurrency() = "Rp."
}
- Lastly, we have
DIViolationActivity
class with a setter functionsetConversion
that acceptsDollar
class as parameter and to show currency to the TextView, we calldollar.currency()
method.
class DIViolationActivity : AppCompatActivity() {
// How if you want to change dollarViolation to RupiahViolation
// while dollarViolation is used in many places? private lateinit var dollar: Dollar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dependency_inversion)
setConversion(Dollar())
setUI()
}
private fun setConversion(dollar: Dollar) {
this.dollar = dollar
}
@SuppressLint("SetTextI18n")
private fun setUI() {
tvMyMoney.text = "${dollar.currency()}5.000"
}
}
Here we have 2 implementation classes with their own currency. In order to call currency()
method in Dollar
, we make an object of it.
Now, imagine how if your Project Manager comes and says, please change the currency of our money into Rupiah.
With this code, you definitely need to change :
- the variable of
dollar
intoRupiah
, setConversion
argument intoRupiah()
,setConversion
function’s parameter intoRupiah
dollar.currency()
intorupiah.moneyCurrency()
.
We can conclude that there are 4 efforts there. And this is what violates the Principle.
Here’s how we refactor the previous 4 efforts code into 1 effort code :
Correct Example :
- First, we have
MoneyConversion
interface withcurrency()
function.
interface MoneyConversion {
fun currency(): String
}
- Then, we have
Dollar
class which implementsMoneyConversion
interface.
class Dollar: MoneyConversion {
override fun currency() = "$"
}
- Then, we have
Rupiah
class which also implementsMoneyConversion
interface.
class Rupiah: MoneyConversion {
override fun currency() = "Rp."
}
- Lastly, we have
DIBestPracticeActivity
class with a setter functionsetConversion
that acceptsMoneyConversion
interface as parameter and to show currency to the TextView, we callmoneyConversion.currency()
method.
class DIBestPracticeActivity : AppCompatActivity() {
private lateinit var moneyConversion: MoneyConversion
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dependency_inversion)
// It can be easily changed to Rupiah
setConversion(Dollar())
setUI()
}
private fun setConversion(moneyConversion: MoneyConversion) {
this.moneyConversion = moneyConversion
}
@SuppressLint("SetTextI18n")
private fun setUI() {
tvMyMoney.text = "${moneyConversion.currency()}5.000"
}
}
Now, we’ve made DIBestPracticeActivity
class depends on its Abstraction which is MoneyConversion
interface.
With this code, we can easily change setConversion()
argument to Rupiah()
. And this is only 1 effort.
Conclusion
This concept will boost your productivity if implemented correctly because your code will be loosely coupled & easier to read.
If you need to look directly into the full source code. You can clone it from here :
I hope you get a better understanding of this concept after reading this. If you do have any suggestion please leave it in the comment section.