SOLID Principles
The SOLID Principles are five principles of Object-Oriented class design. They are a set of rules and best practices to follow while designing a class structure.
Design principles encourage us to create more maintainable, understandable, and flexible software. Consequently, as our applications grow in size, we can reduce their complexity
These five principles help us understand the need for certain design patterns and software architecture in general.
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
S — Single Responsibility
A class should have a single responsibility
If a Class has many responsibilities, it increases the possibility of bugs because making changes to one of its responsibilities, could affect the other ones without you knowing.
A class should have one and only one reason to change, meaning that a class should have only one job.
This principle aims to separate behaviours so that if bugs arise as a result of your change, it won’t affect other unrelated behaviours.
Before SRP
class DataProcessing {
func handle() {
let data = loadData()
let list = parse(data: data)
save(model: list)
}
func loadData() -> Data {
return Data()
}
func parse(data: Data) -> Any {
return ""
}
func save(model: Any) {
//saveData
}
}
In the above, we have DataProcessing class which handles 3 different tasks in a single instance, which means it handles multiple responsibilities, which violated SRP.
Solution: To avoid this basic concept is to handle those 3 tasks in 3 different classes.
After SRP
class DataProcessing {
let restapiHandler: RESTAPIHanler
let dataparseHandler: DataParseHandler
let datastorageHandler: DataStorageHandler
init(restapiHandler: RESTAPIHanler,
dataparseHandler: DataParseHandler,
datastorageHandler: DataStorageHandler
) {
self.restapiHandler = restapiHandler
self.dataparseHandler = dataparseHandler
self.datastorageHandler = datastorageHandler
}
func handle() {
let data = restapiHandler.loadData()
let model = dataparseHandler.parse(data: data)
datastorageHandler.save(model: model)
}
}
class RESTAPIHanler {
func loadData() -> Data {
return Data()
}
}
class DataParseHandler {
func parse(data: Data) -> Any {
return ""
}
}
class DataStorageHandler {
func save(model: Any) {
//saveData
}
}
O — Open-Closed
Classes should be open for extension, but closed for modification
Changing the current behaviour of a Class will affect all the systems using that Class.
If you want the Class to perform more functions, the ideal approach is to add to the functions that already exist NOT change them.
Objects or entities should be open for extension but closed for modification.
This principle aims to extend a Class’s behaviour without changing the existing behaviour of that Class. This is to avoid causing bugs wherever the Class is being used.
Before OCP
class InvoicePayment {
func cashonDelivery(amount: Double) {
//perform
}
func cardPayment(amount: Double) {
//perform
}
func paypalPayment(amount: Double) {
//perform
}
func UPIPayment(amount: Double) {
//perform
}
}
In the above InvoicePayment, we have multiple methods to accept payment but to accept one more payment method we have to modify the class which is not allowed as per the OCP Solid principle.
After OCP
Solution: we have to create a protocol (extension) which can be used in different payment methods
protocol PaymentProtocol {
func makePayment(amount: Double)
}
class cashonDelivery: PaymentProtocol {
func makePayment(amount: Double) {
//perform
}
}
class cardPayment: PaymentProtocol {
func makePayment(amount: Double) {
//perform
}
}
//
class paypalPayment: PaymentProtocol {
func makePayment(amount: Double) {
//perform
}
}
//
class InvoicePayment {
func makePayment(amount: Double, payment: PaymentProtocol) {
payment.makePayment(amount: amount)
}
}
L — Liskov Substitution
If S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program.
If you have a Class and create another Class from it, it becomes a parent and the new Class becomes a child. The child Class should be able to do everything the parent Class can do. This process is called Inheritance.
The child Class should be able to process the same requests and deliver the same result as the parent Class or it could deliver a result that is of the same type.
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
This principle aims to enforce consistency so that the parent Class or its child Class can be used in the same way without any errors.
var greeting = "Hello, playground"
let requestKey: String = "NSURLRequestKey"
// NSError subclass provide additional functionality but don't mess with original class.
// NSError is the Parent Class
// RequestError is the child class
class RequestError: NSError {
var request: NSURLRequest? {
return self.userInfo[requestKey] as? NSURLRequest
}
}
// I forcefully fail to fetch data and will return RequestError.
func fetchData(request: NSURLRequest) -> (data: NSData?, error: RequestError?) {
let userInfo: [String:Any] = [requestKey : request]
return (nil, RequestError(domain:"DOMAIN", code:0, userInfo: userInfo))
}
func willReturnObjectOrError() -> (object: AnyObject?, error: NSError?) {
let request = NSURLRequest()
let result = fetchData(request: request)
return (result.data, result.error)
}
let result = willReturnObjectOrError()
//RequestError
if let requestError = result.error as? RequestError {
requestError.request
}
in this LSP, it allows you to substitute the main class in some or partial manner.
This principle can help you to use inheritance without messing it up.
I — Interface Segregation
Clients should not be forced to depend on methods that they do not use.
A Class should perform only actions that are needed to fulfil its role. Any other action should be removed completely or moved somewhere else if it might be used by another Class in the future.
A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.
This principle aims at splitting a set of actions into smaller sets so that a Class executes ONLY the set of actions it requires.
Before ISP
protocol Vehicle {
func gear()
func interior()
func infotainment()
func sidestand()
}
class Bike: Vehicle {
func gear() {
print("gear")
}
func sidestand(){
print ("sidestand")
}
}
class Car: Vehicle {
func sidestand() {
fatalError("car does not have side stand")
}
func infotainment() {
print("infotainment")
}
}
We must divide our responsibilities, which have an abstract structure, into basic parts.
Now the Bike class will inherit from GearSystem and SideStand, and the Car class from GearSystem, InteriorDesign, InfotainmentSystem.
After ISP
Thus, we do not impose unnecessary responsibility on any class and create a structure suitable for the ISP.
protocol GearSystem {
func gear()
}
protocol InteriorDesign {
func interior()
}
protocol InfotainmentSystem {
func infotainment()
}
protocol SidestandSystem {
func sidestand()
}
class Bike: GearSystem, SidestandSystem {
func gear() {
print("")
}
func sidestand() {
print("")
}
}
class Car: GearSystem, InteriorDesign, InfotainmentSystem {
func gear() {
print("")
}
func interior() {
print("")
}
func sidestand() {
print("")
}
}
D — Dependency Inversion
- High-level modules should not depend on low-level modules. Both should depend on the abstraction.
- Abstractions should not depend on details. Details should depend on abstractions.
This principle says a Class should not be fused with the tool it uses to execute an action. Rather, it should be fused to the interface that will allow the tool to connect to the Class.
It also says that both the Class and the interface should not know how the tool works. However, the tool needs to meet the specification of the interface.
Entities must depend on abstractions, not on concretions. It states that the high-level module must not depend on the low-level module, but they should depend on abstractions.
This principle aims at reducing the dependency of a high-level Class on the low-level Class by introducing an interface.
Before DIP
struct Student {
func study() {
print("working...")
}
}
struct School {
var students: [Student]
func manage() {
students.forEach { student in
student.study()
}
}
}
func run() {
let school = School(students: [Student()])
school.manage()
}
We have a structure Student which has a function study. also, we have another structure School which has a function manage.
We have a run function which takes the Student array as input. everything is good except one and that is the Non-Abstract method School is directly linked with Student.
After DIP
protocol Studies {
func study()
}
struct Student: Studies {
func study() {
print("working...")
}
}
struct School {
var studies: [Studies]
func manage() {
studies.forEach { studies in
studies.study()
}
}
}
func example() {
let school = School(studies: [Student()])
school.manage()
}
So, we created an abstract Studies structure and depend on the Student class to Studies so that the Student structure retains its original functions.
The point is that the School class now expects the array of the abstract struct Studies instead of the array Student. Thus, we have linked the dependency of the School structure to an abstract module. This means that the School structure has come to the point where it can run any structure depend to the Studies module.
Happy Coding 🙂
Discover more from mycodetips
Subscribe to get the latest posts sent to your email.