Clean Code SOLID Swift · 4 min read
SOLID Principles in Swift: Open-Close Principle
Entities should be open for extension, but closed for modification. Let's break this definition down in this article.
This week, let’s revise the S.O.L.I.D. principles and have an in-depth look at the first and probably most well-known principle: the Single Responsibility or SRP. This principle states: A class should have one, and only one reason to change. I think this definition can be a little bit abstract for some. Think of it this way: An object should only have a single reason to change (this doesn’t help either 😭) or A class should exactly have just one and only one job (much better 😁) or ultimately A class should only have a single responsibility. Violating this principle causes classes to become more complex and harder to test and maintain. However, the challenging part is to see whether a class has multiple reasons to change or if it has many responsibilities.
I'm going to give an example in the iOS world where we see a view controller having more than one responsibility. It's a mistake many young developers make in their first days as an iOS Developer. And generally in a MVC architecture, the controller is the place where we throw a lot of unrelated things because I guess it's easier and more convenient to stay at one place and see all the code associated with that particular controller.
final class LoginViewController: UIViewController {
private var emailTextField: UITextField!
private var passwordTextField: UITextField!
private var submitButton: UIButton!
// initializers...
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
private func setupView() {
// ... other view related code here
submitButton.addTarget(self, action: #selector(submitButtonTapped), for: .touchUpInside)
}
@objc private func submitButtonTapped() {
signinUser(email: emailTextField.text ?? "", password: passwordTextField.text ?? "")
}
}
This is straight forward, a simple login screen with two text fields: an email and password fields and a submit button. When the button is tapped, we try to log the user in. This seems to be perfectly okay until we add the remaining methods. We're going to put them in an extension like this:
extension LoginViewController {
// 1
private func signinUser(email: String, password: String) {
let url = URL(string: "https://my-api.com")!
let json = ["email": email, "password": password]
let jsonData = try! JSONSerialization.data(withJSONObject: json, options: [])
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
DispatchQueue.main.async {
self.showErrorAlert(message: error.localizedDescription)
}
}
guard let data = data else {
self.showErrorAlert(message: "sorry, could not log in, try later")
return
}
// 2
let user = try! JSONDecoder().decode(User.self, from: data)
self.log(user: user)
DispatchQueue.main.async {
self.showWelcomeMessage(user: user)
}
}
task.resume()
}
private func showErrorAlert(message: String) {
// logic to show an error alert
}
private func showWelcomeMessage(user: User) {
// logic to show a welcome message
}
// 3
private func log(user: User) {
// log user logic
}
}
We can see the controller violates the SRP because we write some methods that are responsible for very different things.
signinUser
method is responsible for making a network call and tries to log the user insigninUser
, we try to convert the data we get from the API call to a domain object, an User
in our examplelog
method that'll probably log the user's information to a remote service.
By putting the code for these methods and actions into the LoginViewController
class, we have coupled each of these actors to the others, and we can now see the controller has more than one responsibility. If we refer to Apple documentation, a view controller's main responsibilities include the following:When we want our classes to respect the SRP, this generally means we must create additional objects that'll have a single responsibility and use different techniques to make them communicate with each other. Let's see how we can apply this in our example. The first thing to do might be to extract the logic for performing an API request call to a separate object.
struct APIClient {
func load(from request: URLRequest, completionHandler: @escaping (Result<Data, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
return completionHandler(.failure(error))
}
completionHandler(.success(data ?? Data()))
}
task.resume()
}
}
The advantage of doing this is we now have a dedicated object that is only responsible for performing an API call and nothing else. Sweet, let's add another object for decoding the data from the API to a domain object.
struct Decoder<A> where A: Decodable {
func decode(from data: Data) throws -> A {
do {
let object = try JSONDecoder().decode(A.self, from: data)
return object
} catch {
fatalError(error.localizedDescription)
}
}
}
Again, we have a very simple object with a single responsibility: decoding data to a domain object. Now let's add the final object responsible for logging the user.
protocol Loggable {
var infos: String { get }
}
struct Logger<A> where A: Loggable {
func log(object: A) {
print("doing some logging stuff with \(object.infos)")
}
}
You see in this example, all the objects have a single method. Of course, they could have many, but the point is, it's totally fine to have a class or struct with a single method and if you add more, ask yourself if the method you're about to add does belong to the class. Now that we have our different object responsible for specific tasks, the next question is, how would we connect them? We have several solutions here, but the two commons ones in the iOS world are probably:
LoginViewController
via constructor injectionAPlClient
, Decoder
and Logger
objects then inject the ViewModel via LoginViewController
constructor.
I find the last solution a better option, so the view controller is not aware of network calls, decoding, and other stuff, and with the arrival of SwiftUI, we tend to use this pattern a lot. We'll have something like this:class LoginViewModel {
private var logger: Logger<User>
private var apiClient: APIClient
private var decoder: Decoder<User>
init(logger: Logger<User>, apiClient: APIClient, decoder: Decoder<User>) {
self.logger = logger
self.apiClient = apiClient
self.decoder = decoder
}
func signin(email: String, password: String, completionHandler: @escaping (Result<User, Error>)->()) {
let url = URL(string: "https://my-api.com")!
let json = ["email": email, "password": password]
let jsonData = try! JSONSerialization.data(withJSONObject: json, options: [])
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = jsonData
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
apiClient.load(from: request) { response in
switch response {
case .success(let data):
let user = try! self.decoder.decode(from: data)
self.logger.log(object: user)
completionHandler(.success(user))
case .failure(let error):
completionHandler(.failure(error))
}
}
}
}
Now we can use this view model in the LoginViewController
class and let him handle the user sign-in:
final class LoginViewController: UIViewController {
private var emailTextField: UITextField!
private var passwordTextField: UITextField!
private var submitButton: UIButton!
private let viewModel: LoginViewModel
init(viewModel: LoginViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
private func setupView() {
// ... other view related code here
submitButton.addTarget(self, action: #selector(submitButtonTapped), for: .touchUpInside)
}
@objc private func submitButtonTapped() {
viewModel.signin(email: emailTextField.text ?? "", password: passwordTextField.text ?? "") { response in
switch response {
case .success(let user): self.showWelcomeMessage(user: user)
case .failure(let error): self.showErrorAlert(message: error.localizedDescription)
}
}
}
private func showErrorAlert(message: String) {
DispatchQueue.main.async {
// logic to show an error alert
}
}
private func showWelcomeMessage(user: User) {
DispatchQueue.main.async {
// logic to show a welcome message
}
}
}
I don't know about you, but I think this is a much cleaner code. The controller is not aware of anything; he just handles user inputs and responds to them. We have a loosely coupled, more maintainable, and testable code now. That's all about the S.O.L.I.D principles, and as a bonus, our view controller is no more bloated (we have a joke in iOS and it's said MVC stands for Massive View Controller 😅).
The core of the SRP is each class should have its own responsibility, or in other words, it should have exactly one reason to change. If you start identifying multiple consumers and multiple reasons for a class to change, chances are you need to extract some of that logic into their dedicated classes. But as we said above, the most challenging part of this principle is knowing the object boundaries or identifying when an object begins to have more than one responsibility or reason to change. This comes with practice and constant reflection; we should always ask ourselves the right questions in order to move in the right direction. Next week, we'll go through the Open-Closed Principle. Until then, have a nice week, and may the force be with you 👊.
Written by
Independant developer, iOS engineer since 2014. I write articles about Swift, SwiftUI, iOS accessibility, software design, app architecture, testing and more.
Clean Code SOLID Swift · 4 min read
Entities should be open for extension, but closed for modification. Let's break this definition down in this article.
Clean Code SOLID Swift · 5 min read
"Derived classes must be substitutable for their base classes" which is the whole idea behind this principle. Let's have an in-depth look at it in this article and see the pitfalls we might come across.
Clean Code Swift SOLID · 4 min read
"A client should not be forced to implement an interface that it doesn’t use.", that's all about this principle. Let's have a look on this article and learn how to apply it on our codebase.
Clean Code SOLID Swift · 4 min read
Here comes the final letter of the SOLID series. The D or Dependency Inversion Principle (not to be confused with Dependency Injection) which states "High-level module should not depend upon low-level modules; instead they should depend upon abstractions", but also the "low-level modules too should depend upon abstractions". Let's have an in-depth look about this principle.
New posts about Swift, SwiftUI, TDD, software design, accessibility, testing in iOS directly in your inbox. I won't spam ya. 🖖 Unsubscribe anytime.