Ever had the feeling that VIPER should be simpler?
Architecture is whatever facilitates change: adding features and fixing bugs. It includes any work done during the lifecycle of the code: type, test, document, understand, debug, modify.
Your work as a programmer is writing the code and structuring the resulting complexity to provide a high-level view. That is, you create the engine pieces, then hide the engine under the hood, exposing just the accelerator pedal. Correctly done, this divides the application into layers of abstraction separated by well-defined interfaces. Low-level details can be ignored, while high-level components are ready to be re-composed in different configurations.
VIPER, MVC, etc., identify recurring components and the way they communicate. They follow two principles:
Distribute responsibilities in entities with strict roles. A container’s name should reflect its contents. If it doesn’t, you’ll waste time having to read its contents to find out what it does.
Set dependencies one way only. E.g., I use the hammer, but the hammer doesn’t use me. Otherwise, it’s a sentient hammer and… well, see point one.
What is Viper?
As long as arrows point one way only, there will be no screw-ups like the network client telling the router to navigate to the login screen. For why this would be bad, see points one and two above.
The view is a particular case because it displays data and accepts user input. Because of this, view and presenter need bidirectional communication. But at least, they don’t need to depend on each other. They can rely on protocols:
Or one element may offer a closure that the other can set to receive events.
You may wonder:
Didn’t you say a double arrow is bad? View, presenter, and logic never talk back to each other, they participate in a circular input/processing/display flow. For practical reasons, the view is two separate things: a display with a keyboard.
Why is the view the root of the object graph? Given that the view is the element deallocated by UIKit when we navigate away, it’s also the element that strongly references the presenter. Instead, if you wanted your logic to own the graph of objects, you would have to fight UIKit. Uber did that with Riblets, but having a hundred iOS engineers is a special case.
I would replace the router with a coordinator to centralize navigation code and avoid coupling to other screens. Eventually, you may want to launch screens in isolation to iterate UI edits faster.
As for models, you often have three for isolation: entity, domain, view model, with mapping between them. There are other choices of implementation.
In summary, Viper has an SRP (single responsibility principle) architecture, a discussion of the responsibilities involved, and the novelty of saying: every screen should have these elements because you’ll probably need them.
Any architectural style is rooted in basic design principles (SOLID, GRASP, etc.) and provides consistency. But design doesn’t end with the broad strokes of a particular style. You can do let’s say, Viper, and still mess up. Let’s see an example.
Problem. A common anti-pattern is to update individual view elements from multiple points. This is prone to errors and hard to debug because it defines your view as a function of a sequence of events, leading to race conditions.
Discussion: What design principles can we use to fix it?
- From Basics of the Unix Philosophy
- Rule of Transparency: Design for visibility to make inspection and debugging easier.
- Rule of Representation: Fold knowledge into data so program logic can be stupid and robust.
- From Martin Fowler’s Split Phase: read all the data, process all the data, then update all the data.
And two more without catchy terms:
Concurrency is hard. Performance is only a problem when there isn’t enough, so go serial by default.
Imitate pure functions. Even when objects encapsulate state, you want them to behave deterministically, producing the same output for a given input or at least a replayable history of side effects.
Solution. So with all this in mind, the solution is to update the view through a single point, with a single object: view.update(model) –or bindings if you prefer. Now the view is a function of the state.
Takeaway point: you need design principles. They are the pillars of architecture and operate within its boundaries. Without them, we lack criteria and misuse architecture. Passively following an architecture style is not by itself architecture.
Next, we’ll revisit Apple’s MVC with an active attitude.
* * * *
Apple didn’t design key elements of the iOS architecture for complex apps. This is fine. It keeps things simple for the common case and it’s straightforward to fix. I’ll give you two examples.
A Better Delegate
Writing a complex app that uses most of the app delegate will grow the delegate to thousands of lines. If that’s the case, the workaround is easy.
To refactor massive app delegates:
- Create a different object per responsibility (Analytics, Notifications, etc.).
- Conform them to a protocol extended with empty implementations (ApplicationService in the example below).
- Override the appropriate methods for each object.
- Delegate the calls from the AppDelegate to each service.
You may wonder
Should Apple provide those objects as AppDelegate variables? No. There would be use cases they can’t possibly anticipate, and some delegate methods participate in more than one of them.
Should Apple provide this exact design? No. If I’m developing a fart app, I don’t need the extra indirections for the sake of correctness. And it certainly wouldn’t help newbies learning iOS.
A Better View Controller
To refactor view controllers:
- Treat them as views: compose controllers and reuse them across screens.
- Move non-view stuff elsewhere.
Writing a 2000-line class to observe, download, persist and transform data blatantly breaks the single responsibility principle. It’s our job to recognize and fix this, not Apple’s.
Most software written is an ad hoc solution with the minimum number of elements needed. It doesn’t follow any named style. However, this requires making choices. If a team says things like “with Viper we don’t have to think”, or “let’s all do things the same way”, it means they are not comfortable with ad-hoc design and need a template architecture like Viper.
Viper is easy: disregarding the problem, use the same elements, don’t design anything. Here are these boxes, put your code in one of them. This is fine, use Viper if you have to.
To walk my own path I need intuition. Intuition is an instant gut feeling produced by the mind quickly shifting through experience and knowledge. Intuition is built from experience, reflecting at high and low level, learning principles, and experimenting. Intuition provides a quick answer in the face of unquantifiable outcomes.
But in the meantime, I’ll make mistakes, and work will slow down and become complex and frustrating. I won’t notice, or maybe I’ll blame it on something else that doesn’t hurt my ego. It’s hard to recognize the mess because errors don’t slap you in the face, they kill you by a thousand cuts.
Unfortunately, there are no shortcuts, you have to respect the process. Otherwise school would be over in a day. Wouldn’t that be something?
Kids, I have decades of experience being an adult and I’m quite articulate. I’ll explain growing up and today you’ll go back home as adults.
* * * *
Viper constrains your environment to a coding standard:
- Each element has a single responsibility.
- Every screen has the same five components: (view, presenter, interactor, router, entity). This consistency lets people find things.
Problem: One Size Design
The goal of OOP is to decompose a problem in entities of the domain. For instance, in a design for a parking lot there are objects like ParkingTicket, TicketScanner, etc. Nowhere in the domain there is something called interactor. What does interactor even mean?
Because mobile applications are often glorified tables and entry forms, Viper takes a one-size-fits-all approach to design. It tells you to place your code in one of five boxes: view, presenter, interactor, router, entity. No thinking is needed, just choose a box.
Does it work? Often, it does. You probably need those five roles per screen, and the problem may be small enough not to require specialized objects.
But sometimes it doesn’t. Programming is a wicked problem where each feature is different and needs a specific solution. Eventually, you’ll get:
Too much per element (high coupling, low cohesion). A complex screen may require fragments of unrelated logic that are better suited as different objects. Then you have the work of turning a massive interactor into a front for additional objects.
Too little per element (low coupling, low cohesion). What is the presenter adding here (view → presenter → logic)? Shouldn’t the view talk with the interactor and router directly? Or better yet: how about you create elements as you need them? Creating a middleman for consistency shake is being consistently bad.
A symptom of low cohesion is the developer having to debug its way through the app because entities at the same level of abstraction are artificially spread. You no longer understand elements on its own, because part of them is elsewhere.
Problem: Bloated Implementation
There are five Viper objects in most Viper implementations, plus a protocol per component —each element in a folder, plus additional elements: builder, view model, mapper.
On top of that, a design system needs a screen composed of modular elements. If you make each a Viper module, the number of files will leave you thinking there must be a better way.
The book “App Architecture” had this to say about Viper, a “misguided” [sic] pattern they didn’t cover:
Attempts to bring “Clean Architecture” to Cocoa usually claim to manage “massive view controllers,” but ironically, they do so by making the codebase even larger. While interface decomposition is a useful approach for managing code size, we feel it should be performed as needed, rather than methodically and per view controller. Decomposition should be performed along with knowledge of the data and tasks involved so that the best abstraction-and hence the best reduction in complexity-can be achieved.
Before that, Kent Beck defined simple design in 1990:
- Passes the tests
- Reveals intention
- No duplication
- Fewest elements
This controller has 200 lines (4,5 screens). Can you describe what’s going on?
How about now?
Viper is a uniform way to achieve single resposibility and testing. Unfortunately, it also dilutes the programmer’s intention because it taxes the reader’s working memory with lots of abstractions and indirections. Good ingredients, mediocre stew.
Problem: Test-Induced Damage
If you are already doing Viper, consider some tweaks: Why are you writing a different protocol per component?
- Instead, model communication as an I/O exchange of data (example) reusing the same protocols. By imitating a pure function, your object reaps the same benefits. It isolates side effects from logic, reduces coupling to external components, and allows memoization.
Why are you using protocols at all?
To provide a second implementation one day. Don’t design in advance for the neds of tomorrow.
To decouple components. In any sane architecture, dependencies should go one way. It doesn’t mean you have to decouple with a protocol, you may offer closures.
To have a public interface for an object. If you look at the object’s public methods in your IDE, you already have it. That’s a contract too.
To unit test. For testing, you need a deterministic environment. There are several ways to set up one:
Mocks. Create a protocol for the public interface of an object, create a mock as a second implementation. Anything depending on the object can use the mock instead for testing purposes. Bad idea: more complexity in the system. Having protocols to inject mocks is test-induced damage, and a coupling code smell.
Communicate with values. Are you using method calls to signal user interaction? Replace them with values at your component boundary. You may know this from Gary Bernhardt but it’s also an old one. This is the way; no mocks needed!.
Integration testing. e.g. a test database, and pre-recorded network responses. Of course, this is not a unit test, plus the possible paths that may affect your tests are unthinkable.
* * * *
Bloated architectures like Viper are a technical solution to a social problem: we don’t trust people to design correctly, and we don’t know how to teach them. So instead, we rely on a mediocre solution. In time, people end up thinking that’s what professionals do.
Men almost always walk in paths beaten by others and act by imitation. –Niccolò Machiavelli
We do it because it saves a lot of energy. But it should only be a crutch on our way to free thinking. If you love Viper, try not to use it and see what happens. Every mistake has something to teach you.