Skip to content

SOLID Design Principles

In software development, solid design principles refer to a set of best practices for creating software that is reliable, maintainable, and adaptable. The acronym "SOLID" stands for five principles:

  1. S - Single Responsibility Principle
  2. O - Open / Closed Principle
  3. L - Liskov Substitution Principle
  4. I - Interface Segregation Principle
  5. D - Dependency Inversion Principle

Single Responsibility Principle (SRP)

SRP states that a module should have one, and only one reason to change. Each module should have a single responsibility or job to do, so that changes to that responsibility do not affect other parts of the system.

Violation

User class manages user data and sends email or downloads profile.

data class User(
    val id: String,
    val email: String
) {
    fun sendEmail() {
        // sends email from the user
    }

    fun downloadProfile() {
        // download profile of the user
    }
}

Solution

User class only manages user data, EmailSender class only sends email, and ProfileDownloader class only downloads profile.

data class User(
    val id: String,
    val email: String
)

class EmailSender {
    fun sendEmail(user: User) {
        // sends email from the user
    } 
}

class ProfileDownloader {
    fun downloadProfile(user: User) {
        // download profile of the user
    }
}

Open / Closed Principle (OCP)

OCP states that a module should be open for extension but closed for modification. You should be able to add a new feature to a module without changing its existing code.

Violation

We have logic for two vehicle bike and car.

enum class Vehicle {
    BIKE,
    CAR
}

class VehicleMover {

    fun moveVehicle(vehicle: Vehicle) {
        when (vehicle) {
            Vehicle.BIKE -> {
                // logic to ride bike
            }
            Vehicle.CAR -> {
                // logic to drive car
            }
        }
    }
}

To add airplane and boat, we will have to modify the Vehicle enum class and VehicleMover class.

enum class Vehicle {
    BIKE,
    CAR,
    AIRPLANCE,
    BOAT
}

class VehicleMover {

    fun moveVehicle(vehicle: Vehicle) {
        when (vehicle) {
            Vehicle.BIKE -> {
                // logic to ride bike
            }
            Vehicle.CAR -> {
                // logic to drive car
            }
            Vehicle.AIRPLANE -> {
                // logic to fly plane
            }
            Vehicle.BOAT -> {
                // logic to sail boat
            }
        }
    }
}

Solution

We can create Vehicle interface with move method and modify vehicle mover class as follows:

interface Vehicle {
    fun move()
}

class VehicleMover {

    fun moveVehicle(vehicle: Vehicle) {
        vehicle.move()
    }
}

The bike, car, airplane, boat classes will handle their own move logic, this way we will be able to add vehicles without modifying VehicleMover class.

class Bike : Vehicle {

    override fun move() {
        // logic to ride bike
    }
}

class Car : Vehicle {

    override fun move() {
        // logic to drive car
    }
}

class Airplane : Vehicle {

    override fun move() {
        // logic to fly plane
    }
}

class Boat : Vehicle {

    override fun move() {
        // logic to sail boat
    }
}

Liskov Substitution Principle (LSP)

LSP states that you should be able to use any subclass of a class in place of its parent class without causing problems or errors in the program.

Violation

AnimalWalker and AnimalSwimmer classes accept Animal, but AnimalWalker will throw error for Whale and AnimalSwimmer will throw error for Elephant.

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

class Elephant : Animal {

    override fun walk() {
        // logic to walk
    }

    override fun swim() {
        throw Exception("Elephant cannot swim")
    }
}

class Turtle : Animal {

    override fun walk() {
        // logic to walk
    }

    override fun swim() {
        // logic to swim
    }
}

class Whale : Animal {

    override fun walk() {
        throw Exception("Whale cannot walk")
    }

    override fun swim() {
        // logic to swim
    }
}

class AnimalWalker {

    fun walkAnimal(animal: Animal) {
        animal.walk()
    }
}

class AnimalSwimmer {

    fun swimAnimal(animal: Animal) {
        animal.swim()
    }
}

Solution

We created two interfaces WalkingAnimal and SwimmingAnimal. AnimalWalker accepts WalkingAnimal and AnimalSwimmer accepts SwimmingAnimal. This way they can be substituted without any errors.

interface WalkingAnimal {
    fun walk()
}

interface SwimmingAnimal {
    fun swim()
}

class Elephant : WalkingAnimal {

    override fun walk() {
        // logic to walk
    }
}

class Turtle : WalkingAnimal, SwimmingAnimal {

    override fun walk() {
        // logic to walk
    }

    override fun swim() {
        // logic to swim
    }
}

class Whale : SwimmingAnimal {

    override fun swim() {
        // logic to swim
    }
}

class AnimalWalker {

    fun walkAnimal(animal: WalkingAnimal) {
        animal.walk()
    }
}

class AnimalSwimmer {

    fun swimAnimal(animal: SwimmingAnimal) {
        animal.swim()
    }
}

Interface Segregation Principle (ISP)

ISP states that a module should not be forced to depend on interfaces it does not use. You should design interfaces that is specific to need of a module rather than having one interface satisfying needs of all modules.

Violation

Class ClickUiComponent had to override both onClick and onLongClick method where it just needed the onClick method.

interface OnClickListener {
    fun onClick()
    fun onLongClick()
}

class ClickUiComponent : OnClickListener {

    override fun onClick() {
        // handle click
    }

    override fun onLongClick() {
        // don't need to handle
    }
}

Solution

We segregated onClick and onLongClick methods two separate interfaces so that we only use the required interface with onClick method.

interface OnClickListener {
    fun onClick()
}

interface OnLongClickListener {
    fun onLongClick()
}

class ClickUiComponent : OnClickListener {

    override fun onClick() {
        // handle click
    }
}

Dependency Inversion Principle (DIP)

DIP states that high-level modules should not depend on low-level modules; both should depend on abstractions. You should design your software so that modules depend on abstract concepts, rather than concrete implementation details.

Violation

EmailSender class is dependent on Gmail.

class Gmail {
    fun sendEmail(message: String) {
        // logic to send email via gmail
    }
}

class EmailSender {

    fun sendEmail(message: String) {
        val gmail = Gmail()
        gmail.sendEmail(message)
    }
}

If we need to use Outlook we will have to change implementation of EmailSender.

class Outlook {
    fun sendEmail(message: String) {
        // logic to send email via outlook
    }
}

class EmailSender {

    fun sendEmail(message: String) {
        val outlook = Outlook()
        outlook.sendEmail(message)
    }
}

Solution

EmailSender is not dependent on Gmail or Outlook but on EmailProvider. It doesn't know which email provider is being used.

interface EmailProvider {

    fun sendEmail(message: String)
}

class Gmail : EmailProvider {

    override fun sendEmail(message: String) {
        // logic to send email using gmail
    }
}

class Outlook : EmailProvider {

    override fun sendEmail(message: String) {
        // logic to send email using outlook
    }
}

class EmailSender(private val emailProvider: EmailProvider) {

    fun sendEmail(message: String) {
        emailProvider.sendEmail(message)
    }
}

Last update: August 13, 2023