DevAndAndroid

Build for humans, not just for screens.


S.O.L.I.D Principle in Android

We must all have heard about the S.O.L.I.D principle when developing software tools. This post is just a refresher on the basics of the S.O.L.I.D principle and provides some examples on how this would be applicable to Android development.

So let’s begin by stating the 5 principles of S.O.L.I.D.

S — The Single Responsibility Principle (SRP):

A class should have only one reason to change.

What I understood: This means that one class should only have one responsibility.

Scenario: Let’s take the OnBindViewHolder method in RecyclerView.Adapter class. The role of the OnBindViewHolder is to map a list item to a view. There should be no logic in this method.

Let’s look at the below example to see how the SRP principle is violated in the OnBindViewHolder method in this scenario:

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val movie = movies[position]
holder.title.text = movie.title
holder.rating.text = movie.rating
// SRP violation, onBindViewHolder has only the responsibility to display data
// & not make data formatting operations
val genres = movie.genres
val builder = StringBuilder()
for (genre in genres) {
builder.append(genre).append(",")
}
holder.genres.text = builder.toString()
}
OnBindViewHolder has only one responsibility which is to display data & not make data formatting operations

Now, let’s look at the correct way to implement the OnBindViewHolder method using SRP principle:

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val movie = movies[position]
holder.title.text = movie.title
holder.rating.text = movie.rating
// all the logic is moved into util class…now is clean!
holder.authors.text = AppUtils.getGenres(movie)
}
All the logic is moved into the Util class. So now the code is clean!

O — The Open-Closed Principle (OCP):

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

What I understood: This means that if we are required to add a new feature to the project, it is good practice to not modify the existing code but rather write new code that will be used by the existing code.

Scenario: Let’s say we have a class called TimeOfDayGreeting with a single method getGreetingFromTimeOfDay. We would like to display a greeting message when the user opens the app. This message must be based on the time of the day.

Let’s look at the below example to see how the OCP principle is violated in this scenario:

class TimeOfDayGreeting {
private var timeOfDay: String? = null
/*
* Every time this method is called it will
* called an if else logic, which is in violation of the
* OCP principle.
*/
fun getGreetingFromTimeOfDay(): String {
return when (timeOfDay) {
"Morning" -> "Good Morning, sir."
"Afternoon" -> "Good Afternoon, sir."
"Evening" -> "Good Evening, sir."
else -> "Good Night, sir."
}
}
fun setTimeOfDay(timeOfDay: String) {
this.timeOfDay = timeOfDay
}
}
Every time this method is called, if else condition will be executed, which violates the OCP principle

Now, let’s look at the correct way to implement this feature using the OCP principle:

interface TimeOfDay {
fun greet(): String
}
class TimeOfDayGreeting(private val timeOfDay: TimeOfDay) {
fun getGreetingFromTimeOfDay(): String {
return timeOfDay.greet()
}
}
class Morning : TimeOfDay {
override fun greet(): String {
return "Good morning, sir."
}
}
class Afternoon : TimeOfDay {
override fun greet(): String {
return "Good afternoon, sir."
}
}
class Evening : TimeOfDay {
override fun greet(): String {
return "Good evening, sir."
}
}
class Night : TimeOfDay {
override fun greet(): String {
return "Good night, sir."
}
}
Created an interface to handle action. Created 4 different classes for 4 different times of the day. The TimeOfDayGreeting class does not handle any logic now.

L —The Liskov Substitution Principle (LSP):

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

What I understood: This means that a sub class should override the methods from a parent class that does not break the functionality of the parent class.

Scenario: Let’s say we have a interface ClickListener. This interface is implemented by the fragments 1 & 2. We would need to implement the ClickListener interface in both the fragments. Our requirement is to increment click count in fragment 2 but decrement click count in fragment 1.

Let’s look at the below example to see how the LSP principle is violated in this scenario:

interface ClickListener {
fun onClick()
}
class Fragment1 : ClickListener {
override fun onClick() {
//handle logic
}
fun decrementClickCount() {
}
}
class Fragment2 : ClickListener {
override fun onClick() {
//handle logic
}
fun incrementClickCount() {
}
}
fun onButtonClick(clickListener: ClickListener) {
// IF we have a requirement where we need to increment the click count in
// fragment2 but decrement the count in fragment 1
// we would have to follow something like this, which is bad practice.
when (clickListener) {
is Fragment2 -> clickListener.incrementClickCount()
is Fragment1 -> clickListener.decrementClickCount()
}
clickListener.onClick()
}
view raw MainActivity.kt hosted with ❤ by GitHub
Bad Practice of LSP principle

Now, let’s look at the correct way to implement this feature using the LSP principle:

interface ClickListener {
fun onClick()
}
class Fragment1 : ClickListener {
override fun onClick() {
decrementClickCount()
//handle logic
}
fun decrementClickCount() {
}
}
class Fragment2 : ClickListener {
override fun onClick() {
incrementClickCount()
//handle logic
}
fun incrementClickCount() {
}
}
/*
* We handle the individual logic inside the overridden methods
* in the fragments. In the main implementation we should
* never handle logic
*/
fun onButtonClick(clickListener: ClickListener) {
clickListener.onClick()
}
view raw MainActivity.kt hosted with ❤ by GitHub
Good practice of LSP principle

I — The Interface Segregation Principle (ISP):

The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.

What I understood: This means that if an interface becomes too fat, then it should be split into smaller interfaces so that the client implementing the interface does not implement methods that are of no use to it.

Scenario: Let’s take the TextWatcher interface in Android. We know that the TextWatcher interface has 3 methods. This is an example of how this principle is violated, since we only use one of the methods most of the time.

import android.text.Editable
import android.text.TextWatcher
import android.widget.EditText
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
// Empty implementation
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
// In most scenarios, this is the only method we use. The other methods are often unnecessary in these cases.
}
override fun afterTextChanged(s: Editable?) {
// Empty implementation
}
})
view raw TextWatcher.kt hosted with ❤ by GitHub
Bad practice of ISP principle

Now, let’s look at the correct way to implement this feature using the ISP principle:

import android.text.Editable
import android.text.TextWatcher
import android.widget.EditText
// We create an interface with one method
interface TextWatcherWithInstance {
fun onTextChanged(editText: EditText, s: CharSequence, start: Int, before: Int, count: Int)
}
// We create a custom class called MultiTextWatcher.
// And pass the interface here
class MultiTextWatcher {
private lateinit var callback: TextWatcherWithInstance
fun setCallback(callback: TextWatcherWithInstance): MultiTextWatcher {
this.callback = callback
return this
}
fun registerEditText(editText: EditText): MultiTextWatcher {
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
callback.onTextChanged(editText, s, start, before, count)
}
override fun afterTextChanged(editable: Editable) {}
})
return this
}
}
// We can call this class from our Activity/Fragment like this:
// This only has one method, which we are using in the app
MultiTextWatcher()
.registerEditText(editText)
.setCallback(object : TextWatcherWithInstance {
override fun onTextChanged(editText: EditText, s: CharSequence, start: Int, before: Int, count: Int) {
// Implementation here
}
})
view raw TextWatcher.kt hosted with ❤ by GitHub
Good practice of ISP principle

D — The Dependency Inversion Principle (DIP):

High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend upon details. Details should depend upon abstractions.

What I understood: If you use a class insider another class, this class will be dependent of the class injected.

Scenario: Let’s say we have a class called JobTracker. The requirement is to update users via email or call based on the urgency of the job.

Let’s look at the below example to see how the DIP principle is violated in this scenario:

import kotlin.jvm.JvmStatic
/**
* Interface for generating job alerts
*/
interface AlertGenerator {
fun generateJobAlert(job: String): String
}
/**
* Class called Emailer to send email alerts
*/
class Emailer : AlertGenerator {
override fun generateJobAlert(job: String): String {
return "You are alerted for $job"
}
}
/**
* Class called Phone to send phone alerts
*/
class Phone : AlertGenerator {
override fun generateJobAlert(job: String): String {
return "You are alerted for $job"
}
}
/**
* Class called JobTracker.
* This class uses AlertGenerator interface to follow DIP principle
*/
class JobTracker(private val alertGenerators: List<AlertGenerator>) {
/**
* Based on the jobDescription, the alert is sent
*/
fun setCurrentConditions(jobDescription: String) {
when (jobDescription) {
"urgent" -> alertGenerators.forEach { generator ->
if (generator is Phone) {
println(generator.generateJobAlert(jobDescription))
}
}
"brief" -> alertGenerators.forEach { generator ->
if (generator is Emailer) {
println(generator.generateJobAlert(jobDescription))
}
}
}
}
companion object {
@JvmStatic
fun main(args: Array<String>) {
val alertGenerators = listOf(Phone(), Emailer())
val jobTracker = JobTracker(alertGenerators)
jobTracker.setCurrentConditions("urgent")
jobTracker.setCurrentConditions("brief")
}
}
}
view raw JobTracker.kt hosted with ❤ by GitHub
Bad example of DIP principle

Now, let’s look at the correct way to implement this feature using the DIP principle:

// Solution step 1: Create an interface called Notifier.
// It has one method: jobAlert(String jobDescription)
interface Notifier {
fun jobAlert(jobDescription: String)
}
// Step II:
// Create an emailClient class that implements
// Notifier interface
class EmailClient : Notifier {
override fun jobAlert(jobDescription: String) {
if (jobDescription == "brief") {
print("Job description is brief")
}
}
}
// Step III:
// Create a PhoneClient class that implements
// Notifier interface
class PhoneClient : Notifier {
override fun jobAlert(jobDescription: String) {
if (jobDescription == "urgent") {
print("Job description is urgent")
}
}
}
// Step IV: The JobTracker class would just
// call the respective notifiers. It does not
// handle any logic
class JobTracker {
private lateinit var currentAlert: String
fun setCurrentConditions(jobDescription: String) {
this.currentAlert = jobDescription
}
fun notify(notifier: Notifier) {
notifier.jobAlert(currentAlert)
}
}
view raw JobTracker.kt hosted with ❤ by GitHub
Good practice of DIP principle

And that’s it! Happy Coding, Cheers!!




Leave a comment