Why VIPER is a bad choice for your next application
There is a lot of hype about VIPER in the last year, and everyone is so inspired by it. Most of these articles are biased about it and trying to demonstrate how cool it is. It’s not. It has at least the same amount of problems, if not even more, as other architecture patterns. In this article, I want to explain why VIPER isn’t as good as it is advertised and isn’t suitable for most of your apps.
Some articles about comparing architectures usually claim that VIPER is an entirely different from any MVC-based architectures. This claim is not true: It’s just a normal MVC, where we are splitting controller into two parts: interactor and presenter. View remains the same, but model is renamed to entity. Router deserves some special words: Yes, it’s true, other architectures don’t promote this part in their abbreviation, but it still exists, in implicit (yes, when you call pushViewController
you are writing a simple router) or more explicit way (for example FlowCoordinators).
Now I want to talk about benefits that VIPER is offering you. I’ll use this book as a complete reference about VIPER. Let’s start with the second goal, conforming to SRP (Single Responsibility Principle). It’s a little rough, but what strange mind can call this a benefit? You’re paid for solving tasks, not for conforming to some buzzword. Yes, you are still using TDD, BDD, unit-tests, Realm or SQLite, dependency injection, and whatever you can remember, but you are using it for solving customer’s problem, not for just using it.
Testing
Another goal is way more interesting. Testability is a very important concern. It deserves a standalone article since many people are talking about it, but only a few are really testing their apps and even fewer are doing it right.
One of the main reasons of that is a lack of good examples. There are many articles about creating a unit test with assert 2 + 2 == 4
but no real-life examples (btw, Artsy is doing an excellent job by open sourcing their apps, you should take a look at their projects).
VIPER is suggesting to separate all logic into a bunch of small classes with separated responsibilities. It may make testing easier, but not always. Yes, writing unit-test for a simple class is easy, but most of this tests are testing nothing. For example, let’s look at most of a presenter’s methods, they are just a proxy between a view and other components. You can write tests for that proxy, it will increase test coverage of your code, but these tests are useless. You also have a side effect: you should update these useless tests after each edit in the main code.
The proper approach to testing should include testing interactor and presenter at the same time because these two parts are deeply integrated. Moreover, because we separate between two classes we need much more tests in comparison with a case when we have just one class. It’s a simple сombinatorics: class A has 4 possible states, class B has 6 possible states, so a combination of A and B has 20 states, and you should test it all.
The right approach to simplify testing is introducing purity to code instead of just splitting complicated state into a bunch of classes.
It may sound strange, but testing views is easier than testing some business code. A view exposes state as a set of properties and inherits visual appearance from these properties. Then you can use FBSnapshotTestCase for matching state with appearance. It still doesn’t handle some edge cases like custom transitions, but how often do you implement it?
Overengineered design
VIPER is what happens when former enterprise Java programmers invade the iOS world. –n0damage, reddit comment
Honestly, can someone look at this and say: “yes, these extra classes and protocols really improved my ability to understand what is going on with my application”.
Imagine simple task: we have a button that triggers an update from a server and a view with data from a server response. Guess how much classes/protocols will be affected by this change? Yes, at least 3 classes and 4 protocols are going to be changed for this simple feature. Does nobody remembers how Spring started with some abstractions and ended with AbstractSingletonProxyFactoryBean
? I always want a “convenient proxy factory bean superclass for proxy factory beans that create only singletons” in my code.
Redundant components
As I already mentioned it before, a presenter is usually a very dumb class which is just proxying calls from view to interactor (something like this). Yes, sometimes it contains complicated logic, but in most cases it’s just a redundant component.
“DI-friendly” amount of protocols
There is a common confusion with this abbreviation: VIPER is implementing SOLID principles where DI means “dependency inversion”, not “injection”. Dependency injection is a special case of Inversion of Control pattern, which is related, but different from Dependency Inversion.
Dependency Inversion is about separating modules from different levels by introducing abstractions between them. For example, UI module shouldn’t directly depends on network or persistence module. Inversion of Control is different, it’s when a module (usually from a library which we can’t change) delegates something to another module, which is typically provided to the first module as a dependency. Yes, when you implement data source for your UITableView
you are using IoC principle. Using the same language features for different high-level purposes is a source of confusion here.
Let’s back to VIPER. There are many protocols (at least 5) between each class inside a module. And they are not required at all. Presenter and Interactor aren’t modules from different layers. Applying IoC principle can make sense, but ask yourself a question: how often do you implement at least two presenters for one view? I believe most of you answered zero. So why it’s required to create this bunch of protocols which we’ll never use?
Also, because of these protocols, you can’t easily navigate your code with an IDE, because cmd+click will navigate you to a protocol, instead of a real implementation.
Performance problems
It is a crucial thing and there are a lot of people who just don’t care about it, or are just underestimating an impact from bad architecture decisions.
I won’t talk about Typhoon framework (which is very popular for dependency injection in the ObjC world). Of course, it has some performance impact, especially when you use auto-injection, but VIPER doesn’t require you to use it. Instead, I want to talk about runtime and app startup and how VIPER is slowing your app literally everywhere.
App startup time. It is rarely discussed, but it’s an important topic, if your app launches very slowly users will avoid using it. There was a session on last WWDC about optimizing app startup time in last year. TL; DR: Your app startup time directly depends on how many classes you have. If you have 100 classes it’s fine, nobody will notice this. However if your app has just 100 classes do you really need this complicated architecture? But if your app is huge, for example, you’re working on Facebook app (18k classes) this impact is enormous, something around 1 second according to the previously mentioned session. Yes, cold launching your app will take 1 second just to load all classes metadata and nothing else, you read it right.
Runtime dispatch. It is more complicated, much harder to profile and mostly applicable for Swift compiler only (because ObjC has rich runtime capabilities and a compiler can’t safely perform these optimizations). Let’s talk about what is going on underhood when you call some method (I use “call” instead of “send a message” because the second term is not always correct for Swift). There are 3 types of dispatch in Swift (from faster to slower): static, table dispatch and sending a message. The last one is the only type that is used in ObjC, and it’s used in Swift when interop with ObjC code is required, or when we declare a method as dynamic
. Of course, this part of the runtime is hugely optimized and written in assembler for all platforms. But what if we can avoid this overhead because the compiler has knowledge about what is going to be called at compile time? It’s exactly what Swift compiler is doing with static and table dispatch. Static dispatch is fast, but the compiler can’t use it without 100% confidence in types in the expression. But when our variable’s type is a protocol the compiler is forced to use dispatch via protocol witness tables. It’s not literally slow, but 1ms here, 1ms there, and now total execution time is more than one second higher than we could achieve using pure Swift code. This paragraph is related to previous about protocols, but I think it’s better to separate concerns about just reckless amount of unused protocols with a real messing with the compiler with it.
Weak abstractions separation
There should be one — and preferably only one — obvious way to do it.
One of the most popular question in VIPER community is “where I should put X?”. So, from the one side, there are many rules how we should do things right, but from another side a lot of stuff is opinion-based. It can be complicated cases, for example handling CoreData with NSFetchedResultsController
, or UIWebView
. But even common cases, like using UIAlertController
, is a topic for discussion. Let’s take a look, here we have a router dealing with alert, but there we have a view presenting an alert. You can try to beat this with an argument that is a simple alert is a special case for simple alert without any actions except closing.
Special cases aren’t special enough to break the rules.
Yeah, but why here we have a factory for creating that kind of alerts? So, we have a mess even with such simple case with UIAlertController
. Do you want it?
Code generation
Readability counts.
How can this be an issue with an architecture? It’s just generating a bunch of template classes instead of writing it by myself. What is the problem with that? The problem is most time you are reading code (unless you are working in a throwaway outsourcing company), not writing it. So you are reading boilerplate code messed with an actual code most of your time. Is it good? I don’t think so.
Conclusion
I don’t pursue a goal to discourage you from using VIPER at all. There is still can be some apps that can benefit from all. However, before you start developing your app you should ask yourself a few questions:
- Is this app is going to have a long lifetime?
- Are specifications stable enough? Alternatively, you can end up with endless huge refactorings even for small changes.
- Do you really test your apps? Be honest with yourself.
Only if you answer “yes” to all questions VIPER may be a good choice for your app.
Finally, the last one: you should use your own brain for your decisions, don’t just blindly trust some guy from Medium or conference your attended who is saying: “Use X, X is cool.” These guys can be wrong too.