Ever had the feeling that VIPER should be simpler?
Architecture is whatever facilitates change: adding features and fixing bugs. This includes any work done during the lifecycle of the code: type, test, document, understand, debug, modify.
The work of a programmer is writing the code, then 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, the application is divided 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, time is wasted reading 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 some kind of 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 special 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 depend 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 flow of input/processing/display. 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. If instead, 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. Depends on your choice of implementation.
So 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 elements of a view 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.
* * * *
The key elements in iOS architecture are not designed 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
If you write a complex app that uses most of the app delegate, it will grow 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 relevant methods for each object.
- Delegate the calls from the AppDelegate to each service.
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 clearly breaks the single responsibility principle. We tend to attribute this to Apple, but whose fault is it really?
Constraints or Growth?
It’s fair to say that many projects derail. We blame coders’ incompetence, but reality is a system, and when failure happens it happens at every level. Hell, I’ve been there, it’s Kafkaesque. But regarding coding, I see two paths:
- Grow your abilities. Learn SOLID, OOP, FP, etc
- Constrain your environment to a dumbed-down architecture.
Mobile is a great environment to learn because projects are small and it has CPU to spare, so you get to roll out your own everything. The sophistication of enterprise frameworks is reserved for special cases.
So take the chance. It’s not Apple’s job to provide a universal solution to decompose a problem –that’s yours. Not that they can, either. Viper tried, and here’s what happened.
* * * *
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
In most Viper implementations, there are five Viper objects, plus a protocol per component. Each element in a folder, plus additional elements: builder, ViewModel, mapper.
On top of that, a design system needs a screen to be composed of modular elements. If you make each a Viper module, the number of files will leave you thinking there has to 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, do so by making the codebase even larger. While interface decomposition is a valid 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 dilutes the programmer’s intention (what we are doing). It’s all about SRP, testing, and uniformity (how we do it).
A worst case would be to turn a small UI component into a Viper module. For example, imagine a range date picker coded as two objects: a view plus an internal validation rule (start date < end date). That rule would be independent and testable. Wrapping it in ‘clean’ onion layers or Viper whatevers would only complicate things.
When in doubt, Viper is a way to avoid endless discussion and get something through the door. As you grow your abilities I doubt Viper is going to pay off.
Problem: Test-Induced Damage
If you are already doing Viper, here are some tweaks to consider: 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. Let’s be real, you won’t need it.
To decouple components. In any sane architecture, dependencies should go one way. This doesn’t mean you have to decouple with a protocol, you may offer closures for callbacks.
To have a public interface for an object. You already have it if you look at the object’s public methods in your IDE. 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 purposes of testing. 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 really a unit test, plus the possible paths that may affect your tests are unthinkable.
* * * *
One-size-fits-all architectures are a technical solution to a social problem: we don’t trust people to design context specific solutions, and we don’t know how to teach them. So we rely on consistency at the expense of mediocrity. In time, we 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 your way to free thinking. Don’t drink the Kool-Aid. Dare to experiment, iterate, and fail better.