- Architecture is rooted on design.
- Design solves any shortcoming of Apple MVC.
- Viper is a one-size-fits-all bloated solution.
Architecture is whatever facilitates change: adding features and fixing bugs.
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.
What we call architectural styles, like MVC 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.
This is the way to decrease obscurity and dependencies, which are the source of complexity in software.
This is the pattern for every Viper screen:
As long as these arrows point one way, there will be no screw-ups like the network client telling the router to navigate to the login screen. For why this is bad, see points one in the section before.
Because of user input, view and presenter need bidirectional communication. But 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.
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 ot other screens –eventually you may want to launch screens in isolation to iterate UI edits faster.
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 follow Viper, and still mess up.
For instance, 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.
Here are some principles to fix it –some were formulated in 1999:
- Rule of representation: communicate passing data, not method calls.
- Rule of transparency: design for clarity to facilitate inspection.
- “Split Phase” refactoring: read all the data, process all the data, then update all the data.
- Concurrency is hard. Performance is only a problem when there isn’t enough, so go serial if you can.
- 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.
So with all this in mind, the solution is to update the view through a single point, with a single object: view.update(model). Now the view is a function of state.
Takeaway point: you still need design principles. They are the pillars of architecture and operate within its boundaries. Without them, we lack criteria and misuse architecture.
* * * *
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?
* * * *
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?
Given that 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 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.
* * * *
A word about storytelling
We are bad at remembering abstract information but good at visual imaginery. Why can we retell a whole movie but not a phone number?. It’s because visual processing activates more than half of our cortex. The more neurons involved the bigger the memory trail.
Through generations humankind encoded information as stories. If we aim to write a story using OOP, let characters be domain entities with a visual representation.
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 code base 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 programmer’s intention (what we are doing). It’s all about SRP, testing, and uniformity (how we do it).
Viper may be more helpful when there is potential to screw up design or have endless discussions. That’s the case with bigger apps, and a team of different people and skills. But I suspect the better you design, the less 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 for the contract. Let’s be real, you won’t need it.
To unit test. For testing, you need a deterministic environment. You get that by mocking components or providing test data (e.g. a test database). The second option is integration testing but saves a lot of work. Having protocols to inject mocks is test-induced damage, and a coupling code smell. Again, try to imitate pure functions.
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 to callback by setting a closure.
To have a public interface for an object. You already have it if you look at the object’s public methods in your IDE. Objects are contracts too.
* * * *
Mobile is a great environment to learn because projects are small and it has CPU to spare, so the bar is very low. The sophistication of enterprise frameworks is reserved for special needs like Twitter‘s cache.
And yet the most popular architecture seems specifically designed for damage control at the expense of bloat. I find this soul crashing. It’s not humane to write twelve files where four suffice. We follow rules because it saves a lot of energy, but rules should only be a crutch on your way to free thinking.
Should you use Viper? If you have to ask, yes. But don’t stop there. To code is to design, and design trascends styles. Reflect on your work, dare to experiment, and fail better.