SOLID Design Principles in Swift

Piyush Sharma
6 min readJul 14, 2018

--

SOLID represents 5 principles of object-oriented programming: Single responsibility, Open/Closed, Liskov Substitution, Interface Segregation and Dependency Inversion.

(1) The Single Responsibility Principle (SRP)

Every time you create/change a class, you should ask yourself: How many responsibilities does this class have?

Let’s see an example:

How many responsibilities does this class have?

Handler retrieves the data from the API (1), parses the API response, creating an array of String, (2) and saves the array in a database (3).

Once you consider that you have to use in the same class Alamofire for the api request, ObjectMapper for the parsing and the CoreData stack to save the data in the database, you will start understanding the smell of this class.

You can solve this problem moving the responsibilities down to little classes:

This principle helps you to keep your classes as clean as possible. Moreover, in the first example you couldn’t test requestDataToAPI, parse and saveToDB directly, since those were private methods. After the refactor, you can easily do it testing APIHandler, ParseHandler and DBHandler.

Another scenario we can think of is what does a UIViewController do? Well, it assembles the views on the screen. It also prepares table view cells. Oh, it also navigates to another screen. Sometimes it also calls network requests. It also, also, keeps track of the state of our screen. A typical UIViewController is about 12 responsibilities removed from SRP. (This problem is often referred to as Massive View Controller.)

What can we do?

First off, stop adding little features. Shift your brain to thinking about modules, components and APIs. Get out of the mindset of patching and hacking, and into the mindset of creating libraries. Write small classes that do one thing. If you have a large problem, break it down. Write classes for each subset of the problem, and then write another one that will use those classes.

Identify the responsibilities of the view controller, and split them up. The easiest candidates are tableview delegates and data sources. Make a separate class that does that thing. The data source can be any class, not just the view controller.

And always pay attention to the size of your classes. The number of lines can be a very good warning sign that some things should probably be split up.

(2) The Open-Closed Principle (OCP)

If you want to create a class easy to maintain, it must have two important characteristics:

  • Open for extension
  • Closed for modification

What above attributes are trying to say is that it should be easy to add new features to your class. For example, there is a UserFetcher. Let's say that class has one method, fetchUsers, which fetches a JSON of all your users from a database, parses it, and returns it.

But hold on! A super-intelligent species from Jupiter has invaded earth and taken earth hostage! They want you to make the app work with Jovian (species from Jupiter) objects, instead of User objects. And they want it fast!

With our UserFetcher class, we need to change the whole implementation, even to the point of renaming the class, for it to work with Jovian. If we took a different approach, this change would be a lot easier. What if instead of a UserFetcher, we wrote a generic Fetcher, for any Decodable list of things?

The implementation is almost the same, except we changed where we mention User to a generic T, which conforms to decodable. With this approach, we only need to change that single line at the bottom to support Jovian.

(3) The Liskov Substitution Principle (LSP)

LSP says that any function working with a class should work also with any of its subclasses. The user of that method shouldn’t be able to see a difference between your version and the base class’ method.

Let’s go back to our Fetcher. The Jovians are happy with how the code works on Earth, but they don't like how it works when they go back to Jupiter, there's no internet there, so they don't see any data. They want offline mode.

So, you go ahead and make a subclass of the Fetcher, which fetches data from a file on the device which contains cached Jovians. This is a good idea since creating a subclass means you don't have to change code that works with Fetcher, since the API stays the same.

You’re really scared, so you goto your already implemented Fetcher class code and quickly copy that feature into the app.

(4) Interface Segregation Principle

The Interface Segregation Principle 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.

Suppose we have 2 types of workers men and women . Both types of workers work and they need a daily launch break to eat.

But now some Robots came in the company they work as well, but they don’t eat, so they don’t need a launch break. One on side the new Robot class need to implement the Worker protocol/interface because robots works. On the other side, the don’t have to implement it because they don’t eat.

If we keep the present design, the new Robot class is forced to implement the eat method.

Following it’s the code supporting the Interface Segregation Principle. By splitting the Worker interface in 2 different interfaces(Workable, Feedable) the new Robot class is no longer forced to implement the eat method. Also, if we need another functionality for the robot like recharging we create another interface Rechargeable with a method recharge.

It means no run time surprises. At the compile time, we know that Robot can’t eat.

Let’s take another example see how we can implement in other scenarios…

Fat interface (Protocol)

Our SuperButton is happy to implement the methods which it needs:

The problem is that our app has also a PoorButton which needs just didTap. It must implement methods which it doesn’t need, breaking ISP:

We can solve the problem using little protocols instead of a big one:

Fat interface (Class)

We can use, as example, an application which has a collection of playable videos. This app has the class Video which represents a video of the user’s collection:

And suppose our player class is responsible for playing the that video using an injected video instance:

Unfortunately, we are injecting too many information in the method play, since it needs just url, title and duration.

You can solve this problem using a protocol Playable with just the information which the player needs:

This approach is very useful also for the unit test. We can create a stub class which implements the protocol Playable:

(5) Dependency Inversion Principle

The dependency inversion principle refers to a specific form of decoupling software modules.

High-level modules should not depend on low-level modules. Both should depend on abstractions.

If your app is properly architected, even though Any third party framework (Firebase, Parse)is shutting down, it’ll be easy to replace it with whatever you want.

The work() simply invokes the doWork() method on the worker.

The SpecificWorker class conforms to the Worker protocol. To satisfy this conformance, it implements the doWork() method.

You can replace worker inside Factory with SpecificWorker or AnotherSpecificWorker anytime, as long as they both conform to the Worker interface.

Why Dependency Inversion is important?

Imagine if you replace Worker with ParseServiceWorker, and doWork() with fetchUserWith(id: String, completion:). your work() method now looks like:

The Factory class directly depends on ParseServiceWorker. This is a traditional dependency.

If you switch to Firebase or your own backend server, it may need something other object than to identify a user. Now that Parse is shutting down, you would have to change everywhere you use ParseServiceWorker.

That’s not good :)

Let’s design this properly with an abstraction using the Dependency Inversion Principle to see how we can avoid having to make changes throughout the code base even if Parse is shutting down.

We introduce a Worker protocol to define the func fetchUserWith(id: String, completion: ) method.

A specific implementation of a service could be a ParseServiceWorker as follows.

But it could also be FirebaseServiceWorker or KituraWorker, as long as they conform to the Worker protocol and implement the fetchUserWith(id: String, completion:) method.

Now, if you need to swap ParseServiceWorker with FirebaseServiceWorker, you only need to supply an instance of FirebaseServiceWorker when you create the Factory.

Developing and testing specific implementation of the Worker protocol can be completely isolated and delegated to one team of developers. Other developers can work on the high level business logic and the UI. The Worker protocol is a contract that is agreed upon.

Thanks for reading :)

--

--