Blog Infos
Author
Published
Topics
Published
Topics

Before we jump onto the topic of SOLID design principles, you must understand why we need them in the first place. If you are hearing the term, SOLID, for the first time then sit tight as you’ll learn a whole new way of designing your classes.

What problems we are trying to solve using SOLID principles?

Let me try to answer the most important question.

How many of you have been slowed down by really bad code? All of us at some point.

If we know that bad code slows us down then why do we write bad code?
We do it as we had to go fast ….and let that sink in.

As Uncle Bob, Robert C. Martin says

You don’t go fast by rushing.

You don’t go fast by just making it work and releasing it as fast as you can.

You wanna go fast? You do a good job.

You sit carefully, think about the problem, type a little, clean it up and repeat. That’s how you go fast.

What are the symptoms of bad code?

  • Code Rigidity
    Code that has dependencies in so many directions that you can’t make a change in isolation.
    You change one part of the code and it breaks the calling/dependant class and you have to fix it there. In the end, because of that one change, you end up making changes in 10 different classes.
  • Code Fragility
    When you make a change and an unrelated piece of code breaks.
  • Tight Coupling
    It happens when a class depends on another class.

If you can relate to any of the above issues, then this article is for you!

In this article, we will learn how to overcome these issues using SOLID design principles.

So, Why do we need them?

We need them to write

  • Flexible Code
  • Maintainable Code
  • Understandable Code
  • Code can tolerate changes
What are SOLID principles?

SOLID is an acronym that stands for 5 design principles.

  • S — Single Responsibility Principle (SRP)
  • O — Open/Closed Principle (OCP)
  • L — Liskov Substitution Principle (LSP)
  • I — Interface Segregation Principle (ISP)
  • D — Dependency Inversion Principle (DIP)
S — Single Responsibility Principle (SRP)

A module should have one, and only one reason to change.

What is a module?

The simplest definition is just a source file.

Some languages and development environments, though, don’t use source files to contain their code. In those cases, a module is just a cohesive set of functions and data structures.

Source: Clean Architecture, Robert C. Martin

Before understanding how SRP is followed/implemented/used, we should understand how it is not used.

Violation of SRP

class Order {
fun sendOrderUpdateNotification() {
// sends notification about order updates to the user.
}
fun generateInvoice() {
// generates invoice
}
fun save() {
// insert/update data in the db
}
}

Can you spot the violation?

The violation is that the Order handles more than one responsibility which means it has more than one reason to change.

Solution

Create an Order which is responsible for holding the order data.

data class Order(
val id: Long,
val name: String,
// ... other properties.
)
view raw SRPOrder.kt hosted with ❤ by GitHub

Create OrderNotificationSender which is responsible for sending notification updates to the user.

class OrderNotificationSender {
fun sendNotification(order: Order) {
// send order notifications
}
}

Create OrderInvoiceGenerator which is responsible for generating the order invoice.

class OrderInvoiceGenerator {
fun generateInvoice(order: Order) {
// generate invoice
}
}

Create OrderRepository which is responsible for storing the order in the database.

class OrderRepository {
fun save(order: Order) {
// insert/update data in the db.
}
}

We have extracted different responsibilities from the Order class into separate classes and each one of them has a single responsibility.

Optionally you can even go a step further and create a OrderFacade which delegates the responsibilities to the individual classes.

class OrderFacade(
private val orderNotificationSender: OrderNotificationSender,
private val orderInvoiceGenerator: OrderInvoiceGenerator,
private val orderRepository: OrderRepository
) {
fun sendNotification(order: Order) {
// sends notification about order updates to the user.
orderNotificationSender.sendNotification(order)
}
fun generateInvoice(order: Order) {
// generates invoice
orderInvoiceGenerator.generateInvoice(order)
}
fun save(order: Order) {
// insert/update data in the db
orderRepository.save(order)
}
}
view raw OrderFacade.kt hosted with ❤ by GitHub

As we can see that each class has a single responsibility, thus following the Single Responsibility Principle.

O — Open/Closed Principle ( OCP)

The OCP was coined in 1988 by Bertrand Meyer as

A software artifact should be open for extension but closed for modification.

In other words, the behavior of a software artifact ought to be extendible without having to modify that artifact.

Source: Clean Architecture, Robert C. Martin

Violation of OCP

To understand the violation of OCP, let’s take an example of a Notification Service that sends different types of notifications — Push Notifications and Email Notifications to the recipients.

enum class Notification {
PUSH_NOTIFICATION, EMAIL
}
view raw Notification.kt hosted with ❤ by GitHub

class NotificationService {
fun sendNotification(notification: Notification) {
when (notification) {
Notification.PUSH_NOTIFICATION -> {
// send push notification
}
Notification.EMAIL -> {
// send email notification
}
}
}
}

Let’s say that I get a new requirement and we now support SMS Notifications, which means I have to update the Notification enum and the NotificationService to support SMS notifications.

So, the Notification and NotificationService will be like this

enum class Notification {
PUSH_NOTIFICATION, EMAIL, SMS
}
view raw Notification.kt hosted with ❤ by GitHub
class NotificationService {
fun sendNotification(notification: Notification) {
when (notification) {
Notification.PUSH_NOTIFICATION -> {
// send push notification
}
Notification.EMAIL -> {
// send email notification
}
Notification.SMS -> {
// send sms notification
}
}
}
}

This means that every time we change the notification type, we will have to update the NotificationService to support the change.

This is a clear violation of the OCP. Let’s see how you can abide by the OCP.

Solution

Create an interface Notification.

interface Notification {
fun sendNotification()
}
view raw Notification.kt hosted with ❤ by GitHub

Create the implementations of the Notification of each type — PushNotification, and EmailNotification.

class PushNotification : Notification {
override fun sendNotification() {
// send push notification
}
}
class EmailNotification : Notification {
override fun sendNotification() {
// send email notification
}
}

Create NotificationService.

class NotificationService {
fun sendNotification(notification: Notification) {
notification.sendNotification()
}
}

Now, your NotificationService follows OCP as you can add/remove different types of notifications without modifying the NotificationService.

Create SMSNotification which implements Notification.

class SMSNotification : Notification {
override fun sendNotification() {
// send sms notification
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

As you can see, I have added SMSNotification without modifying NotificationService thus following the Open/Closed Principle.

Side Note:

This is the one principle that is really hard to follow and one can’t fully abide by it only in an ideal world.

As 100% closure is not attainable, the closure must be strategic.

L — Liskov Substitution Principle (LSP)

In 1988, Barbara Liskov wrote the following as a way of defining subtypes.

If for each object o1 of type there is an object o2 of type such that for all programs defined in terms of T, the behavior of is unchanged when o1 is substituted for o2 then S is a subtype of T.

In other words, it means that the child type should be able to replace the parent without changing the behavior of the program.

Let us try to understand the principle by seeing the violation of the infamous Square/Rectangle problem.

Violation of LSP

We know that a rectangle is a polygon with 4 sides where opposite sides are equal and are at 90°.

A square can be defined as a special type of rectangle that has all sides of the same length.

If squares and rectangles were to follow LSP then we should be able to replace one with the other.

Please Note: The Square and the Rectangle are written in Java as Kotlin code would clearly show the violation without me proving it

Create a Rectangle

public class Rectangle {
private int height;
private int width;
public void setHeight(int height) {
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public int area() {
return height * width;
}
}
view raw Rectangle.java hosted with ❤ by GitHub

Create Square

public class Square extends Rectangle {
@Override
public void setHeight(int height) {
setSide(height);
}
@Override
public void setWidth(int width) {
setSide(width);
}
private void setSide(int side) {
super.setHeight(side);
super.setWidth(side);
}
}
view raw Square.java hosted with ❤ by GitHub

Create Driver to execute the flow.

fun main() {
val rectangle = Rectangle()
rectangle.setHeight(5)
rectangle.setWidth(2)
val rectangleCheck = rectangle.area() == 10 // true
val square: Rectangle = Square()
square.setHeight(5) // width is also set to 5
square.setWidth(2) // height is also set to 2
val squareCheck = square.area() == 10 // false - not substitutable
}
view raw LSPDriver.kt hosted with ❤ by GitHub

In the above code, Driver we can clearly see that Rectangle and Square cannot replace each other. Hence, LSP is clearly violated.

Under no circumstances the above problem will follow LSP. So, for the solution/example of the LSP, we will look at another problem.

Example of LSP

Let’s consider a Waste Management Service which processes different types of waste — Organic waste and Plastic waste.

Create Waste interface

interface Waste {
fun process()
}
view raw Waste.kt hosted with ❤ by GitHub

Create OrganicWaste and PlasticWaste which implements Waste interface.

class OrganicWaste : Waste {
override fun process() {
println("Processing Organic Waste")
}
}
view raw OrganicWaste.kt hosted with ❤ by GitHub
class PlasticWaste : Waste {
override fun process() {
println("Processing Plastic Waste")
}
}
view raw PlasticWaste.kt hosted with ❤ by GitHub

Create WasteManagementService

class WasteManagementService {
fun processWaste(waste: Waste) {
waste.process()
}
}

Create LSPDriver

fun main() {
val wasteManagementService = WasteManagementService()
var waste: Waste
waste = OrganicWaste()
wasteManagementService.processWaste(waste) // Output: Processing Organic Waste
waste = PlasticWaste()
wasteManagementService.processWaste(waste) // Output: Processing Plastic Waste
}
view raw LSPDriver hosted with ❤ by GitHub

In the LSPDriver we can clearly see that we are able to replace different types of wastes, i.e Organic and Plastic, with each other without affecting the behavior of the program. Thus following the Liskov Substitution Principle.

I — Interface Segregation Principle (ISP)

The Interface Segregation Principle states that developers shouldn’t be forced to depend upon the interfaces that they don’t use.

In other words, the class that implements the interface shouldn’t be forced to use the methods it does not need.

Violation of ISP

Let’s assume that we are building a UI library that has components and the components can have different UI interactions like Click events – single-click and long-click.

We have an interface OnClickListener that has different click behaviors, in order for a UI component to have this behavior, it must implement the OnClickListener interface.

Create OnClickListener

interface OnClickListener {
fun onClick()
fun onLongClick()
}

Create CustomUIComponent

class CustomUIComponent : OnClickListener {
override fun onClick() {
// handles onClick event.
}
// left empty as I don't want the [CustomUIComponent] to have long-click behavior.
override fun onLongClick() {
}
}

We can clearly see that the CustomUICompoenent is forced to override onLongClick method even though as per the requirements we don’t want the CustomUICompoenent to have long click behavior.

This is a clear violation of the LSP.

Solution

This solution is straight-forward, we can separate the OnClickListener interface into two different interfaces — OnClickListener and OnLongClickListener, they handle single-click behavior and long-click behavior respectively.

Create OnClickListener

interface OnClickListener {
fun onClick()
}

Create OnLongClickListener

interface OnLongClickListener {
fun onLongClick()
}

Create CustomUICompoenent which implements OnClickListener

class CustomUIComponent : OnClickListener {
override fun onClick() {
// handle single-click event
}
}

Now the CustomUIComponent is not forced to override onLongClick method. Hence following the Interface Segregation Principle.

D — Dependency Inversion Principle (DSP)

The Dependency Inversion Principle states that the most flexible systems are those in which code dependencies refer only to abstractions, not to concretions.

In order to understand this principle, you should know what I mean when I say that Class A depends on Class B.

Let’s go off-road for a bit to understand the above line.

Let’s say I have two classes ClassA and ClassB, the code is written as follows

class ClassA {
fun doSomething() {
println("Doing something")
}
}
class ClassB {
fun doIt() {
val classA = ClassA() // <- ClassB needs an object of ClassA in order to work properly
classA.doSomething()
}
}

You can see in line 9 that an object of ClassA is created and in line 10 the method doSomething() is called. As ClassB needs an object of ClassA to function properly, we can say that ClassA depends on ClassB.

With the help of DIP, we will inverse this dependency.

The above diagram shows DIP in action as we have invested the dependency between ClassA and ClassB, the same can be seen in the above diagram.

Now let’s see the example to understand DIP.

An example where the classes depend on each other

Let’s say we have a NotificationService which sends only one type of notification, i.e email notifications, as it is tightly coupled with the EmailNotification class.

Create EmailNotification

class EmailNotification {
fun sendNotification(message: String) {
println("Sending email notification with message \"$message\"")
}
}

Create NotificationService

class NotificationService {
fun sendNotification(message: String) {
val emailNotification = EmailNotification() // <- here is the dependency
emailNotification.sendNotification(message)
}
}

Create NotificationDriver

fun main() {
val notificationService = NotificationService()
notificationService.sendNotification("Happy Coding") // Output: Sending email notification with message "Happy Coding"
}

The problem is NotificationServicedepends on EmailNotification to send notifications. That is where the dependency comes in.

We have to remove the dependency in such a way that NotificationService doesn’t depend on the type of notification and should be able to send different types of notifications.

Solution

The solution is pretty straightforward as we have already solved this problem when we looked at OCP.

In order for NotificationService to be independent of the type of notification then it should depend on the abstract class or an interface rather than the concrete class, i.e EmailNotification.

Create Notification interface.

interface Notification {
fun sendNotification(message: String)
}
view raw Notification.kt hosted with ❤ by GitHub

Create the type of notifications — EmailNotification and SmsNotification

class EmailNotification : Notification {
override fun sendNotification(message: String) {
println("Sending email notification with message \"$message\"")
}
}
class SmsNotification : Notification {
override fun sendNotification(message: String) {
println("Sending sms notification with message \"$message\"")
}
}

Create NotificationService

class NotificationService {
// this can be injected through constructor as well and it would be constructor injection
var notification: Notification? = null
fun sendNotification(message: String) {
notification?.sendNotification(message)
}
}

Create NotificationDriver

fun main() {
val message = "Happy Coding"
val notificationService = NotificationService()
var notification: Notification
notification = EmailNotification()
notificationService.notification = notification
notificationService.sendNotification(message)
// Output: Sending email notification with message "Happy Coding"
notification = SmsNotification()
notificationService.notification = notification
notificationService.sendNotification(message)
// Output: Sending sms notification with message "Happy Coding"
}

You can see that the NotificationService now depends on the Notification interface rather than the concretion, i.e Email Service, we can easily swap the implementation of the Notification making the system flexible. Thus following the Dependency Inversion Principle.

Summary

All the SOLID principles can be defined in a single line as follows.

SRP — Each software module should have one, and only one, reason to change.

OCP — Software systems should be easy to change, they must be designed to allow the behavior of those systems to be changed by adding new code, rather than changing the existing code.

LSP — To build a software system from interchangeable parts, those parts must adhere to a contract that allows those parts to be substituted one for another.

ISP — Software designers should avoid depending on the things they don’t use.

DIP — The code that implements high-level policy should not depend on low-level details.

Source: Clean Architecture, Robert C. Martin

Conclusion

SOLID design principles are 5 core principles that every developer must know. These principles help us to write flexible, understandable, and maintainable code which is susceptible to changes.

How do you solve a similar problem in your project? Comment below or reach out to me on Twitter or LinkedIn.

Thank you very much for reading the article. Don’t forget to 👏 if you liked it.

References

This article was originally published on proandroiddev.com on December 05, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu