SOLID Principles in Swift - Single Responsibility Principle

Background

In this series of posts we are going to be covering the SOLID principles of software development. These are a set of principles / guidelines, that when followed when developing a software system, make it more likely that the system will be easier to extend and maintain over time. Let’s take a look at the problems that they seek to solve:

  • Fragility: A change may break unexpected parts, it is very difficult to detect if you don’t have a good test coverage
  • Immobility: A component is difficult to reuse in another project or in multiple places of the same project because it has too many coupled dependencies
  • Rigidity: A change requires a lot of effort because it affects several parts of the project

So what are the SOLID principles?

  • Single Responsibility Principle - A class should have only a single responsibility / have only one reason to change
  • Open-Closed Principle - Software should be open for extension but closed for modification
  • Liskov Substitution Principle - Objects in a program should be replaceable with instances of their sub types without altering the correctness of the program
  • Interface Segregation Principle - Many client-specific interfaces are better than one general-purpose interface
  • Dependency Inversion Principle - High level modules should not depend on low level modules. Both should depend on abstractions

In this article we will focus on the Single Responsibility Principle.

Problem

The first principle in the list is the Single Responsibility Principle. This principle is defined as follows:

A class should have only one reason to change

This means a class should be responsible for only one task, not multiple. Let’s take a look at an example and how we can refactor it using the principle.

struct SomeNews: Codable {
    let id: Int
    let title: String
}

class NewsDatasource {
    func getNews(completion: @escaping ([SomeNews]) -> Void) {
        // 1. Create request
        let url = URL(string: "SomeNews/URL")!
        let request = URLRequest(url: url)
        
        // 2. Fetching data
        let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
            
            // 3. Parsing data
            guard let data = data,
                  let news = try? JSONDecoder().decode([SomeNews].self, from: data) else {
                completion([])
                return
            }
            
            completion(news)
        }
        
        dataTask.resume()
    }
}

This looks like a fairly simple news datasource / service that is fetching some news items from the web. However if we take a closer look we will see it’s responsible for more than one task.

  1. Creating the URLRequest that is used to fetch the news articles
  2. Fetching the data using a URLSession
  3. Parsing the data

Already that is 3 different responsibilities this class has. They may seem fairly straight forward in this example but imagine how this could get out of hand quickly in a larger codebase. Let’s cover some of the scenarios.

  • Is this example the news request is simple. However what if the request was more complex, what if we needed to add headers etc to that request? All that code would be in this class.
  • What if we wanted to change the request used to fetch the news? We would have to make a code change here. Or what if we could fetch news from more than one API? How would we do that in the current structure?
  • Once the request has been made we are using a JSONDecoder to decode the response. What if the response comes back in a different format? What if we wanted to use a different decodable for the response?
  • What if the news request can be used in multiple places?

As we can see from the above list, there are several scenarios that would require a code change of this class. If we recall what the single responsibility stands for:

A class should have only one reason to change

There is also a side effect of this which isn’t as obvious, that is testability. Let’s look at some examples:

  • How would we test changes the URLRequest? If did indeed change the URLRequest or it was being generated differently, how would we test that?
  • How do we test how our class handles responses from the server? What happens if we get an error for example?
  • How do we test our decoding code? How can we be sure that it is going to handle incorrect data correctly? How does our news datasource handle decoding errors?

If we look at the code in the example we can see that in would be impossible to write unit tests covering any of the above scenarios. Let’s have a look at how we can break this class down into single components, allowing us to make changes only in one place and at the same time improving testability.

Breaking it down

URL Builder

Let’s start by breaking this class down into separate classes, each with one responsibility. First of all let’s take out the building of the URLRequest and put it in another class.

class NewsURLBuilder {
    private let hostName: String
    
    init(hostName: String) {
        self.hostName = hostName
    }
    
    func getNews() -> URLRequest {
        let url = URL(string: "\(hostName)SomeNews/URL")!
        let request = URLRequest(url: url)

        return request
    }
}

Great, now we have a class that’s only responsibility is to build and return a URLRequest. In a more complex system this class might need ids, user tokens etc in order to configure the request. In the scenario where we need to change how news is retrieved we only need to modify this one class in order to make that change. We can also change the hostname based on the environment such as dev, test and prod.

The other benefit of doing this is we can now write unit tests to make sure that the URLRequest is being built correctly. Let’s do a small example now:

class URLBuilderTests: XCTestCase {

    func testURLBuilder() throws {
        let builder = NewsURLBuilder(hostName: "http://mytest.com/")
        let request = builder.getNews()
        
        XCTAssertEqual(request.url?.absoluteString, "http://mytest.com/SomeNews/URL", "Request URL string is incorrect")
    }

}

Our URL builder isn’t particularly complex so doesn’t need many tests. But at least here with it being in a separate component we can test the construction and make sure it’s being created correctly. We could expand this test to test other elements of the request if needed, or if we needed different parameters to build the request.

Parser

Next lets take the parser and put that into it’s own class.

class NewsParser {
    private let decoder: JSONDecoder
    
    init(decoder: JSONDecoder) {
        self.decoder = decoder
    }
    
    func parse(data: Data) -> [SomeNews] {
        return (try? decoder.decode([SomeNews].self, from: data)) ?? []
    }
}

Here we can see we have taken our decoding code and put it into a separate class. This class has one reason to change, it only needs to be changed if the parsing needs to be changed! Also like our URL builder class we can now test the decoding to make sure we get the results we are expecting:

class NewsParserTests: XCTestCase {

    func testCorrectData() throws {
        let correctJSON = """
        [
          {
            "id": 1,
            "title": "Test Article 1"
          },
          {
            "id": 2,
            "title": "Test Article 2"
          }
        ]
        """
        
        let data = correctJSON.data(using: .utf8)!
        
        let parser = NewsParser(decoder: JSONDecoder())
        let news = parser.parse(data: data)
        XCTAssertFalse(news.isEmpty)
        XCTAssertEqual(news[0].id, 1)
        XCTAssertEqual(news[0].title, "Test Article 1")
    }

    
    func testInCorrectData() throws {
        let incorrectJSON = """
        [
          {
            "id": 1,
            "title": "Test Article 1"
          },
          {
            "id": 2,
        ]
        """
        
        let data = incorrectJSON.data(using: .utf8)!
        
        let parser = NewsParser(decoder: JSONDecoder())
        let news = parser.parse(data: data)
        XCTAssertTrue(news.isEmpty)
    }
}

So what have we done here. We have created a couple of unit tests for our parser.

  • The first one supplies the parser with some correct JSON data and checks that the news objects we receive are correct and have the right data.
  • The second test sends some incorrect data to the parser and tests that we receive an empty array as expected

I’m aware that we aren’t handling errors in this example, this has been done to try and keep things as simple as possible

Putting it all together

Now that we have separated out these components into separate pieces, lets see how our datasource looks now.

class NewsDatasource {
    private let requestBuilder: NewsURLBuilder
    private let parser: NewsParser
    
    init(requestBuilder: NewsURLBuilder, parser: NewsParser) {
        self.requestBuilder = requestBuilder
        self.parser = parser
    }
    
    func getNews(completion: @escaping ([SomeNews]) -> Void) {
        // 1. Create request
        let request = requestBuilder.getNews()
        
        // 2. Fetching data
        let dataTask = URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in
            
            // 3. Parsing data
            guard let self = self,
                let data = data else {
                completion([])
                return
            }
            
            completion(self.parser.parse(data: data))
        }
        
        dataTask.resume()
    }
}

Now if we look here we can see that we have swapped out the code for building the request and parsing the data for our separate classes. Now our example is following the single responsibility principle. We have 3 components now:

  1. A component to build our request
  2. A component to execute the request
  3. A component to parse the data we get back from the request

So what have we gained:

  • We now have test coverage of our components (we could update the NewsDatasource to have tests too but that is a bit more advanced and out of scope of this article)
  • We have the ability to re-use these components in other parts of the app or in other apps if we need to
  • If we need to make changes, each component is only responsibility for one thing, so we can update and test each change in turn. Rather than making multiple changes in one place and not be able to test them!

Feel free to download the sample and play around with the tests yourself!