Solid Principles - Kotlin Flashcards

1
Q

What is SOLID Principles ?

A

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
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

S - Single Responsibility Principle

A

“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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

Single Responsibility Principle Example 1

A

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

}

}

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

Single Responsibility Principle Example 2

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

Single Responsibility Principle Example 3

A

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)

}

}

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

O — Open Closed Principle

A

“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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

Open Closed Principle Example 1

A

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") }
}**
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

Open Closed Principle Example 2

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

Open Closed Principle Example 3

A

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”

}

}

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

L — Liskov Substitution Principle

A

“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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

Liskov Substitution Principle Example 1

A

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”)
}
}

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

Liskov Substitution Principle Example 2

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

Liskov Substitution Principle Example 3

A

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

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

I — Interface Segregation Principle

A

“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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

Interface Segregation Principle Example 1

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

Interface Segregation Principle Example 2

A

So let’s think that we have an animal interface. And this interface will have methods about behaviors which animals can do. Thus, animals can use this interface to behave.

**interface Animal{
 fun eat()
 fun sleep()
 fun fly()
}**
**class Cat: Animal{
 override fun eat() {
 println("Cat is eating fish")
 }**

override fun sleep() {
println(“Cat is sleeping on its owner’s bed”)
}

override fun fly() {
TODO(“Not yet implemented”) // Cats can’t fly
}
}

**class Bird: Animal{
 override fun eat() {
 println("Bird is eating forage")
 }**

override fun sleep() {
println(“Bird is sleeping in the nest”)
}

override fun fly() {
println(“Bird is flying so high”)
}
}

As you see, some animals can’t fly such as cat. When we will create classes for these animals, it will be unnecessary to implement fly method.
To fix that issue, we will create new interface called as FlyingAnimal. We will remove fly method from Animal interface and we will add it to out new interface.

**interface Animal{
 fun eat()
 fun sleep()
}**
**interface FlyingAnimal{
 fun fly()
}**
**class Cat: Animal{
 override fun eat() {
 println("Cat is eating fish")
 }**

override fun sleep() {
println(“Cat is sleeping on its owner’s bed”)
}
}

**class Bird: Animal, FlyingAnimal{
 override fun eat() {
 println("Bird is eating forage")
 }**

override fun sleep() {
println(“Bird is sleeping in the nest”)
}

override fun fly() {
println(“Bird is flying so high”)
}
}

Note: If we would like to we can implement Animal interface in FlyingAnimal interface. So, we won’t need to implement two interfaces for classes which animals can fly such as Bird.

17
Q

Interface Segregation Principle Example 3

A

A very good example of violating Interface segregation principle is shown as below:

interface Person {

fun code()

fun cook()

fun build()

fun eat()

}

class DevPerson: Person {

override fun code() {doCode()}

override fun cook() {}

override fun build() {}

override fun eat(){doEat()}

}

class CheftPerson: Person {

override fun code() {}

override fun cook() {doCook()}

override fun build() {}

override fun eat(){doEat()}

}

class EngineerPerson: Person {

override fun code() {}

override fun cook() {}

override fun build({doBuild()}

override fun eat(){doEat()}

}

As shown in above, for EngineerPerson class, only build and eat function is being implemented; whereas code function and cook function has no functionality. To resolve this, we should create multiple interfaces, such as:

interface Person {

fun eat()

}

interface DevPerson: Person {

fun code()

}

interface CheftPerson: Person {

fun cook()

}

interface EngineerPerson: Person {

fun build()

}

class DevPersonImpl: DevPerson {

override fun code() {doCode()}

override fun eat(){doEat()}

}

class CheftPersonImpl: CheftPerson {

override fun cook() {doCook()}

override fun eat(){doEat()}

}

class EngineerPersonImpl: EngineerPerson {

override fun build() {doBuild()}

override fun eat(){doEat()}

}

18
Q

D — Dependency Inversion Principle

A

“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”.

This principle suggests that “classes should depend on abstraction but not on concretion”.

This means that if you use a class insider another class, this class will be dependent on the class injected.

19
Q

Dependency Inversion Principle Example 1

A

Dependency Inversion Principle tells us about the coupling between the different classes or modules.

Higher-level classes should not be dependent to lower-level classes. Both should be dependent to abstractions. It is based on removing the dependency with the interface.

The dependence of a class or method on other classes that use it should be minimized. Changes made in the child classes should not affect parent classes.

Let’s think that we need to develop a mobile application for both Android and iOS. To do that, we need an Android Developer and an iOS Developer. These classes will have a method to develop a mobile application by using their own platform and programming language.

**class AndroidDeveloper{
 fun developMobileApp(){
 println("Developing Android Application by using Kotlin")
 }
}**
**class IosDeveloper{
 fun developMobileApp(){
 println("Developing iOS Application by using Swift")
 }
}**

fun main(){
val androidDeveloper = AndroidDeveloper()
val iosDeveloper = IosDeveloper()

androidDeveloper.developMobileApp()
iosDeveloper.developMobileApp()
}

To fix the problem in here, we can create an interface called as MobileDeveloper. AndroidDeveloper and IosDeveloper classes with implement this interface.

If we want to store some different datas for each developer type, we can use this principle. Also, maybe we want to separate mobile services for Android Developers. To do that, we can create different methods for Android Developer but developing mobile application will be same for both Android and iOS developers. So, we should have an interface for same operations.

**interface MobileDeveloper{
 fun developMobileApp()
}**

class AndroidDeveloper(var mobileService: MobileServices): MobileDeveloper{
override fun developMobileApp(){
println(“Developing Android Application by using Kotlin. “ +
“Application will work with ${mobileService.serviceName}”)
}
enum class MobileServices(var serviceName: String){
HMS(“Huawei Mobile Services”),
GMS(“Google Mobile Services”),
BOTH(“Huawei Mobile Services and Google Mobile Services”)
}
}

**class IosDeveloper: MobileDeveloper{
 override fun developMobileApp(){
 println("Developing iOS Application by using Swift")
 }
}**

fun main(){
val developers = arrayListOf(AndroidDeveloper(AndroidDeveloper.MobileServices.HMS), IosDeveloper())
developers.forEach { developer ->
developer.developMobileApp()
}
}

20
Q

Dependency Inversion Principle Example 2

A

Let’s explain it with kotlin.

class BeerBot {

fun dispenseBeer() {

println(“Dispensing 🍺”)

}

}

class WineBot {

fun dispenseWine() {

println(“Dispensing 🍷”)

}

}

class RoboPub {

private val wineBot = WineBot()

private val beerBot = BeerBot()

fun dispenseDrinks(){

wineBot.dispenseWine()

beerBot.dispenseBeer()

}

}

So what we have here is our higher level class RoboPub depending on our low level, concrete classes WineBot and BeerBot, causing this class to violate the Dependency Inversion Principle. In addition to this, dispenseBeer and dispenseWine are details coupled with their classes, meaning that we’d need to change this before we add an abstraction. What if we want our pub to have robots that dispense other drinks? In its current state, we’d have to change the RoboPub class, which violates the Open-Closed Principle. Let’s refactor this.

interface DrinksBot {

fun dispense()

}

class BeerBot: DrinksBot {

override fun dispense() {

println(“Dispensing 🍺”)

}

}

class WineBot: DrinksBot{

override fun dispense() {

println(“Dispensing 🍷”)

}

}

What we’ve done here is introduce an abstraction DrinksBot and refactored our concrete classes to implement this. These classes are now interchangeable, we can also add more implementations with ease, similar to the Open-Closed Example. We still have one refactor left:

class RoboPub(private val drinksBots: List) {

fun dispenseDrinks() {

drinksBots.forEach { it.dispense() }

}

}

So now, rather than the RoboPub creating instances of the robots, we use Dependency Injection to provide the RoboPub with the robots it needs to function. Notice we’re using our abstraction DrinksBot rather than a concrete class, therefore completely decoupling our high level RoboPub from the concrete classes and their details. This means we’d be able to add all kinds of implementations and our RoboPub wouldn’t change at all.

21
Q

Dependency Inversion Principle Example 3

A

An example of violating Dependency inversion principle:

interface Yerba{

fun getBrand(): String

}

class Kurupi: Yerba {

override fun getBrand() {

return “Yerba Kurupi”

}

}

class YerbaTest(val yerba: Kurupi) {

fun getYerbaBrand() {

return yerba.getBrand()

}

}

The example above violates first rule of dependency inversion principle. High-level modules should not understand the actual implementation of low-level modules. They should only communicate through abstractions. To resolve this, we should pass in the interface instead of the class.

interface Yerba{

fun getBrand(): String

}

class Kurupi : Yerba{

override fun getBrand() {

return “Yerba Kurupi”

}

}

class YerbaTest(val yerba: Yerba) {

fun getYerbaBrand() {

return yerba.getBrand()

}

}

22
Q

Benefits and Drawbacks of SOLID Principles

A

Benefits of SOLID Principles:

This approach will lead to cleaner, more manageable code that can be easily unit tested, which in turn can allow for rapid development as it allows a more methodical approach.

These rules will greatly benefit any project and should be heavily considered, along with other agile methodologies.

Writing code in SOLID principles manner will make our software more readable, understandable, flexible and maintainable.

Drawbacks of SOLID Principles:

It may make life easier for developers if it is included from the outset of a project. But trying to bend existing projects to fit this approach could potentially be near impossible.