tcrin / solid-principles

đź’Ž SOLID principles explained in Kotlin with examples

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Solid Principles

alt text

The Solid Principles are a set of coding principles that provide a guide to writing maintainable, scalable, and robust software. The Solid principles are not limited to classes, it can be applied to various software entities such as modules, functions, and components.

âś…Single Responsibility Principle (Srp):

A class should have only one reason to change, meaning it should have only one responsibility.

alt text

Before:

class DatabaseManager(private val databaseName: String) {
    
    fun connectToDatabase(){
        // Implementation code removed for better clarity.
    }

    fun saveDataToDatabase() {
        try {
            // Implementation code removed for better clarity.
            // Perform some operation that may throw an exception.
        } catch (e: Exception) {
            /* 
            ❌ This code violates the Single Responsibility Principle (SRP)
             because the `DatabaseManager` class has two responsibilities:
            1. Saving data to the database.
            2. Writing an error message to a log file.
            */
            File("logFile.txt").writeText(e.message!!)
        }
    }
}

After:

class DatabaseManager(private val databaseName: String) {

    fun connectToDatabase(){
        // Implementation code removed for better clarity.
    }

    fun saveDataToDatabase() {
        try {
            // Implementation code removed for better clarity.
            // Perform some operation that may throw an exception.
        } catch (e: Exception) {
            // âś… Ok
            val logger = FileLogger("logFile.txt")
            logger.log(e.message!!)
        }
    }

}

In this refactored code, the DatabaseManager class only focuses on saving data to the database, while the FileLogger class is responsible for logging errors. Each class now has a single responsibility, and any changes related to error logging won't affect the DatabaseManager class.

âś…Open/Closed Principle (Ocp):

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

alt text

Before:

class PayCalculator(var currency: String) {

    // We use currency in implementation.

    fun calculatePay(typeEmployee: TypeEmployee) {
        if (typeEmployee == TypeEmployee.FULL_TIME) {
            // Implementation code removed for better clarity
        } else if (typeEmployee == TypeEmployee.PART_TIME) {
            // Implementation code removed for better clarity
        } else if (typeEmployee == TypeEmployee.CONTRACTOR) {
            // Implementation code removed for better clarity
        } else {
            // Implementation code removed for better clarity
        }
    }

    enum class TypeEmployee { FULL_TIME, PART_TIME, CONTRACTOR }

   // Other methods
}
  • The class isn't closed for modification because modifications are needed whenever a new employee type is added.
  • The class isn't open for extension because you would need to modify the existing class and add new conditions to handle new employee types.

After:

We don't need enum after refactoring, so delete it.

interface Payable{
    fun calculatePay()
}
class FullTimeEmployeePayable(var hoursWorked:Double) : Payable {
    override fun calculatePay() {
        // Implementation code removed for better clarity
    }
}
class PartTimeEmployeePayable(var hourlyRate:Double) : Payable {
    override fun calculatePay() {
        // Implementation code removed for better clarity
    }
}
class ContractorPayable(var projectDuration:Double) : Payable {
    override fun calculatePay() {
        // Implementation code removed for better clarity
    }
}
class PayCalculator(var currency: String) {

    // We use currency in implementation.

    fun calculatePay(payable: Payable) {
        // Implementation code removed for better clarity
        payable.calculatePay()
    }
   // Other methods
}

✅Liskov’s Substitution Principle (Lsp):

Subtypes must be replaceable with their base types without affecting the correctness of the program.

alt text

Before:

open class Rectangle(var width: Int, var height: Int) {
    open fun calculateArea(): Int {
        return width * height
    }
}
class Square(side: Int) : Rectangle(side, side) {

    override fun calculateArea(): Int {
        if (height != width)
            throw IllegalArgumentException("The width and height of a square should be equal!")
        return width * width
    }
}
fun main() {
    val rectangle: Rectangle = getDefaultRectangle()
    rectangle.width = 7
    rectangle.height = 8
    println(rectangle.calculateArea())
}

private fun getDefaultRectangle(): Rectangle {
    return Rectangle(3, 6)
}

private fun getDefaultSquare(): Rectangle {
    return Square(3)
}

The program encounters a problem when we replace the rectangle (getDefaultRectangle) with a square (getDefaultSquare).

After:

interface Shape {
    fun calculateArea(): Int
}
class Rectangle(var width: Int, var height: Int) : Shape {
    override fun calculateArea(): Int {
        return width * height
    }
}
class Square(var side: Int) : Shape {
    override fun calculateArea(): Int {
        return side * side
    }
}

âś…Interface Segregation Principle (Isp):

Clients should not be forced to depend on interfaces they do not use.

Bad example

interface Animal {
    fun fly()
    fun swim()
}

Good example

interface Flyable {
    fun fly()
}
interface Swimmable  {
    fun swim()
}

alt text

Before:

interface Worker {
    fun work()
    fun eat()
}
class Robot(private val numberRobot:Int) : Worker {
    override fun work() {
        // Implementation code removed for better clarity.
    }

    override fun eat() {
        // ❌ ISP (Interface Segregation Principle) violation occurs when a class does not need a method.
        // This method is not applicable to a robot.
        throw UnsupportedOperationException("Robots don't eat!")
    }

}


class Human(private val name:String) : Worker {
    override fun work() {
        // Implementation code removed for better clarity.
    }

    override fun eat() {
        // Implementation code removed for better clarity.
    }
}

After:

interface Workable {
    fun work()
}

interface Eatable {
    fun eat()
}
class Human(private val name:String) : Workable, Eatable {
    override fun work() {
        // Implementation code removed for better clarity.
    }

    override fun eat() {
        // Implementation code removed for better clarity.
    }
}

class Robot(private val numberRobot:Int) : Workable {
    override fun work() {
        // Implementation code removed for better clarity.
    }
}

âś…Dependency Inversion Principle (Dip):

High-level modules should not depend on low-level modules, both should depend on abstractions.

alt text

❌ Problem: Suppose we have another logger class, then should we create another class like DatabaseManager again?

This class basically only depends on FileLogger, but what if we need DatabaseLogger?

Before:

class DatabaseManager(private val databaseName: String) {

    fun connectToDatabase(){
        // Implementation code removed for better clarity.
    }

    fun saveDataToDatabase() {
        try {
            // Implementation code removed for better clarity.
            // Perform some operation that may throw an exception.
        } catch (e: Exception) {
            val logger = FileLogger("logFile.txt")
            logger.log(e.message!!)
        }
    }

}

After:

We create a Logger interface and two classes implement it. This DatabaseManager class works with any subclass of Logger and depend on abstractions.

interface Logger {
   fun log(message: String)
}

class FileLogger(var fileName: String) : Logger {
   override fun log(message: String) {
       File(fileName).writeText(message)
   }
}

class DatabaseLogger(var tableName: String) : Logger {
   override fun log(message: String) {
       // Implementation code removed for better clarity
   }
}
class DatabaseManager(
    private val databaseName: String,
    private val logger: Logger
) {

    fun connectToDatabase(){
        // Implementation code removed for better clarity.
        /* In this method, the `logger` is also used because there might
         be an exception occurring during the database connection process.*/
    }

    fun saveDataToDatabase() {
        try {
            // Implementation code removed for better clarity.
            // Perform some operation that may throw an exception.
        } catch (e: Exception) {
            logger.log(e.message!!)
        }
    }

}

About

đź’Ž SOLID principles explained in Kotlin with examples

License:MIT License


Languages

Language:Kotlin 100.0%