Why VIPER is a bad choice for your next app
This post is a free translation of the article Why VIPER is a bad choice for your next application by Sergey Petrov
Over the past year, everyone wrote about VIPER. This architecture really inspires developers. But most articles, in fact, are pretty biased. They only show the steepness of this architectural pattern, silent about its negative aspects. But he has no less problems (and maybe even more) than others. And in this article I will try to explain why VIPER is not at all as good as they say, and why it is not suitable for most of your applications.
Some articles comparing architectures typically claim that VIPER is completely different from other MVC architectures. But in reality, VIPER is just a normal MVC, where the controller is divided into two parts: interactor and presenter. View remained in place, and the model was renamed to entity. Router deserves special attention: yes, other architectures do not mention this part in their abbreviations, but it is also present in them: implicitly (when you call
pushViewController, you create a simple router) or more obvious (as an example, FlowCoordinators).
Let's talk about the "goodies" that VIPER offers us (I will refer to this book ). Let's look at goal number two, which refers to SRP (the principle of shared responsibility). It sounds rude, but what kind of eccentric does one have to be to consider this an advantage? You get paid for solving problems, not for matching fashionable words. Yes, you still use TDD, BDD, unit testing, Realm or SQLite, dependency injection and many, many other things, but you use all this not just for the sake of use, but to solve client problems.
This is another interesting aspect and a very important task. On the good side, one could write a separate article about testing, because many people talk about it, but few really test their applications, and even fewer people do it right.
One of the main reasons is that there are no good examples. You can find quite a few articles on how to write a unit test
assert 2 + 2 == 4, but you will not find real examples (however, Artsy keeps its applications open-source, and you should take a look at their projects).
VIPER proposes to separate all logic into many small classes with shared responsibilities. This should make testing easier, but not always so. Yes, writing a unit test for a simple class is easy, but most of these tests don't test anything. Let's look at, for example, most of the presenter methods : they are just proxies between the view and other components. You can write tests for this proxy, this will increase the test coverage of your code, but these tests are useless. And you will have a side effect: you have to update these useless tests after each change to the code.
The correct approach to testing should include testing the interactor and the presenter immediately, because these two parts are strongly connected with each other. In addition, since we split the logic into two classes, we need a lot more tests than a single class. This is a simple combinatorics: a class
Ahas 4 possible states, and a class has
B6, respectively, their combination has 24 possible states, and you need to test them.
The right approach to simplify testing is to keep the code clean, rather than just splitting complex code into a bunch of classes.
Oddly enough, testing a view is easier than testing some parts of the business logic. A view is only a collection of certain properties and inherits the appearance of these properties. You can use FBSnapshotTestCase to compare their state with appearance. This thing still doesn't handle some special cases like custom transitions, but how often do you use them?
Overengineering in design.
VIPER is what happens when former javists break into the iOS world. - n0damage, comment on reddit
Honestly, can someone look at this and say: "yes, these additional classes and protocols really improve my understanding of what is happening in my application."
Imagine a simple task: there is a button that starts the update from the server and there is a view with the data received from the server. Guess how many classes / protocols will be affected by this change? Yes, at least 3 classes and 4 protocols will be changed to implement such a simple function. Does anyone remember how Spring started with some abstractions and ended with
AbstractSingletonProxyFactoryBean? I always dreamed of a " convenient proxy factory superclass for proxy factories that only create singletones " in my code.
(Redundant code is not as harmless as I used to think. It gives a false signal about its necessity.)
As I mentioned earlier, a presenter is usually a pretty stupid class that just passes calls from the view to the interactor (something like this ). Yes, sometimes it contains complex logic, but basically, it's just a redundant component.
"DI-friendly" number of protocols
(Ugly code is easy to recognize, and its cost is easy to evaluate. It's not so if the abstraction is wrong.)
There is a common confusion with this reduction: VIPER implements the SOLID principles, where DI - this " inversion of dependency", rather than " implementation " ( "the dependency inversion ", and not " injection "). Dependency injection is a special case of the "Inversion of Control" pattern, which, of course, is connected, but differs from dependency inversion.
Dependency Inversion is about separating modules of different levels by introducing abstractions between them. For example, a UI module should not directly depend on the network module. Inversion of control is another. This is when a module (usually from a library that we cannot change) delegates something to another module, which is usually provided to the first module as a dependency. Yes, when you implement a data source for yours,
UITableViewyou use the IoC principle. Using similar names for various high-level things is a source of confusion.
Back to VIPER. There are many protocols (at least 5) between classes within the same module. And all of them are not necessary. Presenter and interactor are not modules from different layers. Applying the IoC principle may make sense, but ask yourself: how often do you get at least two presenters for one view? I am sure that most of you will answer "never." So why is it necessary to create this bunch of protocols that we will never use?
In addition, due to these protocols, you cannot easily navigate the code in the IDE. After all,
cmd+clickit will throw you into the protocol, instead of implementing it.
This is a key point, but many simply don’t worry about it, or simply underestimate the impact of poor architecture.
I will not talk about the Typhoon framework (which is very popular for dependency injection in the objective-c world). Of course, it has some impact on performance, especially when using automatic deployment, but VIPER does not require its use. Instead, I would talk about runtime and launching the application, and how VIPER slows down your application literally everywhere.
Application launch time. This topic is rarely discussed, but it is important. After all, if your application starts very slowly, then users will not use it. At the last WWDC, they were just talking about optimizing application launch time . The start time of your application depends on the number of classes in it. If you have 100 classes - this is normal, the delay will be imperceptible. However, if your application has only 100 classes, do you really need this complex architecture? But if your application is huge, for example, you are working on a Facebook application ( 18K classes ), then the difference will be noticeable: about one second. Yes, the cold start of your application will take 1 second only to load all the class metadata and nothing else, you understand correctly.
Rantime challenges. Here everything is more complicated and mainly applies only to the Swift compiler (since the Objective-C runtime has more features and the compiler cannot safely perform optimizations). Let's talk about what happens “under the hood” when you call a method (I say “call” rather than “send a message” because the second is not always correct for Swift). There are three types of calls in Swift (from fast to slow): static, call table and sending messages. The latter is the only one used in Objective-C, and it is used in Swift when compatibility with Objective-C code is required, or when a method is declared as
dynamic. Of course, this part of the runtime will be highly optimized and written in assembler.for all platforms. But what if we can avoid this overhead by giving the compiler an idea of what exactly will be called during compilation? This is exactly what the Swift compiler does with a static and call table. Static calls are fast, but the compiler cannot use them without 100% type assurance. And when the type of our variable is a protocol, the compiler is forced to use table calls. This is not too slow, but one millisecond is here, one is there, and now the total runtime is increasing by more than one second, compared to what could be achieved with a clean Swift code. This item is related to the previous one about protocols, but I think it's better to separate the concern about the number of unused protocols from the fuss with the compiler.
Weak separation of abstractions
There should be one and, preferably, only one obvious way to do this.
One of the most popular questions of the VIPER community: "where should I take X?" It turns out that on the one hand there are many rules on how to do things right, and on the other, many decisions are based on someone else’s opinion. These can be complex cases, such as processing
UIWebView. But even common cases, such as use
UIAlertController, are a topic of discussion. Let's take a look: here a router interacts with our alert, but here it shows a view. You can answer that this simple alert is a special case of an alert without any action other than closing.
Special cases are not enough to break the rules.
How can this be an architectural problem? It is just generating a bunch of classes from a template instead of writing them manually. What is the problem here? The problem is that most of the time you are reading code, not writing it. This way you read boilerplate code mixed with your code most of your time. Is it good? I don’t think so.
My goal is not to dissuade you from using VIPER at all. There may well be applications that only benefit from it. However, before you begin developing your application, you should ask yourself a couple of questions:
Will this app have a long life cycle?
Are the requirements stable enough? Otherwise, you will encounter endless refactoring even when making minor changes.
- Are you really testing your apps? Be honest with yourself.
Only if you answered yes to all three questions, VIPER could be a good choice for your application.
And finally, the last: you have to make your own decisions. Do not just blindly trust some guy from Medium (or Habr) who says "Use X, X is cool." This guy can be wrong too.