Cleaner Architecture on iOS
Today, I’m not going to teach you anything new or groundbreaking. Rather, I’m just going to remind you about something you already know: the single responsibility principle (SRP). More specifically, I want to discuss how to use it properly with clean architecture, and I’ll assume that you already know a fair bit about it (if not, I suggest you read the sources below and then come back). So let’s not forget to remember to remind ourselves to explicitly consider SRP when making decisions, and hopefully that will help us design better software!
What is SRP?
Here’s one definition : “The single responsibility principle is a computer programming principle that states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility”.
A class should have only one reason to change. Following this will make making changes easier, reduce coupling, improve testability, speed up development and a lot more! SRP is also a fundamental idea of clean architecture (but it’s also, of course, applicable to other approaches like MVC, MVVM, reactive, etc.)
Why not MVC?
The obvious answer is: because Massive View Controller. This is, of course, a joke - but it’s funny because it’s true. It almost begs the question: why is MVC the default architecture on iOS when it leads to massive problems such as the massive view controller? The answer is that it’s not a problem in the first place. If the controller is massive, then it’s not the fault of the architecture, it’s the programmer that’s not using it correctly. You can write a perfectly clean application with MVC while the massive view controller problem can be easily fixed for example by:
- not using just one controller for one scene,
- delegate work to worker/service classes.
In other words, by applying SRP. So, if MVC is not the problem, then why use clean architecture at all? If MVC, as it’s used on iOS, has a problem, then it’s that it’s rather vague and leaves many decisions to the programmer. What is the responsibility of the controller? If you’re not careful, it can acquire far too many. But where do we put all the other responsibilities that we need? In the model? How do we structure that? The architecture doesn’t really tell us any of this. We’re on our own, which means there are many opportunities to introduce bugs!
If you don’t want to think about all of that, you can just use clean architecture. Clean architecture explicitly divides some responsibilities among its classes: the presenters bridge the divide between UI and business logic, the interactors handle our use-cases, routers help us get to new scenes, etc. The responsibilities are clear and our codebase is a little cleaner.
So, by simply using clean architecture, did we solve the problem? Do we have SRP in our codebase now? Well, not necessarily. Architecture is immensely helpful but it doesn’t solve all our problems. We still need to think, make choices, and exert some effort to make things even cleaner.
Clean architecture has become quite popular on Apples platforms and for a good reason. There are even several approaches we can choose from, like VIPER  and Clean Swift . Let’s take a look at some real projects that use Clean Swift (or CS as I like to call it, so it’s consistent with the naming convention for the architectures we use on iOS).
I have seen codebases with large and complex interactors that clearly don’t follow SRP. I call it the massive interactor problem (MIP). The MIP may not be as bad as a massive view controller, since interactors don’t concern themselves with UI, but they still try to do far too much. If a programmer is capable of writing a massive view controller, he’ll certainly also write a great massive interactor. The problem is that even though we think we’re using clean architecture, the responsibilities are not properly separated, and therefore not as clean as they could be. To avoid the root cause of the MVC/MIP problem, we’ll need to apply SRP mercilessly.
The interactor contains the applications business logic but there’s only one interactor per view controller. That is fine for small scenes but unless you combine it with the “don’t use one view controller per scene” recommendation that I mentioned earlier, it can get out of hand really quickly. In my opinion, in complex scenes, the interactor shouldn’t really do anything interesting. No algorithms, no database or parsing, nothing that requires a nested if statement or a loop. This belongs in Worker classes. The responsibility of the interactor will be delegating work to its workers and passing the results to the presenter. The original tutorial on CS  only contains a small section on workers and thanks to it’s shortness, its importance may not be so obvious. This makes sense, of course, because the responsibility of the worker is specific to your application and contains its business logic so the tutorial doesn’t really have much to add. Nevertheless, I think the importance of the worker needs to be stressed more.
In clean architecture, an interactor should represent a single use case. That means a single responsibility ought to be the sole concern of one class. Because CS only has a single interactor per scene, and a scene often contains several use-cases the interactor will acquire many responsibilities. We can choose to ignore this, which may be acceptable when things are simple, but as the interactor grows it will become more and more difficult to handle. The other option is to delegate those responsibilities to workers. In this setup, the interactor will create a number of workers and only direct them. So, if a request comes from the input, the interactor will simply delegate it to the worker and then pass the result on to the presenter. That sounds almost too simple but it’s a big improvement on the MIP. With workers each handling one use-case, they sound more like an interactor than the interactor, don’t they?
What about VIPER?
I heard many people say that VIPER is very complicated. Setting it up takes too long, there are too many tiny classes, and think about the new developers! VIPER is a good example of SRP because VIPER is very serious about SRP. If you’re serious about SRP too, and do your best to apply it to your MVC project you might just get something similar (that has actually happened to me. I thought I’d invented the wheel! Only to find out that it was already around for a long time…). You won’t get a detailed documentation for free though.
Clean Swift and VIPER are very similar, both are based on clean architecture, after all. The main difference is the VIP cycle, and the number of interactors per scene. But if we strictly apply SRP to CS, split the responsibilities of the CS interactor and create a worker class for every use case, we essentially get VIPER (if we ignore a few details, CS workers would correspond to VIPERs interactors, and CSs Presenter-Interactor pair to VIPERs Presenter). Actually, CS is even more complex thanks to the VIP cycle. Yes, you heard right, Clean Swift is even more complex than VIPER! (At least for big projects where we want to avoid the MIP and properly apply SRP, etc.) VIPER is not as complicated as its reputation might have you believe. So if you haven’t already, don’t be afraid to learn what it has to offer. Even if you don’t end up using it on your next project, understanding it, and understanding “why” it does things the way it does is another tool in your tool box. Use it to make life easier for yourself and for others.
More on SRP
Here are a few recommendations:
- Don’t mix abstraction layers. A low level computation in the middle of high level business logic is a sign of SRP violation. After all, we don’t want to deal with the complexities of a cell when working with tissues or organs and vice versa.
- Be explicit about the responsibility of your class. Write it down in documentation commentaries above the header. The description should be concise while encompassing all responsibilities. But be careful about words like “manager”. What do managers do anyway? It’s too vague and can easily encompass several responsibilities without you noticing. When you make changes to the class, make sure it’s still accurate, else refactor. If you think you don’t have time for that, then don’t worry, it will save time in the long run since you’re reducing technical debt and keeping the project a bit cleaner.
- SRP can be applied at multiple levels, from general architecture to individual methods. If a method does multiple things, split its contents into new methods that execute partial tasks and call those from the original method. Remember that a method should only occupy one abstraction layer.
- A large line count is an indicator that SRP is broken. If you notice that you’ve been scrolling for a long time, then you probably have a problem. SRP is a better metric than the number of lines, so rather than inventing a magic number that you can’t surpass, list the responsibilities. If it’s more than one, split it. Shorter classes and methods are also easier to maintain so try to keep things as short as reasonable (but not shorter).
- Be pragmatic. Rather than exactly following specifications and guidelines, it’s more useful to understand why they make their design choices and what they achieve. Then make informed decisions (just don’t disguise laziness and excuses as pragmatism, because they’re not!)
In conclusion, don’t write massive controllers. Explicitly consider responsibilities and make sure you don’t have too many in a single place (ideally, too many means two). Don’t just add more code to a class because it’s easier right now, if it contradicts with its responsibility. Adhering to SRP will most likely save time in the long run — so don’t make excuses. Can striving to write cleaner, pragmatic code lead to anywhere other than better software?