SOLID Principles: To Develop Swift/iOS Applications in right way

Improve Coding in iOS App Development with SOLID Principles and Swift

Robert C. Martin introduced the SOLID principles of development in 2000 in a paper named Design Principles and Design Patterns. The author’s approach to bringing out these principles were inspired by rotting design.

Identify the rotting in software by four symptoms;

  • Rigidity: Difficulty to change
  • Fragility: Tendency of the software to break
  • Immobility: Software cannot be reused
  • Viscosity: Changing the software design leads to issues, and the development environment becomes slow & inefficient.

Due to these symptoms, software underperforms and becomes obsolete, which is not ideal for any developer. Hence, Robert C. Martin introduced the SOLID principles for object-oriented software development.

The sections ahead will help you understand the core tenets of the SOLID principles and help you understand their utility in software development.

What are SOLID Principles?

The acronym SOLID stands for;

  • Solid Responsibility Principle (S or SRP)
  • Open/Closed Principle (O or OCP)
  • Liskov Substitution Principle (L or LSP)
  • Interface Segregation Principle (I or ISP)
  • Dependency Inversion Principle (D or DIP)

Single Responsibility Principle

In the words of Robert C. Martin, “a class must only have one and one reason to change.” In other words, when you define a class or module, it should be responsible for only one part of the software.

For example;

classHandler {
 funchandle() {
let data = requestDataToAPI()
let array = parse(data: data)
        saveToDatabase(array: array)
	}
 privatefuncrequestDataToAPI() ->Data {
// Network request and wait the response
	}
 privatefuncparseResponse(data: Data) -> [String] {
// Parse the network response into array
	}
 privatefuncsaveToDatabase(array: [String]) {
// Save parsed response into database
	}
}

In the example above, the Handler class performs many tasks like initiating a network call, processing the response, and saving the data to the database. You can fix this issue by breaking down the responsibilities into smaller classes. Follow the example below.

classHandler {
letapiHandler: APIHandler
letparseHandler: ParseHandler
letdatabaseHandler: DBHandler
 init(apiHandler: APIHandler, parseHandler: ParseHandler,dbHandler: DBHandler) {
self.apiHandler = apiHandler
self.parseHandler = parseHandler
self.dbHandler = dbHandler
    }	
 funchandle() {
let data = apiHandler.requestDataToAPI()
let array = parseHandler.parse(data: data)
    	databaseHandler.saveToDatabase(array)
    }
}
 classNetworkHandler {
funcrequestDataToAPI() ->Data {
// Network request and wait the response
    }
}
 classResponseHandler {
funcparseResponse(data: Data) -> [String] {
// Parse the network response into array
    }
}
 classDatabaseHandler {
funcsaveToDatabase(array: [String]) {
// Save parsed response into database
    }
}

Using this principle, developers can improve their coding experience by making the code easier to test and maintain. Furthermore, following SRP, the developers can implement the code easily while shielding it from the probable side-effects of code changes in the future.

Open/Close Principle

The Open/Close principle dictates that any developer should be able to extend any class without leading to a change in its behavior.

To put it another way, it’s open for expansion but closed for modification.

  1. Open to expansion: A class’s behaviors should be able to be extended or changed without difficulty.
  1. No Modifications allowed: This means that a developer cannot change the source code of the software.

We have a Logger class that iterates through the Car array and publishes information about the automobiles.

classCar {
letname: String
letcolor: String
 init(name: String, color: String) {
self.name = name
self.color = color
	}
 funcprintDetails() ->String {
return"I have \(self.color) color \(self.name)."
	}
}
classLogger {
funcprintData() {
let cars = [ Car(name: "BMW", color: "Red"),
Car(name: "Audi", color: "Black")]
     	cars.forEach { car in
print(car.printDetails())
     	}
 	}
}

If you wish to add the ability to print the information of a new class, you’ll need to alter the printData implementation every time you want to log a new class that violates the open-close principle.

classBike {
letname: String
letcolor: String
 init(name: String, color: String) {
self.name = name
self.color = color
	}
 funcprintDetails() ->String {
return"I have \(self.name) bike of color \(self.color)."
	}
}
classLogger {
funcprintData() {
let cars = [ Car(name: "BMW", color: "Red"),
                 	Car(name: "Audi", color: "Black")]
     	cars.forEach { car in
         	print(car.printDetails())
     	}
 let bikes = [ Bike(name: "Homda CBR", color: "Black"),
Bike(name: "Triumph", color: "White")]
    	bikes.forEach { bike in
print(bike.printDetails())
     	}
 	}
}

This problem will be solved by developing a new protocol Printable, which will be implemented by the log classes

Finally, printData() returns an array of Printable objects.

We establish a new abstract layer between printData() and the class to log in this fashion, allowing us to print other classes like Bike without changing the printData() implementation.

protocolPrintable {
funcprintDetails() ->String
}
 classCar: Printable {
letname: String
letcolor: String
 init(name: String, color: String) {
self.name = name
self.color = color
    }
 funcprintDetails() ->String {
return"I have \(self.color) color \(self.name)."
    }
}
 classBike: Printable {
letname: String
letcolor: String
 init(name: String, color: String) {
self.name = name
self.color = color
    }
 funcprintDetails() ->String {
return"I have \(self.name) bike of color \(self.color)."
    }
}
 classLogger {
funcprintData() {
let vehicles: [Printable] = [Car(name: "BMW", color: "Red"),
Car(name: "Audi", color: "Black"),
Bike(name: "Honda CBR", color: "Black"),
Bike(name: "Triumph", color: "White")]
    	vehicles.forEach { vehicle in
print(vehicle.printDetails())
    	}
    }
}

You must feel confused after reading the example and description of this principle. Because on the one hand, we are saying that we should not modify the code, but at the same time, we want to extend the class’s functionality.

But you will get a hold of it after some practice. The trick is to change the code by using abstractions. You can use inheritances or interfaces that give way to the polymorphic substitution that helps comply with this principle.

Liskov Substitution Principle

This principle is an extension of the Open/Close Principle, and it is fairly difficult to comprehend. The rationale behind this principle is that child classes should never break any type of definition of the parent class.

It means that new derived classes must expand the base classes without affecting the behavior of the base classes. In other words, a subclass should override the parent class methods in a way that does not break the functionality of the base class from the client’s perspective.

Interface Segregation Principle

While explaining this principle, Robert C. Martin said that the clients or end-users should not be able to implement interfaces they will not use. To ensure this, developers need to make fine-grained interfaces specific to the client’s requirements.

Hence, it is preferable to have multiple client-specific interfaces (protocols) rather than a single general interface. In addition, it indicates that a client would not have to implement methods that they do not use.

For instance, we can design an animal interface that includes displacement methods:

protocolAnimalProtocol {
funcwalk()
funcswim()
funcfly()
}
 structAnimal: AnimalProtocol {
funcwalk() {}
funcswim() {}
funcfly() {}
}
 structWale: AnimalProtocol {
funcswim() {
// Wale only needs to implement this function
// All the other functions are irrelavant
	}
 funcwalk() {}
 
funcfly() {}
}

Despite the fact that Wale follows the protocol, it does not implement two methods. The approach is to create three different interfaces (protocols), one for each method.

protocolWalkProtocol {
funcwalk()
}
 protocolSwimProtocol {
funcswim()
}
 protocolFlyProtocol {
funcfly()
}
 structWale: SwimProtocol {
funcswimm() {}
}
 structCrocodile: WalkProtocol, SwimProtocol {
funcwalk() {}
funcswimm() {}
}

As a developer, your motive will be to build a single-purpose extensive interface. However, this is not the right way to move forward, especially when you want to implement SWIFT principles. The interface segregation principle demands that developers build multiple client-specific interfaces.

Dependency Inversion Principle

The DI principle aims to decouple software modules from each other. It asserts that any developer must depend on abstractions and not on corrections. Also, high-level modules need not rely on low-level modules; rather, they must depend on Abstractions.

Take, for example, the Handler class, which stores a string to the filesystem. Internally, it invokes FilesystemManager, which controls how the string is saved in the filesystem:

classHandler {
 letfm = FilesystemManager()
 funchandle(string: String) {
fm.save(string: string)
	}
}
 classFilesystemManager {
 funcsave(string: String) {
// Open a file
// Save the string in this file
// Close the file
	}
}

FilesystemManager module can be reused in several projects because it is low-level. The issue is with the high-level module Handler, which isn’t reusable due to its close relationship with FilesystemManager.

We should be able to reuse the high-level module with various types of storage, such as a database or the cloud.

You can resolve this dependency using the Storage protocol. As a result, Handler can use this abstract protocol regardless of the storage type. We can easily switch from a filesystem to a database using this method:

classHandler {
 letstorage: Storage
 init(storage: Storage) {
self.storage = storage
	}
 funchandle(string: String) {
storage.save(string: string)
	}
}
 protocolStorage {
 funcsave(string: String)
}
 classFilesystemManager: Storage {
 funcsave(string: String) {
// Open a file in read-mode
// Save the string in this file
// Close the file
	}
}
 classDatabaseManager: Storage {
 funcsave(string: String) {
// Connect to the database
// Execute the query to save the string in a table
// Close the connection
	}
}

Conclusion

To recap the five principles of SOLID;

  • (S) Each class should have only one responsibility.
  • (O) If changes are required, the existing class should be extended rather than modified.
  • (L) Extending or inheriting a child/derived class should not affect the functionality of the parent or base class.
  • (I) The Interface or should have the minimum information required by the client.
  • (D) Class A should not rely on Class B and vice versa; It is also possible to loosely connect both

Implementing these SOLID principles in iOS development will make the solution easier to maintain, scale, test, and reuse. Plus, you can improve the quality of your code and its result.

Coding is a tough exercise, but if you know how to implement SOLID principles your work will become easier. If you want to build software that follows the SOLID principles and can help you win more business, let us know.

At DEV Information Technology Ltd., we have the right experience and skills to build the best possible solution for your ios application development requirements.