Jun.12

SOLID in OOP

SOLID is an acronym for 5 design principles in OOP (Object-Oriented Programming).

These principles are a subset of many principles introduced by Robert C. Martin aka Uncle Bob in Design Principles and Design Patterns. SOLID acronym was later introduced by Michael Feathers. 🙂

Objective of these principles is to make software design and development more understandable, maintainable and extendable.

Codes or examples are written in Kotlin language. So let’s start. 😃

S: Single Responsibility Principle
Every entities (classes, modules, functions, etc.) should only have a single responsibility.

Wrong

class Zoo(val database: Database) {
   fun addAnimal(animal: Animal) {
      try {
         database.addAnimal(animal)
      } catch (exception: Exception) {
         Log.e("Error", exception.message)
      }
   }
}

Right

class Zoo(val database: Database) {
   val logger: Logger = Logger()
   fun addAnimal(animal: Animal) {
      try {
         database.addAnimal(animal)
      } catch (exception: Exception) {
         logger.log(exception)
      }
   }
}

Explanation: Zoo should not handle exceptions. So we are passing Exception to Logger for handling and allowing those to have single responsibility.

O: Open-Closed Principle
Every entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

Wrong

class Zoo(val database: Database) {
   val logger: Logger = Logger()
   fun addAnimal(animal: Animal) {
      try {
         if (animal.type = "Giraffe") {
            database.addGiraffe(animal)
         } else {
            database.addAnimal(animal)
         }
      } catch (exception: Exception) {
         logger.log(exception)
      }
   }
}

Right

open class Zoo(open val database: Database) {
   val logger: Logger = Logger()
   fun addAnimal(animal: Animal) {
      try {
         database.addAnimal(animal)
      } catch (exception: Exception) {
         logger.log(exception)
      }
   }
}

class GiraffeZoo(override val database: Database) : Zoo(database) {
   val logger: Logger = Logger()
   override fun addAnimal(animal: Animal) {
      try {
         database.addGiraffe(animal)
      } catch (exception: Exception) {
         logger.log(exception)
      }
   }
}

Explanation: By using inheritance e.g. override, we are adding extended behavior to Zoo without modifying existing method. Code is now clean and readable.

L: Liskov Substitution Principle
Entities should be replaceable with instances of their subtypes without altering correctness. Subtype should represents usage of base type.

Wrong

open class Zoo(open val database: Database) {
   val logger: Logger = Logger()
   fun addAnimal(animal: Animal) {
      try {
         database.addAnimal(animal)
      } catch (exception: Exception) {
         logger.log(exception)
      }
   }
}

class PandaZoo(override val database: Database, val notifier: Notifier) : Zoo(database) {
   override fun addAnimal(animal: Animal) {
      notifier.notify(animal)
   }
}

Right

class PandaZoo(override val database: Database, val notifier: Notifier) : Zoo(database) {
   override fun addAnimal(animal: Animal) {
      super.addAnimal(animal)
      notifier.notify(animal)
   }
}

Explanation: By not invoking super or adding to database, we are altering functionality which doesn’t align with base class. PandaZoo is a subtype of Zoo, so it should extend without costing existing behavior.

I: Interface Segregation Principle
Many entity specific interfaces are better than one general purpose interface. No entity should be forced to depend on methods it does not use.

Wrong

interface IAnimal {
   fun awake()
   fun sleep()
   fun run()
   fun fly()
}

Right

interface IAnimal {
   fun awake()
   fun sleep()
}

interface IGirrafe : IAnimal {
   fun run()
}

interface IEagle : IAnimal {
   fun fly()
}

Explanation: Giraffes don’t fly and Eagles don’t run. Now we have entity specific interfaces and entities are not forced to depend on useless methods.

D: Dependency Inversion Principle
Dependency inversion principle is a specific way of decoupling entities. Higher entities should not depend on lower entities. Both entities should depend on abstractions.

Wrong

class Zoo(val database: Database) {
   val logger: Logger = Logger()
   fun addAnimal(animal: Animal) {
      try {
         database.addAnimal(animal)
      } catch (exception: Exception) {
         logger.log(exception)
      }
   }
}

Right

class Zoo(val database: IDatabase, val logger: ILogger) {
   fun addAnimal(animal: Animal) {
      try {
         database.addAnimal(animal)
      } catch (exception: Exception) {
         logger.log(exception)
      }
   }
}

Explanation: Zoo should not create and depend on Logger directly. Plus we might want to use a subtype of Logger. So we are injecting dependencies (via interfaces) from outside instead of creating inside.

May be we are using SOLID in OOP all along more or less. 🤔 May be we didn’t know how to call it. But now we know! 😃

Architecture,Principle