S.O.L.I.D Principles (Examples in Kotlin)

S.O.L.I.D Principles (Examples in Kotlin)

Lots of developers do the code, but A good developer is one who takes care of the code with respect to many programming paradigms, such as clean code, reusability, modularization, scalability, maintainability, and many more.

The first and foremost concept is to understand solid principles in order to achieve the above paradigms.

So in this blog, we will cover SOLID principles so let's start

No alt text provided for this image

1. Single Responsibility Principle:

A Class should have only one reason to change.

In other words, A class has to have only one responsibility.

If our class is doing more than one responsibility, Then we should split our class in such a manner, That one class has only one responsibility and others have other responsibilities, Else we are violating SRP.

See the bad example of SRP:

data class Employee(
    var empId: String,
    var empName: String,
    var empPhoneNumber: String,

    var empCtc: String,
    var empTakeHome: String,
    var empTax: String,

    var addressLine: String,
    var landmark: String,
    var pinCode: String,
    var city: String,
    var state: String
) {

    fun getEmpBasicInformation() {
        println("Emp Id: $empId")
        println("Emp name: $empName")
        println("Emp name: $empName")
        println("Emp PhoneNumber: $empPhoneNumber")
    }

    fun getEmpSalaryInformation() {
        println("Emp ctc: $empCtc")
        println("Emp take home: $empTakeHome")
        println("Emp tax: $empTax")
    }

    fun getEmpAddressInformation() {
        println("Emp address: $addressLine,$landmark,$city,$pinCode,$state")
    }

}        

Here Employee class is managing more than one responsibility.

1 Employee info, 2 Ctc Info, 3 Address info.

So updating any of this will take more time as well as in more places, We need to maintain the code, and if we want to reuse one of the subparts so it is not reusable.

See the SRP version of the above code snippet:

data class EmployeeDetail(
    val employee: Employee,
    val employeeCTC: EmployeeCTC,
    val employeeAddress: EmployeeAddress
) {
    fun getEmployee() {
        employee.getEmpBasicInformation()
    }

    fun getEmployeeCTC() {
        employeeCTC.getEmpCTCInformation()
    }

    fun getEmployeeAddress() {
        employeeAddress.getEmpAddressInformation()
    }
}

data class Employee(
    var empId: String,
    var empName: String,
    var empPhoneNumber: String
) {
    fun getEmpBasicInformation(): String {
        return "Employee(empId='$empId'," +
                " empName='$empName', " +
                "empPhoneNumber='$empPhoneNumber')"
    }
}

data class EmployeeCTC(
    var empCtc: String,
    var empTakeHome: String,
    var empTax: String
) {
    fun getEmpCTCInformation(): String {
        return "EmployeeCTC(empCtc='$empCtc'," +
                " empTakeHome='$empTakeHome', " +
                "empTax='$empTax')"
    }
}

data class EmployeeAddress(
    var addressLine: String,
    var landmark: String,
    var pinCode: String,
    var city: String,
    var state: String
) {
    fun getEmpAddressInformation(): String {
        return "EmployeeAddress(addressLine='$addressLine'," +
                " landmark='$landmark'," +
                " pinCode='$pinCode', " +
                "city='$city', state='$state')"
    }
}        

Above we have separated each class as a responsibility, So the code is cleaner and readable, and maintainable, If we want to use any of the classes independently in many places we can do it easily, and If we want to update any class so it is much easier now.

2. Open-Closed Principle:

Software entities such as classes, functions, and modules should be open for extension but closed for modification.

If we have already written the class, So it should be open for extension but closed for modification, Hence we should design the class in such a way that follows this principle(for a new feature we should not change the current class rather we should create a new class by extending the current class)

Let's see the bad example:

class MathematicalOperation {

    fun doCalculation(operationName: String, firstNumber: Int, secondNumber: Int): Int {
        return when (operationName) {
            "Addition" -> {
                //do addition operation here
                firstNumber + secondNumber
            }
            "Subtraction" -> {
                // do subtraction operation
                firstNumber - secondNumber
            }
            else -> {
                throw OperationNotSupportedException()
            }
        }
    }

}        

In the above example, We have a method that takes operation names and does the operation if we want to increase the operations so in that case, We need to touch the code of the above class and need to modify the doCalculation() method. Hence we are violating the Open close Principle.

Let's fix the above issue:

abstract class MathematicalOperation {
    abstract fun doCalculation(firstNumber: Int, secondNumber: Int): Int
}

class Addition : MathematicalOperation() {
    
    override fun doCalculation(firstNumber: Int, secondNumber: Int): Int {
        return firstNumber + secondNumber
    }

}

class Subtraction : MathematicalOperation() {

    override fun doCalculation(firstNumber: Int, secondNumber: Int): Int {
        return firstNumber - secondNumber
    }

}

class Multiplication : MathematicalOperation() {

    override fun doCalculation(firstNumber: Int, secondNumber: Int): Int {
        return firstNumber * secondNumber
    }

}        

Here in the above example, we are having an abstract class which is having the abstract method and all the subclasses are implementing its functionality according to the type of classes, So in the future, any new class will introduce so we need not modify existing code rather we can just define 1 class and its functionality and we are done.

3. Liskov substitution Principle:

Child classes should never break the parent class’ type definitions.

Parent classes should be easily substituted with their child classes without changing the behaviour of the parent classes. It means that a subclass should override the methods from a parent class, which does not break the functionality of the parent class.

This becomes important when we use inheritance, We always need to check if the child class is a proper subset of a parent class then only we should use inheritance otherwise we are breaking LSP.

Let's see the bad example:

open class Bird {
    fun fly() {}
}
class Parrot : Bird()
class Hen : Bird()        

Here in the above example, we have bird class and we have Parrot and Hen subclasses

Bird class has the fly method but the Hen can not fly so it violates the principle

Let's fix the issue by LSP:

open class Bird {
}

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

class Parrot : FlyingBirds()
class Hen : Bird()        

Here we have introduced one more class which has a fly method that is applicable only to the type of bird that can fly

3. Interface Segregation Principle:

The Interface Segregation Principle (ISP) states that a client should not be exposed to methods it doesn’t need. Declaring methods in an interface that the client doesn’t need pollutes the interface and leads to a “bulky” or “fat” interface.

Let’s see the bad example:

interface Listener{
    fun longClickListener()
    fun onClickListener()
    fun sendDataToOtherPage()
    fun showToast()
    fun clearInput()
}        

Here in the above example, we have only 1 interface and many methods which some classes will not need so we are making a bulky interface that is of not much use for many classes

Let's fix this:

interface ClickListener{
    fun longClickListener()
    fun onClickListener()
}

interface DataDownloaded{
    fun showResult()
    fun showToast()
}        

Here we have 2 interfaces at the time of listeners we can use the click listener interface and on data loaded we can use another interface which are having fewer and more relevant methods

3. Dependency Inversion Principle:

Any higher classes should always depend upon the abstraction of the class rather than the detail.

Higher classes are not dependent on the lower classes but instead depend upon the abstraction of the lower classes.

Let's see the bad example:

class Computer(keyboard:DellKeyboard){
    
    fun assembleComputer(){
        //use keyboard and other components
    }
}

class DellKeyboard(){
    
}        

Here In the above example, I am dependent on Dell Keyboard which violates the DI principle

Let’s Fix the example:

class Computer(keyboard: Keyboard) {

    fun assembleComputer() {
        //use keyboard and other components
    }
}

open class Keyboard {
}

class DellKeyboard : Keyboard() {
}
class HPKeyboard : Keyboard() {
}        

Here, we are expecting a keyboard but the type is not defined so we can pass any type of keyboard depending on the requirements.

Conclusion I have covered the basics of the SOLID principle there are so many complex examples available but before going to that we need to be clear about the basic ones.

Now that we have learned about the SOLID principle basics and have seen some examples now try to find usage in your current projects and apply them.

=============================================================

Subrat Shankar Sahu

Mobile Application Developer at Skillmine Technology Consulting

2y

Best Article regarding SOLID Principle

To view or add a comment, sign in

More articles by Aalishan Ansari

  • Android Activity Frequently Asked Interview Questions Part-1

    1. What is the activity?/What do you know about the activity? Ans: Activity is one of the building blocks of Android…

    6 Comments
  • MVP Architecture with Example in Android

    In this blog, we will take an example of a login screen in Android and cover up MVP very simply. As shown in the above…

    2 Comments
  • Activity lifecycle callbacks A →B and B →A

    Suppose you are having one more activity called C If you kill Activity C from the task manager so onDestroy(C)…

    1 Comment
  • Fragment to Fragment Communication Using ViewModel

    In MainActivity class MainActivity : AppCompatActivity() { private var _activityMainBinding: ActivityMainBinding?…

    2 Comments
  • Design Patterns (Very simple examples)

    In the software industry, most common problems have common solutions, and that solution follows common types of…

    1 Comment
  • References Role in Garbage Collection

    In Java, there are four types of references differentiated by the way by which they are garbage collected. Strong…

  • Activity|LaunchMode|With Lifecycle

    Before starting development in any platform first, we should understand the ecosystem of the platform. If you are…

    2 Comments
  • OOPs Concepts (Examples in Kotlin)

    Object-Oriented Programming in detail. The Oops concepts are the foundation when you want to create a good application…

  • Rx Android

    Rx Android is a concept used in android app development to give app better performance. Reactive Extension is a library…

    1 Comment
  • Context/Application Context?

    Context is the bridge between components. You use it to communicate between components, instantiate components, and…

    1 Comment

Insights from the community

Others also viewed

Explore topics