If you have to ask, maybe. It’s better than nothing.
- iOS architecture
- Final thoughts
The work of a programmer is writing the code, then structure the resulting complexity to provide a high level view. That is, you create the engine pieces, then hide the engine in the trunk and expose an accelerator pedal. Correctly done, the application is divided in layers of abstraction separated by well defined interfaces. Low level details can be ignored, while high level components become obvious.
Distribute responsibilities in entities with strict roles.
A container’s name should reflect its contents. If it doesn’t, you have to waste time 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 more than 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 point 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. Because the view is the element that gets deallocate by UIKit, it’s the element that strongly references the other, so the closure goes on the presenter.
Viper is SRP with the novelty of (mistakenly) saying: every screen should have these elements because you’ll probably need them.
Eight thousand pages explaining that. Between this and killing the bees, extinction is only logical.
Any architectural style is rooted on basic design principles (SOLID, GRASP, etc.) and provides consistency. But an architectural style alone is not architecture. Design doesn’t end with the broad strokes of a particular style.
You can do Viper, and still mess up. A common anti-pattern is to update individual elements of a view from multiple points. Doing so is prone to errors and hard to debug, because it defines your view as a function of a sequence of events, which leads to race conditions. Instead, you should create an object with all data needed to fully update the view, and pass it from a single point. Design principles tell you why:
- Rule of representation and transparency
- The Value Is the Boundary
- Split Phase: read all the data, process all the data, then update all the data.
- Concurrency is hard, so avoid it if you can.
Design principles to the rescue. What if, we apply them to Apple MVC?
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.
If you write a complex app that uses most of the app delegate, it will grow to thousands of lines. However, it’s easy to design around it.
To refactor massive app delegates
- Create a different object per responsibility.
- Conform them to a protocol extended with empty implementations.
- Override the relevant methods for each object.
- Delegate the calls.
Should Apple provides those objects as app delegate variables? No. There would be use cases they can’t possible 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.
To refactor view controllers
- Treat them as views: compose controllers and reuse them across screens.
- Move non view stuff elsewhere.
So here is the thing. Writing a 2000 lines class to observe, download, persist, and transform data is an obvious design mistake for which, there is a solution. However, not everyone knows how to find it:
I don’t consider myself a bad programmer, but even I am unable to write good code without a strong framework to base my work on. Object-Oriented Programming — The Trillion Dollar Disaster
This is a great example of why we should question things. He can’t write code without a framework so he blames OOP with a critique that explains why he can’t:
…it encourages promiscuous sharing of mutable state.
…introduces additional complexity with its numerous design patterns.
OOP code is non-deterministic… the dependencies of the Calculator object might change the result
That’s just the first section. Bad practices lead to bad code, and rants lead to flawed logic.
I get it tho. Some projects derail, and resorting to a dumbed down way of doing things (Viper), is easier than actually learning. But if life on training wheels is our future we may as well go back to the oceans.
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 tho, and here is what happened.
- Each element has a single responsibility.
- Every screen has the same five components: (view, presenter, interactor, router, entity). This consistency lets people find things.
In OOP the goal is to decompose a problem in entities of the domain. For instance, in a design of a parking lot design there are objects like ParkingTicket, TicketScanner, etc. Nowhere in the domain there is something called Interactor. What is even the meaning of the word interactor?
However, because mobile applications are often glorified tables and entry forms, Viper takes a one-size-fits-all design 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 Viper work? often it does. You probably need those five roles per screen, and the problem may be small enough to not 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).
The presenter adds nothing in this interaction: 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? If you create a middle man for consistency shake you are 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.
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 with modular elements. Make each a Viper module and 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.
Kent Beck defined simple design in 1990:
- Passes the tests
- Reveals intention
- No duplication
- Fewest elements
The intention of a button in Viper is calling the presenter, and then… ready your debugger because we are ten classes away from actually performing a meaningful action. That breaks #2, #3, and #4.
I would tell you to remove it entirely, but if you must do Viper consider this:
Why are you writing a different protocol per component?
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). Second option avoids mocks, and thus, protocols.
- 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.
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 “Viper-ish” architecture. You should too.
Men almost always walk in paths beaten by others and act by imitation.
We do, it saves a lot of energy. But that is a crutch until you become a free thinker. Dare to experiment, learn, iterate, and fail better.