SOLID Principles in Swift
SOLID represents 5 principles of object-oriented programming:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation
- Dependency Inversion
Single Responsibility Principle
It states that every module should have only one responsibility and reason to change. SRP starts with small concrete and specific cases such as a class and/or an object having only one purpose and being used only for one thing. This principle helps you to keep your classes as clean as possible.
Let's have an example
class Handler {
func handle() {
let data = requestDataToAPI()
let array = parse(data: data)
saveToDatabase(array: array)
}
private func requestDataToAPI() -> Data {
// Network request and wait the response
}
private func parseResponse(data: Data) -> [String] {
// Parse the network response into array
}
private func saveToDatabase(array: [String]) {
// Save parsed response into database
}
}
from an above example, Handler class perform multiple responsibilities like making a network call, parsing the response and saving into the database.
You can solve this problem moving the responsibilities down to little classes.
class Handler {
let apiHandler: APIHandler
let parseHandler: ParseHandler
let databaseHandler: DBHandler init(apiHandler: APIHandler, parseHandler: ParseHandler, dbHandler: DBHandler) {
self.apiHandler = apiHandler
self.parseHandler = parseHandler
self.dbHandler = dbHandler
} func handle() {
let data = apiHandler.requestDataToAPI()
let array = parseHandler.parse(data: data)
databaseHandler.saveToDatabase(array)
}
} class NetworkHandler {
func requestDataToAPI() -> Data {
// Network request and wait the response
}
}class ResponseHandler {
func parseResponse(data: Data) -> [String] {
// Parse the network response into array
}
}class DatabaseHandler {
func saveToDatabase(array: [String]) {
// Save parsed response into database
}
}
Open/Closed principle
In simple term, Open for extension but closed for modification.
- Open for extension: You should be able to extend or change the behaviors of a class without efforts.
- Closed for modification: You must extend a class without changing the implementation.
We have Logger class which iterate Car array and prints details of cars
class Car {
let name: String
let color: String init(name: String, color: String) {
self.name = name
self.color = color
} func printDetails() -> String {
return "I have \(self.color) color \(self.name)."
}
}class Logger {
func printData() {
let cars = [ Car(name: "BMW", color: "Red"),
Car(name: "Audi", color: "Black")] cars.forEach { car in
print(car.printDetails())
}
}
}
If you want to add the possibility to print also the details of a new class, we should change the implementation of printData every time we want to log a new class which breaking open close principle.
class Bike {
let name: String
let color: String init(name: String, color: String) {
self.name = name
self.color = color
} func printDetails() -> String {
return "I have \(self.name) bike of color \(self.color)."
}
}class Logger {
func printData() {
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())
}
}
}
We can solve this problem creating a new protocol Printable which will be implemented by the classes to log. Finally, printData() will print an array of Printable.
In this way, we create a new abstract layer between printData() and the class to log, allowing the print of other classes like Bike and without changing the printData() implementation.
protocol Printable {
func printDetails() -> String
}class Car: Printable {
let name: String
let color: String init(name: String, color: String) {
self.name = name
self.color = color
} func printDetails() -> String {
return "I have \(self.color) color \(self.name)."
}
}class Bike: Printable {
let name: String
let color: String
init(name: String, color: String) {
self.name = name
self.color = color
} func printDetails() -> String {
return "I have \(self.name) bike of color \(self.color)."
}
}class Logger {
func printData() {
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())
}
}
}
Liskov Substitution Principle
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
This principle can help you to use inheritance without messing it up.
let requestKey: String = "NSURLRequestKey"
// NSError subclass provide additional functionality but don't mess with original 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
}
Interface Segregation Principle (ISP)
It states that clients should not be forced to implement interfaces they don’t use. Instead of one fat interface, many small interfaces are preferred based on groups of methods, each one serving one submodule.
//We start with the protocol GestureProtocol with a method didTap:protocol GestureProtocol {
func didTap()
} //After some time, you have to add more gestures to the protocol
protocol GestureProtocol {
func didTap()
func didDoubleTap()
func didLongPress()
}
If SuperButton implements all the methods which it needs.
class SuperButton: GestureProtocol {
func didTap() {
// Single tap operation
} func didDoubleTap() {
// double tap operation
} func didLongPress() {
// long press operation
}
}//But if implement Double Tab Button it implement all the action
class DoubleTapButton: GestureProtocol {
func didTap() {
// Single tap operation
} func didDoubleTap() {
// double tap operation
} func didLongPress() {
// long press operation
}
}
Double tab button un-necessary implement didTap() and didLongPress() action. Here it breaks the Interface Segregation Principle. We can solve the problem using little protocols instead of a big one.
protocol TapProtocol {
func didTap()
}protocol DoubleTapProtocol {
func didDoubleTap()
}protocol LongPressProtocol {
func didLongPress()
}class SuperButton: TapProtocol, DoubleTapProtocol, LongPressProtocol {
func didTap() {
// Single tap operation
}func didDoubleTap() {
// double tap operation
}func didLongPress() {
// long press operation
}
}class DoubleTapButton: DoubleTapProtocol {
func didDoubleTap() {
// double tap operation
}
}
Dependency Inversion Principle
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)
class FileSystemManager {
func save(string: String) {
// Open a file
// Save the string in this file
// Close the file
}
}class Handler {
let fileManager = FilesystemManager()
func handle(string: String) {
fileManager.save(string: string)
}
}
FileSystemManager is the low-level module and it’s easy to reuse in other projects. The problem is the high-level module Handler which is not reusable because is tightly coupled with FileSystemManager. We should be able to reuse the high-level module with different kind of storages like a database, cloud, and so on.
We can solve this dependency using protocol Storage. In this way, Handler
can use this abstract protocol without caring for the kind of storage used. With this approach, we can change easily from a filesystem to a database.
protocol Storage {
func save(string: String)
}class FileSystemManager: Storage {
func save(string: String) {
// Open a file in read-mode
// Save the string in this file
// Close the file
}
}class DatabaseManager: Storage {
func save(string: String) {
// Connect to the database
// Execute the query to save the string in a table
// Close the connection
}
}class Handler {
let storage: Storage // Storage types
init(storage: Storage) {
self.storage = storage
}
func handle(string: String) {
storage.save(string: string)
}
}
Dependency Inversion Principle is very similar to the Open-Closed Principle the approach to use, to have a clean architecture, is decoupling the dependencies. You can achieve it to abstract layers
References:
Thanks for reading 😊