Solid Principles - Kotlin Flashcards
What is SOLID Principles ?
In object-oriented computer programming, SOLID is design principles intended to make software more understandable, flexible and maintainable.
SOLID helps us to write sustainable code while developing software. This means, when software requirements have changed or we want to make additions to existing software, the system won’t resist this. We will add new requirements and functions easily. In addition to these, there are reasons such as maintenance and being easy to understand.
- S - Single Responsibility Principle
- O - Open-Closed Principle
- L - Liskov substitution Principle
- I - Interface segregation Principle
- D - Dependancy Inversion Principle
S - Single Responsibility Principle
“A class should have one, and only one, reason to change.”
Single-responsibility principle means that every module, class or method in a computer program should have responsibility over a single part of that program’s functionality.
We shouldn’t have objects which know too much and have unnecessary behavior. So, a class will do only one job. Thus, this class should have only one reason to change.
Single Responsibility Principle Example 1
For example, let’s think that we have class called as user which holds the information about user. Then we will add sign in and sign out methods for this user to manage authentication operations.
data class User(
var id: Long,
var name: String,
var password: String
){
fun signIn(){
// This method will do signing in operations
}
fun signOut(){
// This method will do signing out operations
}
}
But as you have learned with Single Responsibility Principle, all classes should have responsibility for a single process of a program.
If we would like to make some changes for authentication process in sign in or sign out methods, our User class will be affected too. So, we will add more than one responsibility to one class. We shouldn’t do that and we should separate our classes.
That means, User class should do operations for only holding informations of the user. If we would like to manage authentication process of user like signing in and signing out, we should add a new class to manage authentication process.
data class User( var id: Long, var name: String, var password: String )
class AuthenticationService(){
fun signIn(){
// This method will do signing in operations
}
fun signOut(){
// This method will do signing out operations
}
}
Single Responsibility Principle Example 2
Let’s have a look at some kotlin:
class Robot(val name: String, val type: String) {
fun greet() {
println(“Hello my name is $name, and I am a $type robot”)
}
}
Currently, this class is doing two things. Firstly, it’s representing our Robot entity and holding state for a name and type, and secondly, it’s concerned with how our robot is communicating. In this case, our robot isn’t very advanced and just prints to communicate.
data class Robot(val name: String, val type: String)
class RobotPrinter {
fun greet(robot: Robot) {
val name = robot.name
val type = robot.type
println(“Hello my name is $name and I am a $type robot”)
}
}
Now, after some minor refactoring, we have two classes with more specific tasks. We still have our Robot class, however, the functionality of greet has been moved to a separate class RobotPrinter. Without writing too much more code we’ve managed to decouple our entity from its presentation logic, which will benefit us in the long term. We’ve also been able to use a data class for our robot, a wonderful feature of kotlin.
Single Responsibility Principle Example 3
An example of violating Single Responsibility Principle:
class Yerba (val name: String, val brand: String, val db: Database){
fun saveYerbaToDb(yerba: Yerba) {
db.save(yerba)
}
}
The example above violates the single responsibility principle is because class Yerba have two responsibilities: Properties management responsibility and Database management responsibility.
To resolve this, we can separate the responsibilities into two different classes:
class Yerba (val name: String, val brand: String){}
class DbUtils (val db: Database){
fun saveYerbaToDb(yerba: Yerba) {
db.save(yerba)
}
}
O — Open Closed Principle
“Software entities such as classes, functions, modules should be open for extension but not modification.”
The second SOLID Principle is the Open-Closed Principle. This means that classes, modules and methods should be open for extension but closed for modification. We do this by creating abstractions in languages such as kotlin we can use interfaces, this abstraction should then be injected where needed. The aim of this is to drive a modular design.
Open Closed Principle Example 1
There are two different important meanings for this principle.
- Open: Means that we can add new features to class. When there are some changes on our dependencies, we should be able to add new features to our class easily.
- Closed: Means that base features of class shouldn’t be able to change Let’s imagine that we have a MobilePhoneUser class which is holding mobile phone and mobile services. This class will do operations for users’ mobile phones by working with mobile services. And there are 2 different mobile services(HMS and GMS).
**class MobilePhone{ lateinit var brandName: String }**
**class MobilePhoneUser{ fun runMobileDevice(mobileServices: Any, mobilePhone: MobilePhone){ if(mobileServices is HuaweiMobileServices) println("This device is running with Huawei Mobile Services") } }**
**class HuaweiMobileServices{ fun addMobileServiceToPhone(mobilePhone: MobilePhone){ println("Huawei Mobile Services") } }**
In the above code, we are checking mobile service type with if-else condition. This is a bad example, because when we want to add new mobile services, we will always need to check mobile services with if-else conditions.
According to Open-Closed Principle, we should add one interface for all mobile services. Then, each mobile service types will implement this interface and will do their own business. Thus, we won’t need to check mobile service type to make different operations.
**class MobilePhone{ lateinit var brandName: String }**
**class MobilePhoneUser{ fun runMobileDevice(mobileServices: IMobileServices, mobilePhone: MobilePhone){ mobileServices.addMobileServiceToPhone(mobilePhone) } }**
**interface IMobileServices{ fun addMobileServiceToPhone(mobilePhone: MobilePhone) }**
**class HuaweiMobileServices: IMobileServices{ override fun addMobileServiceToPhone(mobilePhone: MobilePhone){ println("Huawei Mobile Services") } }**
**class GoogleMobileServices: IMobileServices{ override fun addMobileServiceToPhone(mobilePhone: MobilePhone){ println("Google Mobile Services") } }**
Open Closed Principle Example 2
Let’s look at an example.
class Network {
private val uri = URI(“https://robot-hq.com”)
fun broadcast(message: String) {
val httpClient = HttpClient.newHttpClient()
val body = HttpRequest.BodyPublishers.ofString(message)
val request = HttpRequest.newBuilder(uri).POST(body).build()
httpClient.send(request, HttpResponse.BodyHandlers.discarding())
}
}
So looking at the above class, if we ever wanted to change how our Network broadcasts messages, we’ll have to change this class. What if we want to introduce a different means of communication, such as RPC? Or what if we want to format the message in a particular way? Let’s try and clean this up.
interface Network {
fun broadcast(message: String)
}
class HttpNetwork: Network {
private val uri = URI(“https://robot-hq.com”)
override fun broadcast(message: String) {
val httpClient = HttpClient.newHttpClient()
val request = HttpRequest.newBuilder(uri).POST(HttpRequest.BodyPublishers.ofString(message)).build()
httpClient.send(request, HttpResponse.BodyHandlers.discarding())
}
}
With only a small refactor we’ve made the code more modular. If we want to add WebSockets, that easy, just write a new class that implements Network. What if we want to use RPC or some other method? No problem, just write a new class. This means that any class dependant on Network will not need to change when we create new classes derived from Network.
Open Closed Principle Example 3
An example of violating Open/Closed Principle:
class Yerba (val name: String){
fun getBrand(): String {
when(name){
“kurupi” ->{return “Yerba Kurupí”}
“campesino” -> {return “Yerba Campesino”}
}
}
}
The example above violates open/closed principle is because class Yerba does not apply to every yerba in the Market. Think about what will happen if the yerba name is “pajarito”.
To resolve this, we can create an interface of class Yerba.
interface Yerba {
fun getBrand(): String
}
and Yerba with different name can extends from the class above:
class Kurupi: Yerba{
override fun getBrand(): String {
return “Yerba Kurupi”
}
}
class Campesino: Yerba{
override fun getBrand(): String {
return “Yerba Campesino”
}
}
class Pajarito: Yerba{
override fun getBrand(): String {
return “Yerba Pajarito”
}
}
L — Liskov Substitution Principle
“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
“Child classes should never break the parent class’ type definitions”.
“The derived class must be usable through the base class interface, without the need for the user to know the difference.”
This principle suggests that “parent classes should be easily substituted with their child classes without blowing up the application.”
This means that a subclass should override the methods from a parent class that does not break the functionality of the parent class.
Liskov Substitution Principle suggests that we can replace a Parent Class with a Child class without altering the correctness of the application. It was named of renowned computer scientist–Barbara Liskov.
Liskov Substitution Principle Example 1
We should be able to use subclasses instead of the parent classes which class they have extended, without the need to change our code. In simple words, the child class must be substitutable for the parent class.
Since child classes extended from the parent classes, they inherit their behavior. If child classes can not perform the behaviors belonging to the parent classes, probably, we won’t write any code in the method that does the behavior or we will throw an error when objects want to use it. But these actions cause code pollution and unnecessary code crowds.
Let’s think that we have an abstract class called as Vehicle. This abstract class have some methods about engine situation and moving forward/back. When we want to create child classes such as Car, Truck. to extend Vehicle abstract class will be fine for us.
abstract class Vehicle{
protected var isEngineWorking = false
abstract fun startEngine()
abstract fun stopEngine()
abstract fun moveForward()
abstract fun moveBack()
}
**class Car: Vehicle(){ override fun startEngine() { println("Engine started") isEngineWorking = true }**
override fun stopEngine() {
println(“Engine stopped”)
isEngineWorking = false
}
override fun moveForward() {
println(“Moving forward”)
}
override fun moveBack() {
println(“Moving back”)
}
}
**class Bicycle: Vehicle(){ override fun startEngine() { // TODO("Not yet implemented") }**
**override fun stopEngine() { // TODO("Not yet implemented") }**
override fun moveForward() {
println(“Moving forward”)
}
override fun moveBack() {
println(“Moving back”)
}
}
But as you see in the above code, when we want to create child class called as Bicycle, it’s startEngine and stopEngine methods will be unnecessary. Because bicycles don’t have an engine.
To fix this situation, we can create a new child class which will extend the Vehicle. This class will work with vehicles which will have an engine.
**interface Vehicle{ fun moveForward() fun moveBack() }**
**abstract class VehicleWithEngine: Vehicle{ private var isEngineWorking = false open fun startEngine(){ isEngineWorking = true } open fun stopEngine(){ isEngineWorking = false } }**
**class Car: VehicleWithEngine(){ override fun startEngine() { super.startEngine() println("Engine started") }**
override fun stopEngine() {
super.stopEngine()
println(“Engine stopped”)
}
override fun moveForward() {
println(“Moving forward”)
}
override fun moveBack() {
println(“Moving back”)
}
}
**class Bicycle: Vehicle{ override fun moveForward() { println("Moving forward") }**
override fun moveBack() {
println(“Moving back”)
}
}
Liskov Substitution Principle Example 2
Let’s we have the following code. Someone has decided to follow the Open-Closed Principle in order to make the code more modular, we can add as many different robots as we like with easy (Good for the Open-Closed Principle).
abstract class Robot {
abstract fun goToLocation(lat: Double, long: Double)
abstract fun jump()
}
However, later on, we decide to start shipping heavy robots(to deal with larger tasks). So we have this kotlin class:
class HeavyRobot: Robot() {
override fun jump() {
}
…
}
Can you see the issue? The method doesn’t do anything as this is a heavy robot, when we follow the Open-Closed Principle, we want our derived classes to work the same. So when our developer wants the robots to jump, we’d be left with a set of robots just sitting around, not doing much. This is what happens when we fail to follow the Liskov Substitution Principle, things stop working as we expect. Let’s see how we’d fix this.
abstract class Robot {
abstract fun goToLocation(lat: Double, long: Double)
}
abstract class LightweightRobot: Robot() {
abstract fun jump()
}
What we’ve done is moved the jump method into another derived class. Now, our HeavyRobot can inherit from Robot and we will have a much easier time getting the robots to where they need to be.
Liskov Substitution Principle Example 3
A very good example of violating Liskov substitution principle is shown as below:
class Rectangle {
private var width: Int = 0
private var height: Int = 0
fun getArea(): Int {
return width * height
}
fun setWidth(width: Int) {
this.width = width
}
fun setHeight(height: Int) {
this.height = height
}
}
class Square : Rectangle() {
override fun setWidth(width: Int) {
super.setWidth(width)
super.setHeight(width)
}
override fun setHeight(height: Int) {
super.setWidth(height)
super.setHeight(height)
}
}
val rectangle = Rectangle()
rectangle.setWidth(3)
rectangle.setHeight(5)
rectangle.getArea() = 15
val square = Square()
square.setWidth(3)
square.setHeight(5)
square.getArea() = 25
getArea() will return as different value for a square object and a rectangle object, hence violating the rule without altering the correctness of that program. To resolve this, what we can do is treat them as a different shape.
interface Shape {
fun getArea(): Int
}
class Rectangle: Shape {
private var width: Int = 0
private var height: Int = 0
override fun getArea(): Int {
return width * height
}
fun setWidth(width: Int) {
this.width = width
}
fun setHeight(height: Int) {
this.height = height
}
}
class Square: Shape {
private var diameter: Int = 0
override fun getArea(): Int {
return diameter * diameter
}
fun setDiameter(diameter: Int) {
this.diameter = diameter
}
}
val rectangle = Rectangle()
rectangle.setWidth(3)
rectangle.setHeight(5)
rectangle.getArea() = 15
val square = Square()
square.setWidth(3)
square.setHeight(5)
square.getArea() = 25
I — Interface Segregation Principle
“The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use”.
This principle suggests that “many client specific interfaces are better than one general interface”.
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.
This is the first principle which is applied on interface, all the above three principles applies on classes.
Interface Segregation Principle Example 1
The Interface Segregation Principle sounds pretty simply right? ISP violations can sneak into your system over time as features are added and requirements change. Codebases that violate this principle tend to be a bit tough to change as a lot of side effects can occur due to the larger interfaces and classes that implement them. What we’re aiming for is a few smaller interfaces, for specific tasks, rather than larger more generic ones.
interface Robot {
fun goToLocation(lat: Double, long: Double)
fun wave()
}
Looks harmless right? However, what if one day we decide to write an implementation like so:
class StationaryRobot: Robot {
override fun goToLocation(lat: Double, long: Double) {
}
override fun wave() {
println(“👋”)
}
}
We have an implementation that doesn’t implement goToLocation, a clear violation of the Interface Segregation Principle, therefore the Robot interface is not a great abstraction. What we can do to remedy this is split the Robot interface into two smaller interfaces. Let’s say MobileRobot and WavingRobot.
interface WavingRobot {
fun wave()
}
interface MobileRobot {
fun goToLocation(lat: Double, long: Double)
}
By breaking up the monolithic interface, we’re decoupling moving from waving and separating the responsibilities. If left unattended, interfaces like these can grow over time and cause a lot of headaches later on, splitting them up early will make it much easier in the long run.