Skip to content Skip to footer

Mastering SOLID Principles: 5 Points Guide to Writing High-Quality Maintainable Code

In the world of software development, writing code that stands the test of time, while remaining flexible and easily understandable, is a challenge that every programmer faces. Over the years, experts have devised numerous methodologies and principles to guide developers in creating high-quality, maintainable code. One such set of guiding principles that have proven extremely useful, particularly in object-oriented programming, is known as SOLID principles. Robert C. Martin first conceptualised these principles in the early 2000s, and they have since become a cornerstone of modern software design.

What are SOLID Principles?

These principles help make code easier to maintain and understand. SOLID is a handy acronym used in object-oriented programming that stands for –

  1. Single Responsibility Principle 
  2. Open-Closed Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle.

Let’s break down each principle for better comprehension.

Note: Examples have basic Kotlin code snippets.

Single Responsibility Principle

It is the first of the 5 SOLID Principles. It states that one module or a class should have only one reason to change.

Example:

class Media {
    fun playAudio()
    fun playVideo()
    fun showImage()
    fun calculateAudioTimeLength()
    ...
    ...
}
Code language: Kotlin (kotlin)

In the above example, different media types are handled in one Media class. Here Media class has multiple responsibilities and modification in any specific media type can affect other media’s functionality. This is bad practice. To resolve this issue, we can assign its own responsibility to each media type as Audio, Image or Video class. So changing the logic for any of the media classes will not impact other media types.

class Audio{
    fun playAudio()
    fun calculateAudioTimeLength()
}

class Video{
    fun playVideo()
}

class Image{
    fun showImage()
}
Code language: Kotlin (kotlin)

Open-Closed Principle

Moving on to the second of the 5 SOLID Principles. This principle states that one module or a class should be open for extension and closed for modification.

Example:

class Vehicle {
    val priceFactor = 7
    val price

    fun calculatePrice() {
        if (vehicleType is Bike) price = priceFactor * 2.5
        else if (vehicleType is Car) price = priceFactor * 4
    }
}

Code language: Kotlin (kotlin)

Here if we want to calculate the price for a truck, we have to add another if else statement. So this code is violating the “Open-Closed Principle”. The Vehicle class should open for extension and there should not be modifications related to a particular vehicle type. So we can make a Vehicle class such that it can have only common vehicle properties and can be inherited for specific vehicle types like Bike, Car or Truck classes.

class Vehicle {
    val priceFactor = 7
    val price

    fun calculatePrice() {}

    fun getPrice(): Int {
        return price
    }
}

class Bike : Vehicle {
    override fun calculatePrice() {
        price = priceFactor * 2.5
    }
}

class Car : Vehicle {
    override fun calculatePrice() {
        price = priceFactor * 4
    }
}

class Truck : Vehicle {
    override fun calculatePrice() {
        price = priceFactor * 10
    }
}

Code language: Kotlin (kotlin)

Liskov Substitution Principle

The third principle of the 5 SOLID principles was introduced by Barbara Liskov in a 1987 conference keynote. You can read the original paper here. It says that objects of a superclass must be substitutable for their subclass objects without breaking the system.

Example:

class Employee {
    public fun calculateSalary(): Int {
        return 50000
    }

    public fun calculateBonus(): Int {
        return 5000
    }
}

class PermanentEmployee : Employee {
    override fun calculateSalary() {
        return 70000
    }

    override fun calculateBonus(): Int {
        return 7000
    }
}

class ContractualEmployee : Employee {
    override fun calculateSalary() {
        return 70000
    }

    override fun calculateBonus(): Int {
        throw Exception()
    }
}
Code language: Kotlin (kotlin)

Here PermanentEmployee and ContractualEmployee classes are inheriting the methods calculateSalary and calculateBonus from Employee class. As both types of employees will fetch salary, So calculateSalary method fits well in both types of employees but the bonus is not applicable to contractual employees. While calculating bonuses for contractual employees, the system will break. So the ContractualEmployee class object can’t replace its base class object. 

class Employee {
    public fun calculateSalary(): Int {
        return 50000
    }
}

class PermanentEmployee : Employee {
    override fun calculateSalary() {
        return 70000
    }

    fun calculateBonus(): Int {
        return 7000
    }
}

class ContractualEmployee : Employee {
    override fun calculateSalary() {
        return 70000
    }
}
Code language: Kotlin (kotlin)

Interface Segregation Principle

The Interface Segregation Principle (ISP) is one of the SOLID principles of object-oriented design. This principle says that any class should not be forced to implement an interface method when the method is not used in the class.

Example:

interface Animal {
    fun feed()

    fun groom()
}

class Dog : Animal {
    override fun feed() {}

    override fun groom() {}
}

class Tiger : Animal {
    override fun feed() {}

    override fun groom() {}
}
Code language: Kotlin (kotlin)

In the above example, both Dog class and Tiger class are implementing Animal interface that has feed() and groom() methods. As we can feed and groom a dog, these functions are relevant to the Dog class. But we can’t groom a Tiger so Tiger class is forcefully overriding the groom function. We can solve this by having separate interfaces for feed and groom functions as a pet can be groomed so we can have a Pet interface.

interface Animal {
    fun feed()
}

interface Pet {
    fun groom()
}

class Dog : Animal, Pet {
    override fun feed() {}

    override fun groom() {}
}

class Tiger : Animal {
    override fun feed() {}
}
Code language: Kotlin (kotlin)

Dependency Inversion Principle

The Dependency Inversion Principle (DIP) is the last in the list of SOLID principles. This principle helps to create loosely coupled systems that are easier to maintain and extend.

It has two rules:

  •  Higher-level modules should not depend on lower-level modules instead both modules should depend on the abstractions.
  • Abstractions should not depend on details. Details should depend upon abstractions.

Example:

class StudentReport(database: MySQLDatabase) {
    fun open() {
        database.get()
    }

    fun save() {
        database.insert()
    }
}

class MySQLDatabase {
    fun get() {
        // get by id
    }

    fun insert() {
        // inserts into db
    }

    fun update() {
        // update some values in db
    }

    fun delete() {
        // delete some records in db
    }
}

// Client
mysql = MySQLDatabase()

report_mysql = StudentReport(mysql)

report_mysql.open()
Code language: Kotlin (kotlin)

In the above example, StudentReport class that is a high-level module is dependent upon the low-level module MySQLDatabase. In future, if we want to change the database system to Mongodb then We need to change it in StudentReport class also. That is not good practice because the StudentReport class is tightly coupled with a specific database class. So we can make a database interface that will be implemented by different types of database classes and the StudentReport class will depend on that abstraction.

interface DatabaseInterface {
    fun get()
    fun insert()
    fun update()
    fun delete()
}

class MySQLDatabase : DatabaseInterface {
    override fun get(){
        // get by id
    }

    override function insert(){
        // inserts into db
    }

    override fun update(){
        // update some values in db
    }

    override fun delete(){
        // delete some records in db
    }
}

class MongoDB : DatabaseInterface {
    override fun get(){
        // get by id
    }

    override function insert(){
        // inserts into db
    }

    override fun update(){
        // update some values in db
    }

    override fun delete(){
        // delete some records in db
    }
}

class StudentReport(database: DatabaseInterface) {
    fun open(){
        database.get()
    }

    fun save(){
        database.insert()
    }
}

// Client
mysql = MySQLDatabase()
report_mysql = StudentReport(mysql)

report_mysql.open()

mongo = MongoDB()
report_mongo = StudentReport(mongo)

report_mongo.open()
Code language: Kotlin (kotlin)

SOLID Principles explained through videos

@kudvenkat explains SOLID Principles in a lot more detail in this playlist.

Conclusion

In this blog, we have started with why SOLID principles are important for better software development. Then we have understood each principle with very simple and real-world examples. Even though the SOLID principles are explained with Kotlin code examples, these principles apply to other coding languages too.

I want to thank you for taking the time to read the whole blog and I hope that the above concepts are clear.

I suggest keeping these principles in mind while designing, writing, and refactoring your code so that your code will be much more clean, maintainable, and testable.

Lastly, you should check out more of our coding practices posts like 2 git branches workflow for convenient mobile app development.

Author

2 Comments

Leave a comment