This repository has been created to teach SOLID principles.

Overview

SOLID Principles

Single Responsibility

Each class should have only one purpose and not be filled with excessive functionality.

Let's look at this AreaCalculator class below:

class AreaCalculator {

    fun sum(vararg shapes: Any): Double {
        var sum = 0.00
        shapes.forEach { shape ->
            when (shape) {
                is Square -> {
                    sum += shape.length.pow(2)
                }

                is Circle -> {
                    sum += PI * shape.radius.pow(2)
                }
            }
        }
        return sum
    }

    fun getSumAsJson(sum: String): String {
        return """
                {
                    sum: $sum
                }
            """.trimIndent()
    }

}

According to the name of this class, It's responsible for calculating the area of shapes passed to the sum function.

Defining the print function at the bottom of the class assigns another responsibility to this class which is conversion functionality. So it violates Single Responsibility principle.

To fix this violation, we need to define another class and call it Printer, which is responsible for printing values, and remove the getSumAsJson function from AreaCalculator class.

Solution :
class AreaCalculator {

    fun sum(vararg shapes: Any): Double {
        var sum = 0.00
        shapes.forEach { shape ->
            when (shape) {
                is Square -> {
                    sum += shape.length.pow(2)
                }

                is Circle -> {
                    sum += PI * shape.radius.pow(2)
                }
            }
        }
        return sum
    }

}
class Printer {

    fun getSumAsJson(sum: String): String {
        return """
                {
                    sum: $sum
                }
            """.trimIndent()
    }
}

Open Closed

Imagine we want to add another class called Rectangle as a new shape to the project. We need to consider its area calculation within our AreaCalculation class like below:

data class Rectangle(
    val width: Double,
    val height: Double
)
class AreaCalculator {
    fun sum(vararg shapes: Any): Double {
        var sum = 0.00
        shapes.forEach { shape ->
            when (shape) {
                is Square -> {
                    sum += shape.length.pow(2)
                }

                is Circle -> {
                    sum += PI * shape.radius.pow(2)
                }

                is Rectangle -> { //---> Here we added the Rectangle
                    sum += shape.width * shape.height
                }
            }
        }
        return sum
    }
}

As you can see, due to the addition of Rectangle, we're modifying the body of the sum function, which is a bad practice. You must not change the implementation of a function within a class in case of adding a new feature to the project. Instead, it would be best if you defined an interface named Shape, and every new shape added to the project must implement it like below :

Solution :
  1. Define a new interface for shapes
interface Shape {
    fun getArea(): Double
}
  1. Shapes implement this interface
data class Square(
    private val length: Double
) : Shape {
    override fun getArea(): Double = length.pow(2)
}
data class Rectangle(
    private val width: Double,
    private val height: Double
) : Shape {
    override fun getArea(): Double = width * height
}
data class Circle(
    private val radius: Double
) : Shape {
    override fun getArea(): Double = PI * radius.pow(2)
}
  1. Look at the beauty of using these shapes inside the AreaCalculator class
class AreaCalculator {

    // 1. Changed the argument from Any to Shape
    fun sum(vararg shapes: Shape): Double {
        var sum = 0.00

        shapes.forEach { shape ->
            // 2.Getting area of the shape
            sum += shape.getArea()
        }

        return sum
    }
}

So without modifying the body of the sum function within AreaCalculation class, you can pass any shape to this function for calculation.

This principle tells you :

Classes should be open for extension and closed for modification. In other words, you should not have to rewrite an existing class to implement new features.

Liskov Substitution

Let's create a new shape called NoShape for the project. WHAT?!?

What kind of shape is that? You're right.

There's no shape around the world that people call it NoShape. But to understand this principle, I'm going to define it.

Let's do it together :

class NoShape : Shape {
    override fun getArea(): Double {
        throw IllegalStateException("Undefined shape has no area")
    }
}

Since the NoShape is the child class of Shape, whenever you create a new instance of this class with the type Shape and pass it to the sum function of AreaCalculator, you'll face an exception.

val areaCalculator = AreaCalculator()
val noShape: Shape = NoShape()
val sum = areaCalculator.sum(noShape) // It will throw an exception

So based on this example, the child class is not substitutable for its parent "Shape".

This principle tells you :

Every child or derived class should be substitutable(replaceable) for their base or parent class.

Interface Segregation

Okay, let's move forward and learn another beautiful principle: Interface Segregation. Now we want to create another shape within our project, a Cube class. As you know, cubes have another function which we call volume. Let's add this volume function to the Shape interface and implement it within our shape classes, like below :

  1. Add the getVolume function to our Shape interface :
interface Shape {
    fun getArea(): Double

    // Calculates and returns volume of 3D shapes
    fun getVolume(): Double
}
  1. Implement this interface within our Cube class :
class Cube(
    private val edge: Double
) : Shape {

    override fun getArea(): Double = 6 * edge.pow(2)

    override fun getVolume(): Double = edge.pow(3)

}
  1. So far, so good; now, let's also look at the Circle class because it also needs to implement the getVolume function.
data class Circle(
    private val radius: Double
) : Shape {

    override fun getArea(): Double = PI * radius.pow(2)

    // Circle is a 2D shape, then we need to return 0
    override fun getVolume(): Double {
        return 0.toDouble()
    }
}

As you know, a circle is a two-dimensional shape. So defining volume for it, is pointless. And being forced to have the getVolume function made it so ugly. Sometimes you may have to implement meaningless functions in your classes because you have used an interface. So it feels like you're doing something wrong! right?

Here, the Interface Segregation Principle comes into the picture! This principle tells you : Interfaces should not force classes to implement what they can’t do. Large interfaces should be divided into small ones. So to fix this violation :

  1. Create ThreeDimensionalShape interface and add the getVolume to it :
interface ThreeDimensionalShape {

    // Calculates and returns volume of 3D shapes
    fun getVolume(): Double
}
  1. Remove the getVolume function from the Shape interface :
interface Shape {
    fun getArea(): Double
}
  1. Implement the ThreeDimensionalShape inside the Cube class :
class Cube(private val edge: Double) : Shape, ThreeDimensionalShape {

    override fun getArea(): Double = 6 * edge.pow(2)

    override fun getVolume(): Double = edge.pow(3)

}

Dependency Inversion

We want to change the getSumAsJson function within the Printer class, which receives the sum as a String in its arguments. We want to pass the shapes to this function instead of a String, and inside the implementation of this function, use the AreaCalculator class to calculate the sum of the area of shapes, then convert it to JSON and return it. So let's do it :

  1. Change the Printer class :
// 1. Pass the AreaCalculator instance within the constructor
class Printer(private val areaCalculator: AreaCalculator) {

    // 2. Pass shapes instead of sum
    fun getSumAsJson(vararg shapes: Shape): String {

        // 3. Calculate sum of the shape by AreaCalculator
        val result = areaCalculator.sum(*shapes)

        return """
                {
                    sum: $result
                }
            """.trimIndent()
    }
}
  1. Use it like below :
fun main(args: Array<String>) {
    val areaCalculator = AreaCalculator()

    // Passing the areaCalculator to the constructor
    // of Printer class for calculations
    val printer = Printer(areaCalculator)

    val rectangle = Rectangle(
        width = 10.toDouble(),
        height = 20.toDouble()
    )
    val square = Square(length = 10.toDouble())
    val circle = Circle(radius = 12.toDouble())

    val sum = printer.getSumAsJson(square, circle, rectangle)

    println(sum)
}

As you can see, the constructor of the Printer class accepts an instance from the AreaCalculator class. Imagine that you may have created countless instances from the Printer class in a massive project. Suppose one day you decide to create a new class called NewAreaCalculator and you want to use it within the Printer class. In that case, you will have to make numerous changes to your project to achieve this goal (because you need to pass an instance of this new class to the constructor of the Printer class everywhere). Sounds terrible?

Okay, here the Dependency Inversion comes into the picture. This principle tells you : Components should depend on abstractions, not on concretions. So let's fix this violation:

  1. We need to define an interface called AreaCalculator :
interface AreaCalculator {
    fun sum(vararg shapes: Shape): Double
}
  1. Implement the above interface within AreaCalculatorImpl :
class AreaCalculatorImpl : AreaCalculator {

    override fun sum(vararg shapes: Shape): Double {
        var sum = 0.00
        shapes.forEach { shape ->
            sum += shape.getArea()
        }
        return sum
    }
}
  1. Within the Printer class, use the AreaCalculator interface instead of AreaCalculatorImpl (actual implementation) :
// Using AreaCalculator interface instead of the real implementation
class Printer(private val areaCalculator: AreaCalculator) {

    fun getSumAsJson(vararg shapes: Shape): String {

        val result = areaCalculator.sum(*shapes)

        return """
                {
                    sum: $result
                }
            """.trimIndent()
    }
}

Whenever you need to define a new class or implementation for AreaCalculator, you need to implement this interface and use it wherever you want without any trouble.

Note : What happens to your code by this principle is that it reduces the dependencies between classes and makes them decoupled. It also increases the testability of your classes.

Conclusion

Stick to these principles because they help you to implement testable, maintainable, and reusable code.

You might also like...
App that says if the movie you typed in has bazooka or not.

TemBazuca App that says if the movie you typed in has bazooka or not. But... why? The idea for the app came up while I was watching Choque de Cultura

A sussy 1.17.1 Airplane fork that (hopefully) has better performance and relatively stable
A sussy 1.17.1 Airplane fork that (hopefully) has better performance and relatively stable

Fiadelity A sussy Minecraft server software This project is experimental, its usage in production environment is discouraged if you are not ready to f

A sample demo app which has Clean Architecture with MVVM , UI built with Jetpack Compose
A sample demo app which has Clean Architecture with MVVM , UI built with Jetpack Compose

A sample demo app (two screen todo list app) which has Clean Architecture with MVVM , UI built with Jetpack Compose and includes Modern Android Development Best Practices with components

An Android app that gives you a password generated by a given phrase with a custom algorithm, it also has password and biometric security.

An Android app that gives you a password generated by a given phrase with a custom algorithm, it also has password and biometric security.

Simple timer app inspired by Newton's Cradle. Created in Jetpack Compose for #AndroidDevChallenge.
Simple timer app inspired by Newton's Cradle. Created in Jetpack Compose for #AndroidDevChallenge.

Newton's Timer 📜 Description Simple timer app inspired by Newton's Cradle. Created in Jetpack Compose for #AndroidDevChallenge. 💡 Motivation and Con

A gallery application created by photographs taken by NASA's rover vehicle
A gallery application created by photographs taken by NASA's rover vehicle

Nasa-Gallery An Android application that uses the a Nasa API to display photos taken by the Mars Rover is built with MVVM pattern as well as Architect

ZeAppp v3, created by Android enthusiasts joining the Droidcon 2021 in Berlin, coming to the GDG Booth and writing code, 15 minutes at a time

ZeThree App build at the GDG Community booth at Droidcon Berlin 2021. Come join the fun™. ZeWhat? Based on the previous success of the ZeAppp-app, thi

An app created for Code Lousiville in order to learn the in-and-outs of basic Android development

Tea House is an app created for Code Lousiville in order to learn the in-and-outs of basic Android development.

A pet project created to practise Android Development skills in Kotlin after finishing multiple courses online.

A pet project created to practise Android Development skills in Kotlin after finishing multiple courses online. The app displays a list of hundreds of characters provided by The Rick and Morty API https://rickandmortyapi.com/. In other screens user can access detailed information about a particular character, such as status, location and episodes. Libraries used in a project: - Kotlin - Jetpack libraries (Navigation, Room, Hilt, Palette) - other: Glide, Retrofit

Owner
Ali Soleimani
Android Developer
Ali Soleimani
An advanced Kotlin (Android Native) application that uses SOLID architectural principles, consumes a RESTFUL Service, downloads & images using best practices

Dog-Playground Kotlin An advanced Kotlin (Android Native) application that uses SOLID architectural principles, consumes a RESTFUL Service, downloads

Amose Suwali 1 Jan 10, 2022
The News App has been carried out within the framework of the MVVM architecture, information about news is obtained by consulting an API, it is built usisng Jetpack Copose, Coroutines, Dependency Injection with Hilt and Retrofit

Journalist The News App consists of an application that displays the latest news from EEUU from an API that provides official and updated information.

null 0 Nov 3, 2021
SlushFlicks has been built upon public APIs from IMDB.

SlushFlicks SlushFlicks has been built upon public APIs from IMDB. This application helps users to view trending, popular, upcoming and top-rated movi

Sifat Oshan 14 Jul 28, 2022
This assignment has been given to me for Android developer position at SonyLiv.

asssignmentSonyLiv This assignment has been given to me for Android developer position at SonyLiv. This codeis not full functional but can give a cont

Rudra Chouhan 0 Nov 21, 2021
This is an open source launcher project for Android devices that has been built completely from scratch

Description This is an open source launcher project for Android devices that has been built completely from scratch. The main goal of this launcher is

OpenLauncher Team 1.3k Dec 21, 2022
Android multimodule project based on Kotlin, MVVM, SOLID, flow, coroutines and paging3.

TVMaze David Ferrándiz Features Retrieve TVMaze shows in a grid. See more information about the show in a new screen Tech Kotlin Retrofit Modularizati

David Ferrandiz 1 Nov 18, 2022
Here OneSignal push and in-app-message have been implemented

OneSIgnal demo project Here we have implemented OneSignal inAppMessage and Push Notification ANDROID SDK SETUP WITH ONESIGNAL: https://documentation.o

Fakhrul Alam Siddiqei 2 Dec 7, 2022
A simple and minimal app to track how long you've been sober from anything you want.

Sobriety A simple and minimal app to track how long you've been sober from anything you want. I mostly designed this out of spite for "I Am Sober", wh

Katherine Rose 26 Nov 21, 2022
The app has got fullscreen Turkey map via Huawei Map. App selects random province and shows it borders on the map than user will try to guess the provinces name.

Il Bil App Introduction I will introduce you to how to implement Account Kit, Map Kit, Game Service. About the game: The app has got fullscreen Turkey

Gökhan YILMAZ 4 Aug 2, 2022
Awesome Kid's Drawing App. It has a click and draws feature.

CanvaKids Overview It's a kid's drawing app which is build for android users. It is built in Kotlin with some very good practices involved.

null 9 Oct 30, 2021