Ads

Tuesday, 9 November 2021

SOLID Principles

SOLID principles are the basic essential thing to know for every developer before he/she starts coding in any IDE.

Here is the full form

S - Single Responsibility Principle

- While developing we should always try to achieve high level cohesion.

Ex: Lets say, we have a class Square and there are four methods which is as follows

class Rectangle {

func caluclateArea() { ... }

func caluclatePerimeter() { ... }

func draw() { ... }

func rotate() { ... }

}

If we observe the above class, we can say that 'caluclateArea' and 'caluclatePerimeter' are related and 'draw' and 'rotate' are related. But 'caluclateArea' and 'draw' are not related to anything. Therefore we can say the level of Cohesion is low.

So what can we do here is, we can splint the code as follows.

class Rectangle {

func caluclateArea() { ... }

func caluclatePerimeter() { ... }

}

class RectangleUI {

func draw() { ... }

func rotate() { ... }

}

If we observe above, I have divided the code into Rectangle and RectangleUI. 

RectangleUI handle the responsibility of UI update and Rectangle handles the responsibility of calculation. Now we can say that the level of Cohesion is high.

- While developing the code we should always try to do loose coupling.

Definitions

1. Cohesion - Cohesion is the degree to which the various parts of the software components are related.

2. Coupling - Coupling is defined as the level of inter dependency between various software components.

Notes:

Software always changes with the time. And time is money. If any software component holds the single responsibility principle, changes can made easy so always consider the below points before starting developing anything.

1.  The changes required for the software component should have only one reason to change / modify.

2. Every Software component should handle one and only responsibility for the same reason above.


O - Open Close Principle

- Software component should be closed for modification, but open for extension.

The meaning of closed for modification, but open for extension is as follows.

1. New features adding to the software component, shouldn't have to modify the existing functionality.(Which we can say as "Closed for modification").

2. A software component should be extendable to add a new feature or to add a new behaviour to it.

Ex:

class Flat {

let ownerName: String

let flatNumber: Int

init(ownerName: String, flatNumber: Int) {

self. ownerName = ownerName

self.flatNumber = flatNumber

}

class Apartment {

var flats: [Flat]

init(flats: [Flat]) {

self.flats = flats 

func addFlat(flat: Flat)) {

flats.append(flat)

}

Usage as follows:

let app = Apartment(flats: [])

app.append(flat: <someValue>)

Now If I introduce new type of Flats, lets say NewFlats. The above code will not work.

If we observe the above code, if we change the name of the Flat to NewFlats or want to introduce new functionality, we have to modify the code in the Apartment classes. Which means the code should be open for modification, therefore it doesn't follow the OCP.

Therefore now I am changing the above code as follows to follow OCP.

protocol ResidentialFlats {}

class Flat: ResidentialFlats {

let ownerName: String

let flatNumber: Int

init(ownerName: String, flatNumber: Int) {

self. ownerName = ownerName

self.flatNumber = flatNumber

}

class Apartment {

var flats: [ResidentialFlats]

init(flats: [ResidentialFlats]) {

self.flats = flats 

func addFlat(flat: ResidentialFlats)) {

flats.append(flat)

}

Usage as follows:

let app = Apartment(flats: [])

app.append(flat: <someValue type 'Flat'>)


Now If I introduce new type of Flats

 class NewFlat: ResidentialFlats {

let ownerName: String

let flatNumber: Int

let isCornerFlat: Bool

}

Usage as follows:

let app = Apartment(flats: [])

app.append(flat: <someValue type 'NewFlat'>)

The above code still works without any issue. We can say that now 'Apartment' class is closed for modification and open for extension.

L - Liskov Substitution Principle

- Objects should be replaceable with their  subtypes without effecting  the correctness of the program.

Lets explain with an example.

class Rectangle {

var width: Int

var height: Int

 init(width: Int, height: Int) {

self.width = width

self.height = height

}  

func area() -> Int{

return  width * height

}

}

class Square: Rectangle {

ovveride var width: Int {

didSet {

super.height = width

ovveride var height: Int {

didSet {

super. width = height

}

}

let square = Square(width: 10, height: 10)

let rectangle: Rectangle = square

rectangle.height = 7

rectangle.widht = 5

rectangle.area() // This value will be 25 instead 35


What is happening here is, we are creating Rectangle attaching the reference of the Square, which is Polymorphism. So the actual reference will be pointed to the Square, therefore when we call the area(), the value we get will be the result of Square instead of Rectangle.

So according to the LSP, even though we change the subclass type, it should not effect the program. To achieve this, we have to modify the above code as follows.


protocol Shape {

func area() -> Int

}

class Rectangle: Shape {

var width: Int

var height: Int

 init(width: Int, height: Int) {

self.width = width

self.height = height

}  

func area() -> Int{

return  width * height

}

}

class Square: Shape {

var side: Int 

init(side: Int) {

self.side = side

}   

func area() -> Int {

return  side * side

}

let square: Shape = Square(side: 10)

//let rectangle: Rectangle = square// This will throw compile time error, because Rectangle and Square are 2 different types.

So we can use it as 

let rectangle: Shape = Rectangle(width: 10, height: 12)

print(rectangle.area())

I - Interface Segregation Principle

- The name it self suggest the interface segregation. No client should be forced to use unwanted methods.

Example:

protocol XeroxMachine {

func print()

func scan()

func fax()

}

class MultiFunctionMachine: XeroxMachine {

func print() { ... } 

func scan() { ... }

func fax() { ... }

}

class PrintAndScanMachine: XeroxMachine {

func print() { ... } 

func scan() { ... }

func fax() {// Empty}

}

class FaxMachine: XeroxMachine {

func print() { // Empty  } 

func scan() { // Empty }

func fax() { ... }

}

If we observe the above example, FaxMachine class have the empty methods scan and print because we are using XeroxMachine protocol. Similarly different classes uses different mandatory methods that are required and we have some empty functions. If any other classes call this empty methods, it doesn't do anything. 

According to the ISP, we shouldn't have these kind of empty methods, therefore I am going to change the above code as follows.

protocol PrintScanProtocol {

func print()

func scan()

}

protocol FaxProtocol {

func fax()

}

class MultiFunctionMachine: PrintScanProtocol,  FaxProtocol {

func print() { ... } 

func scan() { ... }

func fax() { ... }

}

class PrintAndScanMachine: PrintScanProtocol {

func print() { ... } 

func scan() { ... }

}

class FaxMachine: FaxProtocol {

func fax() { ... }

}

Identification of Violations:

1. Fat Protocols/ Interface -> Follow SRP, every component should have one and only one responsibility.

2. Low cohesion ->  Again Follow SRP, try to achieve high cohesion.

3. Empty methods -> Again SRP, loose coupling.

D - Dependency Inversion Principle

- High-level modules doesn't depend on low level modules. Both should depend on abstraction.
- Abstraction doesn't depend on details, but details should depend on the abstraction.

Ex:

struct Product {
var id
var name
}
class ProductCatalog {
func getProducts() -> [Products] {
// Returns the products having in the catalog
}

class ProductListScreen {
var products: [Products]?
var productCatalog: ProductCatalog?

func getProducts() {
productCatalog = ProductCatalog()
products = productCatalog.getProducts()
}
}

If we observe the above code, the high level code will be ProductListScreen (because it is directly interact with the UI) and ProductCatalog is the low level code. In the above ProductListScreen directly depends  on ProductCatalog for fetching the products.

But according to the DIP, high level and low level should depend on the abstraction, not on anyone of them. So now I am going to change the code as follows.

protocol ProductProtocol {
var id: String { get set }
var name: String { get set }
}

protocol ProductCatalogProtocol {
func getProducts() -> [ProductProtocol]

struct Product: ProductProtocol {
var id: String
var name: String
}

class ProductCatalog: ProductCatalogProtocol {
func getProducts() -> [ProductProtocol] {
// Returns the products having in the catalog
}

class ProductListScreen {
var products: [ProductProtocol]?
var productCatalog: ProductCatalogProtocol?

init(....) {

....

}

func getProducts() {
products = productCatalog.getProducts()
}
}

If we observe the above code, the high level code ProductListScreen and the low level code ProductCatalog depends on ProductCatalogProtocol which is an abstraction.

 Here are the main terminologies which we will use to achieve DIP.
1. Dependency Injection
2. Inversion of control

No comments:

Post a Comment

SOLID Principles

SOLID principles are the basic essential thing to know for every developer before he/she starts coding in any IDE. Here is the full form S ...