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! 😃