Sitemap

SOLID

4 min readJul 11, 2025

--

Press enter or click to view image in full size
SOLID principles illustration

The SOLID principles are five design principles intended to make software designs more understandable, flexible, and maintainable. Coined by Robert C. Martin (also known as Uncle Bob), these principles are a cornerstone of object-oriented programming.

Single Responsibility Principle
Each entity in a software, be it a module or a class or a function, should just have a single job or responsibility. A function or class ideally should have just one responsibility. If it does something unrelated to the main responsibility, it is an indication that it should move to an independent unit. This helps in making each unit testable and thereby increasing the code coverage to the maximum possible value.

An example of not following SRP. The functions does follow SRP though!

class Logger{
fun log(text: String){
Log.i("Tag", text)
}

// This violates SRP for this class as it does a job unrelated to Logger
fun saveToFile(text: String) {
// code to save text to a file
}
}

This can be fixed to follow SRP. Each class does its responsibility.

class Logger{
fun log(text: String){
Log.i("tag", text)
}
}

class SaveToFile () {
fun save(text: String){
// code to save text to a file
}
}

Open/Closed Principle
Any entity should be open for extension and closed for modifications.

A class that follows OCP should be open for extension. We should be able to make sub classes by extending parent class. We can add new functionalities.

But at the same time, we shouldn’t be modifying all related classes which will result in refactor through out the code base.

Here is an implementation that violates OCP

open class Shape
class Circle(val radius: FLoat): Shape()
class Square(val side: FLoat): Shape()

class AreaCalculator(){

fun calculateArea(shape: Shape): Double{
return when(shape){
is Circle -> Math.PI * shape.radius * shape.radius
is Square -> shape.side * shape.side
// Every time you add a new shape, end up modifying here as well
}
}

}

But if we modify the shape classes to implement Area calculation, we can add a new shape without affecting or change in any other code. So this follows OCP

interface Shape {
fun area(): Double
}

class Circle(radius: FLoat): Shape{
override fun area(): Double = Math.PI * shape.radius * shape.radius
}


class AreaCalculator(){
fun calculateArea(shape: Shape): Double{
return shape.area()
}
}

// When adding a new shape, just implement its area calculation.
// Other classes remain untouched!
class Recatngle(width: Float, height: Float): Shape {
override fun area(): Double = width * height
}

Liskov Substitution Principle

LSP essentially says, if we have a parent class and its child classes. We should be able to substitute the object of parent class with objects of child classes without any modifications.

Below is an example where LSP is violated.

interface Bird {
fun fly()
}

class Eagle: Bird(){
override fun fly(){
println("Eagle is flying")
}
}

class Duck: Bird(){
override fun fly(){
println("Duck is flying")
}
}

class Penguin: Bird(){
override fun fly(){
throws Exception("Penquin cannot fly")
}
}

class FlyingEvent{
fun startFlying(bird: Bird){
bird.fly()
}
}

class Main(){
val event = FlyingEvent()
event.fly(Eagle())
event.fly(Duck())
event.fly(Penguin()) // This will throw an exception

}

This will compile successfully but fail at run time. These violations can be caught during code review or when running unit tests.

Get Reluctant Starter’s stories in your inbox

Join Medium for free to get updates from this writer.

Below is the code that does not violate LSP

open class Bird

open class FlyingBird: Bird() {
open fun fly()
}

class Eagle: FlyingBird(){
override fun fly(){
println("Eagle is flying")
}
}

class Duck: FlyingBird(){
override fun fly(){
println("Duck is flying")
}
}

class Penguin: Bird(){
// No fly method to override
}

class FlyingEvent{
fun startFlying(bird: FlyingBird){
bird.fly()
}
}

class Main(){
val event = FlyingEvent()
event.fly(Eagle())
event.fly(Duck())
event.fly(Penguin()) //Build error. This is not allowed at compile time

}

Interface Segregation Principle

ISP says, when an interface is defined, it should not force its implementations to define methods that are unrelated or not required for its existence.

If such a case arises, it means, we have to separate out or segregate them into a separate interface.

Example where ISP is violated

interaface Worker{
fun work()
fun eat()
}

class Human: Worker{
override fun work() {
// Human is working
}
override fun eat() {
// Human is eating
}
}

class Bot: Worker{
override fun work() {
// Bot is working
}
override fun eat() {
// Not required
// this is forcing to override a functionality
}
}

This is fixed by segregation

interaface Workable{
fun work()
}
interface Eatable
fun eat()
}

class Human: Workable, Eatable{
override fun work() {
// Human is working
}
override fun eat() {
// Human is eating
}
}

class Bot: Workable{
override fun work() {
// Bot is working
}
}

Dependency Inversion Principle

DIP states that higher level modules and lower level modules should depend on abstraction. They should not depend on concrete implementations

This helps classes to be loosely coupled. Easily achievable through Dependency Injection frameworks like Dagger, Hilt

Advantages are testable, maintainable code base. Can swap in dependencies without much changes.

Example where DIP is not followed:

We cannot test the function. Also, if we want to change the analytics type, we have to change the private dependency

class MainAnalyticsLayer{
fun track(event: String){
// Track api call
}
}

class MainViewModel: ViewModel {
val analyticsLayer = MainAnalyticsLayer() //Dependency

fun trackEvent(event: String){
analyticsLayer.track(event)
}
}

class MainViewModelTest{

@Test
fun testTrackEvent(){
val vmToTest = MainViewModel()
vmToTest.trackEvent("Test") // Cannot test as the dependency cannot be mocked
}
}

How to fix this with DIP

interface AnalyticsLayer{
fun track(event: String)
}

class MainAnalyticsLayer: AnalyticsLayer{
override fun track(event: String){
// Track api call
}
}

//Using manual dependency injection
class MainViewModel (val analyticsLayer:AnalyticsLayer) : ViewModel {

fun trackEvent(event: String){
analyticsLayer.track(event)
}
}

//OR Using HILT in Android

@Module
@InstallIn(SingletonComponent::class)
class AnalyticsProvider{

@Provides
fun provideAnalytics(): AnalyticsLayer{
return MainAnalyticsLayer()
}
}

class MainViewModel @Inject constructor(val analyticsLayer :AnalyticsLayer) : ViewModel {

fun trackEvent(event: String){
analyticsLayer.track(event)
}
}


class MainViewModelTest{

@Test
fun testTrackEvent(){
val mockAnalyticsLayer = Mockk.mock(AnalyticsLayer::class.java)
val vmToTest = MainViewModel(mockAnalyticsLayer)
vmToTest.trackEvent("Test")

verify(mockAnalyticsLayer).track("Test")
}
}

References: Github Copilot and Google Gemini

--

--