All Ruby on Rails Node JS Android iOS React Native Frontend Flutter QA

From Hobbyist to Professional iOS Developer: Single Responsibility Principle - Part 1

Introduction

So you know how to code in general, understand the object-oriented programming¹, learned Swift, and completed at least one iOS Development Course (if you’re not there yet, these links are a great place to start!). You can write a simple app using Model-View-Controller and it works well, but is your code any good? Could it be done any better? Is it clean (and what on earth does that mean)?

Is your architecture any good? Should you use a different one? What about design patterns? These were some of the questions I’ve had when I started, and answering them helped me to step up to a professional level.

In this series of articles, I will try to layout a good software development foundations in the context of iOS development, so you can answer these questions yourself.

So let’s start with the topic that is mentioned in every job interview: SOLID principles.

SOLID

SOLID is an acronym of 5 principles made by Uncle Bob that are fundamental to writing good object-oriented code:

  • Single responsibility principle
  • Open/closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

Maybe you have already heard about those principles, or even remember them. But if I were to ask you for an example, would you be able to write a code for each principle and then explain it? That’s where most people have a problem: in practical day-to-day usage. In order to write good code, you need to understand why the bad code is bad. So to give you the whole picture I will:

  • Give a simple definition of each SOLID principle
  • Give an example of a bad code, which in some way breaks discussed principle
  • Explain why the principle is broken in the given example
  • Refactor the code, so the example complies with the discussed principle
  • Explain why now it complies with it

Every example that I will present you with, will work in Swift Playground
To make the size of this article reasonable, I will split it by each SOLID principle per part. So let’s start with the first one!

Single Responsibility Principle

“A class should have only one reason to change”²

What this rule says is that given class should have only one reason to exist, and therefore there should be only one reason to change it. When creating a class, you should strictly define what’s it raison d’etre and stick to it. Make classes that have one specialized purpose and you’re in the clear.

Example 1

Let’s look at the very simple example of a class that’s supposed to send emails with attachments:

/*
 This example works well in Swift Playground

 For tests porpuses create a file in ~/Documents/Shared Playground Data/some_file.dat

 Use exactly this file's location, otherwise Playgrounds's sandbox will prevent the files from loading (code will work as there was no attachment)
 */

import Foundation
import PlaygroundSupport

struct Email {
    enum Style {
        case plaintext
        case html
    }

    var body: String
    var style: Style
}

class EmailSender {

    func sendEmail(from: String, to: String, email: Email, attachmentFileName: String? = nil) {

        //logic to prepare file
        var attachment: Data?
        if let unwrappedAttachmentFileName = attachmentFileName {
            let fullPath = playgroundSharedDataDirectory.appendingPathComponent(unwrappedAttachmentFileName, isDirectory: false)
            attachment = try? Data(contentsOf: fullPath)
        }

        //logic to send email
        if let unwrappedAttachment = attachment {
            //logic to send email with attachment
            print("\(unwrappedAttachment) of attachment send, along with message:\n\"\(email.body)\"")
        } else {
            //logic to send email without attchment
            print("message send:\n\"\(email.body)\"")
        }

    }
}

let sender = EmailSender()

print("-------------------------")
let myEmailWithAttachment = Email(body: "I'm sending you some_file.dat as you've asked! :)", style: .plaintext)
sender.sendEmail(from: "johnappleseed@apple.com", to: "myfriendsemail@host.com", email: myEmailWithAttachment, attachmentFileName: "some_file.dat")
print("-------------------------")
let myEmailWithOnlyText = Email(body: "Hello, how are you?", style: .plaintext)
sender.sendEmail(from: "johnappleseed@apple.com", to: "myfriendsemail@host.com", email: myEmailWithOnlyText)

 

Class EmailSender breaks the Single Responsibility Principle, because there are 2 reasons to change it:

  1. Any change in the logic of sending emails would need the EmailSender class to be changed.
  2. Change in the logic of how we load files from our operating system would also need a change in the EmailSender class.

Only the first reason for change makes sense. Sending the email itself shouldn’t have anything to do with getting files from our operating system. EmailSender should have all the data it needs to send an email, and it shouldn't care, how that data was acquired.

The example that complies to the Single Responsibility Principle would look like that:

/*
 This example works well in Swift Playground

 For tests porpuses create a file in ~/Documents/Shared Playground Data/some_file.dat

 Use exactly this file's location, otherwise Playgrounds's sandbox will prevent the files from loading (code will work as there was no attachment)
 */

import Foundation
import PlaygroundSupport

struct Email {
    enum Style {
        case plaintext
        case html
    }

    var body: String
    var style: Style
}

class EmailSender {

    func sendEmail(from: String, to: String, email: Email, attachment: Data? = nil) {
        //logic to send email
        if let unwrappedAttachment = attachment {
            //logic to send email with attachment
            print("\(unwrappedAttachment) of attachment send, along with message:\n\"\(email.body)\"")
        } else {
            //logic to send email without attchment
            print("message send:\n\"\(email.body)\"")
        }
    }
}

class FileReader {
    static func prepareDataFromFile(dataFileName: String) -> Data? {
        let fullPath = playgroundSharedDataDirectory.appendingPathComponent(dataFileName, isDirectory: false)
        return try? Data(contentsOf: fullPath)
    }
}

let sender = EmailSender()

print("-------------------------")
let myEmailWithAttachment = Email(body: "I'm sending you some_file.dat as you've asked! :)", style: .plaintext)
sender.sendEmail(from: "johnappleseed@apple.com", to: "myfriendsemail@host.com", email: myEmailWithAttachment, attachment: FileReader.prepareDataFromFile(dataFileName: "some_file.dat"))
print("-------------------------")
let myEmailWithOnlyText = Email(body: "Hello, how are you?", style: .plaintext)
sender.sendEmail(from: "johnappleseed@apple.com", to: "myfriendsemail@host.com", email: myEmailWithOnlyText)

 

Now there are two classes, and each of them has only one responsibility. FileReader handles files and EmailSender sends emails (regardless of how the attachments are created).

Example 2

The previous example broke the SRP in a very simple way, but in real life, people tend to break it in a little bit more nuanced fashion. The following example will look correct at the very first sight, but when you’ll take a closer look, you will see that it’s actually breaking the SRP.

Imagine a Hotel Application that holds room reservations, guests’ personal information, room details (number of beds, number of bathrooms, etc.).

A typical real-life example that breaks the Single Responsibility Principle would look like this:

class Hotel {
    var rooms: [Room] = []
}

class Room {
    var isReserved = false //it's not reserved by default
    var numberOfBeds: Int
    private (set) var guestsInTheRoom: [Guest] = []

    init(numberOfBeds: Int) {
        self.numberOfBeds = numberOfBeds
    }

    func addGuestThatWillOccupyTheRoom(guestFirstName: String, guestLastName: String, age: Int) {
        if guestsInTheRoom.count < numberOfBeds {
            let guest = Guest(firstName: guestFirstName, lastName: guestLastName, age: age)
            guestsInTheRoom.append(guest)
        } else {
            print("Cannot add new guest to this room! No beds left!")
            //good place to put some proper error handling here instead of a simple printout, but overkill for this example
        }
    }
}

class Guest {
    var firstName: String
    var lastName: String
    var age: Int

    init(firstName: String, lastName: String, age: Int) {
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
    }
}

let hotelCalifornia = Hotel()
let room1 = Room(numberOfBeds: 1)
let room2 = Room(numberOfBeds: 2)
hotelCalifornia.rooms = [room1, room2]

hotelCalifornia.rooms[0].addGuestThatWillOccupyTheRoom(guestFirstName: "Mark", guestLastName: "Appleseed", age: 21)
hotelCalifornia.rooms[0].addGuestThatWillOccupyTheRoom(guestFirstName: "Mark", guestLastName: "Pear", age: 22) //no room for you, sorry!

hotelCalifornia.rooms[1].addGuestThatWillOccupyTheRoom(guestFirstName: "Jane", guestLastName: "Doe", age: 45)
hotelCalifornia.rooms[1].addGuestThatWillOccupyTheRoom(guestFirstName: "John", guestLastName: "Doe", age: 46)


hotelCalifornia.rooms.forEach() { $0.guestsInTheRoom.forEach(){ print($0.firstName) } } //print all guests names in all rooms

 

If the hotel owner buys an air conditioner for one of the rooms, storing this information will change the implementation of the Room class, and that's perfectly understandable.

Imagine there is a new law, which prohibits hotels to gather data about the age of customers. To implement this change we would need to change both Room and Guest classes. Why is removing the age of guests from our app affecting the implementation of the Room class at all?! Class Room breaks the Single Responsibility Principle, because there are 2 reasons to change it:

  1. Any change in the logic of hotel rooms would need the Room class to be changed. For example: adding the air condition.
  2. Change in the logic of creating a guest would also need the change in the Room class inside the addGuestThatWillOccupyTheRoom(guestFirstName:guestLastName:age:) method.

As you can see, all it takes is just to call a constructor in a bad place, to create the class that is having more than one reason to change.

The example that complies to the Single Responsibility Principle would look like this:

class Hotel {
    var rooms: [Room] = []
}

class Room {
    var isReserved = false //it's not reserved by default
    var numberOfBeds: Int
    private (set) var guestsInTheRoom: [Guest] = []

    init(numberOfBeds: Int) {
        self.numberOfBeds = numberOfBeds
    }

    func addGuestToTheRoom(guest: Guest) {
        if (guestsInTheRoom.count < numberOfBeds) {
            guestsInTheRoom.append(guest)
        } else {
            print("Cannot add new guest to this room! No beds left!")
            //good place to put some proper error handling here instead of a simple printout, but overkill for this example
        }
    }
}

class Guest {
    var firstName: String
    var lastName: String
    var age: Int

    init(firstName: String, lastName: String, age: Int) {
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
    }
}

let hotelCalifornia = Hotel()
let room1 = Room(numberOfBeds: 1)
let room2 = Room(numberOfBeds: 2)
hotelCalifornia.rooms = [room1, room2]

let guest1 = Guest(firstName: "Mark", lastName: "Appleseed", age: 21)
let guest2 = Guest(firstName: "Mark", lastName: "Pear", age: 22)
let guest3 = Guest(firstName: "Jane", lastName: "Doe", age: 45)
let guest4 = Guest(firstName: "John", lastName: "Doe", age: 46)

hotelCalifornia.rooms[0].addGuestToTheRoom(guest: guest1)
hotelCalifornia.rooms[0].addGuestToTheRoom(guest: guest2) //no room for you, sorry!

hotelCalifornia.rooms[1].addGuestToTheRoom(guest: guest3)
hotelCalifornia.rooms[1].addGuestToTheRoom(guest: guest4)

hotelCalifornia.rooms.forEach() { $0.guestsInTheRoom.forEach(){ print($0.firstName) } } //print all guests names in all rooms

 

Now we have made the implementation of the addGuestToTheRoom(guest:) independent from the Guest implementation, by removing the Guest's constructor call. This way addGuestToTheRoom(guest:) method doesn't really care how the Guest objects are created. Just pass the Guest object (regardless of how it's initialised) to it, and it will work.

So now our change in the law can be implemented without affecting the Room class at all, because now it has only one reason to change (the room logic itself).

How to define the reason to change?

Not every example is as black and white as our Email and Hotel apps. If a reason for a class to exist is broad enough, we can always pretend that we have only one reason to change it.

You could create a class DataManager instead of two separate classes: InputReader and OutputWriter. Both input and output would be managed by the DataManager. This class would obviously have 2 reasons to change (handling input and handling output), but with smart wording we made it seems like a one reason: data management. This is arguably still a bad design choice!

But it goes both ways: Be too specific and you end up with hundreds of ridiculously interconnected classes, that could easily be one.

Look at the String struct. It does a lot, but all of it makes sense in one given context. We don't want to have too specialized structs/classes like StringCapitalizer, StringUppercaser, StringLowercasser. That would be absurd!

So the classes and their reasons to exist (and change) should be specialised enough. The fine balancing is the key here.

Considering this, the original principle can also be (and was!) worded like that:

“Gather together the things that change for the same reasons. Separate those things that change for different reasons.”

So that’s it!

Now you know all there is to Single Responsibility Principle and should be able to come up with examples for it on your own! In the next article, I will focus on the Open/Closed Principle.

. . .

¹Yes, I know this link is for a Java course, but it’s a very good language to learn basics. Java doesn’t do too much job for the programmer (like Python does) so you can understand how everything works, yet it does all the memory management magic, leaving you free from worrying about pointers (unlike C++ or Objective-C). And this particular Java Masterclass course has a proven record of teaching people not only Java itself but also object-oriented programming.

²It’s worth noting that sometimes the term “software module” is used instead of the “class”, to make this principle more generic.

 

This article was originally posted on Medium  From hobbyist to professional iOS Developer: Single Responsibility Principle

New call-to-action
Looking for new opportunities? Check our offers!
READ ALSO FROM Mobile
Read also
Need a successful project?
Estimate project or contact us