SOLID principles using Kotlin
As software developer, we faced some issues on daily basis and some issues are mostly due to bad decisions we took while writing the code.
We write code according to business requirements, and after some days minor business requirement change comes and makes our code useless, and changing it will be a huge task.
While writing the code we designed it without following coding standards, we just keep adding line after line without thinking of its implications in future and tell ourselves that we will refactor it once we will get free time but everyone knows the cost of refactoring is higher as we are dealing with production code.
A famous quote from Robert Martin in one of his lectures,
Bad code slows us down, but why do we write bad code? to go fast 😅.
Best code is boring and you can predict the next step.
Some common mistakes while writing code,
- Code is rigid, one change in code affects other parts of code.
- The code is unreadable, and one class is doing too many things.
- Code is unpredictable, as we are not following coding principles and standard.
How can we deal with this code?
S.O.L.I.D. Principles are solution for most of the problems. Let’s go one by one.
S: Single Responsibility Principle
A class should have only 1 reason to change.
This principle tells us that one class should have one specific task and should avoid doing multiple things.
Let’s take one example, We have one company where there are thousands of employees working, and for that, we have one Employee class. Let’s check out what does class does?
class Employee(val name: String, val dept: String){
fun calculateSalary(){
//some math formulae to calculate the salary
}
fun reportHours() {
// how many hours does employee work
}
fun save(){
//save employee details
}
}
Now in the above class, we have three different tasks, doing some work, now if I have to do some changes in calculateSalary I will come to this class if I have to do some changes in reportHours again I will come to this, similarly, if I have to add a new entity to employee model I will come here. Now, this class is overloaded with a lot of functionality, and making this class hard to maintained. If any bugs comes, debugging will be hard.
Let’s find solution for this, Create separate classes for each task and pass the employee object to each class like below.
interface SalaryRepo {
fun calculate(emp: Employee)
}
interface AuditRepo {
fun reportHour(emp: Employee)
}
class SalaryRepoImpl: SalaryRepo {
fun calculate(emp: Employee){
}
}
class AuditRepoImpl: AuditRepo {
fun reportHour(emp: Employee){
}
}
Advantage of single responsibility principle
- We can reuse the module at other places, as there is no extra functionality present.
- Debugging will be easy as we can isolate the problem easily.
- Makes code more readable.
O: Open/Close principle
Software entities such as classes, modules, functions, etc. should be open for extension, but closed for modification
This is most important principle of SOLID principles according to Robert Martin.
As definition suggest, Suppose if we have any class which is required some changes due to some business requirement change, you should create new subclass which is extending current class and overide the existing function instead of changing the original implementation Or create abstract Class and extend it. Reason for this is pretty straightforward because if we change the original function it will have some side-effect. Code will have unnecessary logic at one class, also QA team have to test entire flow as we modified the existing flow. Also it will break the single responsibility principle.
Let’s go through this example
class Employee(
val name: String,
val salary: Int,
) {
fun calculateBonus(): Double {
return salary * 0.10
}
}
fun main(){
val employee = Employee("John", 10000)
println("Emp Bonus: " + employee.calculateBonus())
}
We have one employee class and we are calculating bonus, hmm looks simple and code is readable also everything seems fine right? Spoke to soon, now comes new requirement 😈.
Now business want to give bonus to contract employees also, now we will add one simple hack here by adding the employee type and comparing the employee type and modifying the existing code like below.
class Employee(
val name: String,
val salary: Int,
val type: String
) {
fun calculateBonus(): Double {
return if(type == "Permanent"){
salary * 0.10
} else {
salary * 0.05
}
}
}
This comes violation of Open/Close principle, now when there is 1 more type we will add 1 more if condition and make code more complicated and doing unnecessary checks. Even after dong this we have to add more unit tests and hope it doesn’t break in production.
There is very simple solution for this, checkout below code
abstract class Employee(
val name: String = "",
val salary: Int = 0
) {
abstract fun calculateSalary(): Double
}
class PermanentEmployee(
name: String = "",
salary: Int = 0
) : Employee() {
override fun calculateSalary(): Double {
return salary * 0.10
}
}
class ContractEmployee(
name: String = "",
salary: Int = 0
) : Employee() {
override fun calculateSalary(): Double {
return salary * 0.05
}
}
fun main(){
val employee = PermanentEmployee("John", 10000)
val contractEmployee = ContractEmployee("Mac", 10000)
println("Permanent Emp Bonus: " + employee.calculateBonus())
println("Contract Emp Bonus: " + contractEmployee.calculateBonus())
}
Here we created one abstract class Employee and it has one abstract function calculateSalary, this abstract class is extended by PermanentEmployee and ContractEmployee who will override calculateSalary function and their respective logic to it. By doing this our logic will be separated and we will not violate the Open/Close principle as we are extending the abstract class and not modifying it.
L: Liskov substitution principle
This principle is hard to understand if you go by the definition available on internet, but in my perspective it is pretty straight forward. I have change some wording according to my understanding.
ChildrenClass(subtype class) which is extending the ParentClass then ChildrenClass should able to substitute the ParentClass without any side-effects.
open class Bird(private val weight: Int, private val color: String) {
open fun flyingSpeed(): Int {
//some calculation
return x
}
}
class Ostrich(weight: Int, color: String) : Bird(weight, color ){
override fun flyingSpeed(): Int {
throw Exception("Ostrich can't fly")
}
}
class Parrot(weight: Int, color: String) : Bird(weight, color)
Take above example, we have Bird class and and we defined some basic functionality like flyingSpeed(just for sake of example, I am no bird expert). Now we created one more class Ostritch. Almost all birds can fly and we created flyingSpeed method on basis of it, but Ostrich can’t fly so we overide the method flyingSpeed and throws exception. Now this is the problem, yes we can replace the ostritch with bird as it is extending it, but it is not giving expected result for flyingSpeed function which is side-effect. So Ostrich can’t replace Bird and violating the Liskov substitution principle. Here if we create class Parrot which can fly will perfectly substitute the bird class without any side-effect.
I: Interface segregation
Interfaces should be segregated to lower levels so that the Class which is implementing it will have required functionality.
Each class should be concern with particulat behavior or functionality.
Let’s take a example, we have one Restaurant and we have different tasks performed by restaurant employees like cookFood, serveFood, makeBill, takeOrder.
interface Task{
fun cookFood()
fun serveFood()
fun makeBill()
fun takeOrder()
}
class Waiter: Task {
override fun cookFood() {
TODO("Not yet implemented")
}
override fun serveFood() {
println("Serving food")
}
override fun makeBill() {
TODO("Not yet implemented")
}
override fun takeOrder() {
println("Serving food")
}
}
In the above example, you guys must have figureout the problem, Waiter is not responsible for cooking food and making bills so this generic interface is violating our Interface segregation principle. It is also sending wrong signals by saying that the waiter has makeBill and cookFood functionality available.
How can we tackle this?
By creating class-specific interfaces like below
interface WaiterTasks{
fun serveFood()
fun takeOrder()
}
interface ChefTasks{
fun cookFood()
}
interface CashierTasks{
fun makeBill()
}
class Waiter: WaiterTasks{
override fun serveFood() {
println("Serving food")
}
override fun takeOrder() {
println("Serving food")
}
}
class Chef: ChefTasks {
override fun cookFood() {
print("Cooking food")
}
}
class Cashier: CashierTasks {
override fun makeBill() {
print("making bill")
}
}
Now here we fixed the problem by creating three different interfaces and segregated according to their tasks. This makes our code cleaner and more readable.
D: Dependency inversion
High-level modules should not have knowledge about low-level modules, and both should communicate using the interface.
This principle is teaching us that 2 classes should be loosely coupled, and if there change in low-level modules, it should not affect the high-level module.
Let’s take 1 basic example, we are working on a food delivery service, and now we have one payment option available ABCPayment.
data class User(var name: String = "", var email: String = "", var age: Int = 0)
class OrderFoodService (private val user: User){
val abcPayment: AbcPayment by lazy {
AbcPayment(user)
}
fun order(){
abcPayment.makePayment(1000)
}
}
class AbcPayment(val user: User){
fun makePayment(amount: Int){
// API to make payment
}
}
In the above code, as we can see we are creating an object of AbcPayment and making a payment, Now what happened here is we are using AbcPayment and everything is fine and we are good, one day your Product Manager came and tell that we are out of budget and AbcPayment is charging more for each transaction, we have to replace AbcPayment gateway with XyzPayment.
Now you are thinking WTF, we have to change a hell lot of code, and testing this flow is going to be an exhaustive task.
Why did this happen?
The answer is simple, our OrderFoodService is tightly coupled with AbcPayment. Let’s fix this, Below is a simple solution for it
data class User(var name: String = "", var email: String = "", var age: Int = 0)
interface Payment {
fun makePayment(amount: Int, user: User)
}
class OrderFoodService(private val user: User, private val payment: Payment) {
fun order() {
payment.makePayment(1000, user)
}
}
class XyzPayment : Payment {
override fun makePayment(amount: Int, user: User) {
print("Using payment gateway Xyz")
}
}
class AbcPayment : Payment {
override fun makePayment(amount: Int, user: User) {
print("Using payment gateway Abc")
}
}
Now let’s check out the updated solution, We created 1 interface named Payment and both AbcPayment and XyzPayment both(Low-level modules) are implementing it. Now we are passing the payment object to OrderFoodService (High-level module), here OrderFoodService is unaware of the Payment gateway we are using and it is communicating using the Payment interface. By doing this we are preventing any changes at High-Level module and we don’t have to change any unit test also.
Special note: Don’t get confused with Dependency injection(DI) and dependency inversion(DIP). They are totally different concepts. Dependency injection tells us that all the dependencies should come from outside and there should not be any object creation in Class, so that while unit testing the code, we can easily mock the functionality for a given object which is injected.
Now if we check our code, you will find at many places we have violated the above principles. If your business permits, pick up small chunk of code and try to fix it, add unit test. Repeat the process till you fix at all places.
For new features if we follow SOLID principles or just take some time before implementing any new feature and go through all the principles and take the decisions, we will able to write more robust code which will be reusable, easy to understand, and extend without affecting existing code.
Please let me know the feedback for this blog. Let’s write better code and Happy coding.