Ever had the feeling that VIPER should be simpler?
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.
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.
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 and two above.
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 do let’s say, 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 a few ideas to fix it:
- Rule of representation and transparency
- “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 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) (or bindings if you prefer). Now the view is a function of 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. 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.
- G̶o̶ ̶b̶a̶c̶k̶ ̶t̶o̶ ̶t̶h̶e̶ ̶o̶c̶e̶a̶n̶s̶.
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 doe 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 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.
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 ptotocols 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. That’s a contract too.
* * * *
There isn’t a 300 IQ man in the heavens writing Viper commandments. The original formulation feels like a second job, that’s why everyone uses a ”Viperish” architecture.
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 be a crutch on your way to free thinking. Dare to experiment, iterate, and fail better.