The S in SOLID - Swift Edition

The S in SOLID - Swift Edition

ยท

5 min read

Have you ever heard a software developer go on and on about the SOLID principles and how they can potentially end all of the world's problems? And have you ever wondered what on earth these are?

Well, wait no more.

Today we will break down each of the SOLID principles with code examples and also attempt to break down their definitions.

What are the SOLID principles in software development?

SOLID is an acronym for the first five object-oriented design (OOD) principles by Robert C. Martin.

S - Single Responsibility Principle

O - Open/Closed Principle

L - Liskov Substitution Principle

I - Interface Segregation Principle

D - Dependency Inversion Principle

I will be dedicating one article to each of the above principles so that we can get into the nitty-gritty of each of them, and today it is the turn of the Single-Responsibility Principle.

What is the Single-Responsibility Principle?

The Single-responsibility Principle (SRP) states:

A class should have one and only one reason to change, meaning that a class should have only one job.

Let's first look at (what I think is) a suitable analogy to illustrate the concept and then apply it to code.

Assume that you are a software engineer at a fast-paced startup where along with doing the development, design and release of the software features, you ALSO handle customer support and collect product requirements from customers and stakeholders.

Phew! Sounds like a nightmare, doesn't it?

Well, that's exactly what classes feel when they are given multiple responsibilities.

But let's say you're the kind of employee that is capable of actually executing these tasks and have carved out a routine for yourself in a way that you're able to juggle all your responsibilities without mistake.

That's how some classes function too. Class representations are more than capable of handling all the responsibilities that they are supposed to. Until they aren't.

Now imagine a scenario where you, as software engineer of aforementioned fast-paced company, are asked to change the routine in which you have been carrying out your multiple responsibilities. More often than not, this disruption of your routine is going to have a cascading effect and you are probably not going to be able execute any of your tasks with the accuracy and efficiency with which you used to.

The same happens to these class representations as well. Any modification to any of the responsibilities of the class would cause so many unknown side effects in the class that you simply cannot predict. And you would probably end up disrupting unrelated features that worked just fine to begin with.

This is called tight coupling. And we know that loose coupling is the way to go if we want to build software that is robust, resilient and open to change.

Here is an example of a class that has more than one responsibility :

// MODEL
struct EmployeeModel: Codable {
    var id: String
    var name: String
    var age: Int
}

// VIEWCONTROLLER
class EmployeeListViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    let employees = [EmployeeModel]()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.delegate = self
        self.tableView.dataSource = self
    }

    func getEmployee() {
       guard let url = URL(string: "") else { return }
       URLSession.shared.dataTask(with: URLRequest(url: url)) { (data, response, error) in
            do {
                let result = try JSONDecoder().decode(EmployeeModel.self, from: data!)
                print(result)
            } catch {
                print(error)
            }
        }.resume()
    }
}

extension EmployeeListViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return employees.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
}

Here we see that the EmployeeListViewController has four responsibilities :

  • Setting up the UITableView property
  • Fetching a list of employees from an API
  • Decoding the API response into a usable model
  • Rendering the list of Employees in the tableView

It would be fair to say that setting up of the tableView and rendering the list of Employees within it are closely related to the same cause. The cause of showing the list of employees on the screen.

And it would be fair to say that the fetching of content from an API and decoding its response to a usable model are also related to that same cause. The cause of sourcing data.

But the responsibility of knowing how to and when to fetch the content of that tableView should not be part of the list-rendering component.

Hence we can extract the data fetching logic into its own separate class which can be referenced by the EmployeeListViewController and invoked whenever required.

class HTTPClient {
    func getDataFromApi<T: Codable>(url: URL, of type: T.Type, completion: @escaping (T) -> Void) {
        URLSession.shared.dataTask(with: URLRequest(url: url)) { (data, response, error) in
            do {
                let result = try JSONDecoder().decode(T.self, from: data!)
                completion(result)
            } catch {
                print(error)
            }
        }.resume()
    }
}

struct EmployeeModel: Codable {
    var id: String
    var name: String
    var age: Int
}

class EmployeeListViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!

    let employees = [EmployeeModel]()
    let httpClient: HTTPClient = HTTPClient()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.delegate = self
        self.tableView.dataSource = self
    }

    func getEmployee() {
        self.httpClient.getDataFromApi(url: URL(string: "")!, of: EmployeeModel.self) { employee in
            print(employee)
        }
    }
}

extension EmployeeListViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return employees.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
}

So now any logical changes made to, let's say, the caching policies or the decoding strategies can be made in an isolated manner to just the HTTPClient class without affecting any of its depending classes.

How do you identify if you're violating the SRP?

Single responsibility is about having a single reason to change. In other words, "Gather together those things that change for the same reason, and separate those things that change for different reasons." (If the methods change for different reasons, they should not be together. Otherwise, keep them together for cohesion.)

AAAAAAAND we are done!

I hope you have liked and understood the content. All feedback is appreciated :)

Don't forget to Get Swifty ๐Ÿ‘‰๐Ÿป๐Ÿ˜Ž๐Ÿ‘‰๐Ÿป with me and the iOS Community on Twitter and LinkedIn! Tschรผss!

ย