📜 ⬆️ ⬇️

Why VIPER is a bad choice for your next application.


This post is a free translation of the article Why VIPER is a bad choice for your next application by Sergey Petrov


Over the last year, everyone felt like a VIPER. This architecture really inspires developers. But most of the articles are, in fact, rather biased. They only show the steepness of this architectural pattern, keeping silent about its negative sides. 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 about it, and why it is not suitable for most of your applications.


Some articles on the comparison of architectures, as a rule, state that VIPER is completely different from other MVC architectures. But in fact, 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 "buns" that VIPER offers us (I will refer to this book ). Let's look at goal number two, which deals with SRP (the principle of shared responsibility). It sounds rude, but what kind of crank you need 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 customer problems.


Testing.


This is another interesting aspect and a very important task. On the good side, it would be possible to write a separate article about testing, because many people talk about it, but very few people really test their applications, and even fewer people do it correctly.


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 (nevertheless, Artsy keeps its applications in open-source, and you should look at their projects).


VIPER proposes to separate all logic into many smaller classes with divided responsibilities. This should make testing easier, but not always. Yes, writing a unit test for a simple class is easy, but most of these tests do not test anything. Let's look, for example, at most of the presenter methods : they are only 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 must update these useless tests after each change to the code.


The right approach to testing should include testing the interactor and the presenter right away, because these two parts are strongly related to each other. In addition, since we divide logic into two classes, we need a lot more tests than one class. This is a simple combinatorics: class A has 4 possible states, and class B 6, respectively, their combination has 24 possible states, and you need to test them.


The correct approach to simplify testing is the purity of the code, instead of simply separating the complex code into a bunch of classes.


Oddly enough, testing a view is easier than testing some areas of 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?


Ovingengineering in design.


VIPER is what happens when former Javists break into the world of iOS. - n0damage, comment on reddit

Honestly, can anyone 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 launches an update from the server and there is a twist 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 modified to implement such a simple function. Does anyone remember how Spring started with some abstractions and ended up with AbstractSingletonProxyFactoryBean ? I have always dreamed of a " convenient superclass of proxy factories for proxy factories that create only singletons " in my code.


Excess components



(The redundant code is not as harmless as I used to think. It gives a false signal about its necessity.)


As I mentioned earlier, the presenter is usually a rather stupid class that simply transfers calls from view to interaction (something like that ). Yes, sometimes it contains complex logic, but basically it’s just a redundant component.


"DI-Friendly" number of protocols



(An ugly code is easy to recognize and its cost is easy to estimate. This is not true if the abstraction is wrong.)


There is a general confusion with this abbreviation: VIPER implements the principles of SOLID, where DI is “dependency inversion ”, not “ injection ” (not “ injection ”). Dependency injection is a special case of the "Inversion of Control" pattern, which is certainly related, but different from dependency inversion.


Inversion of dependencies is about the separation of modules of different levels by entering abstractions between them. For example, the UI module should not be directly dependent 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 the data source for your UITableView you use the IoC principle. Using similar names for different high-level things is a source of confusion.


Let's go back to VIPER. There are many protocols (at least 5) between classes within a single module. And all of them are not necessary. Presenter and interactor are not modules from different layers. Applying the IoC principle can make sense, but ask yourself: how often do you have at least two presenters for one view? I’m sure most of you will say never. So why is it necessary to create this bunch of protocols that we will never use?


In addition, because of these protocols, you cannot easily navigate through the code in the IDE. After all, cmd+click will throw you into the protocol, instead of the implementation.


Performance issues


This is a key point, but many simply do not worry about it, or simply underestimate the impact of bad architectural decisions.


I will not talk about the Typhoon framework (which is very popular for introducing dependencies in the world of objective-c). Of course, it has some impact on performance, especially when using automatic injection, but VIPER does not require its use. Instead, I would talk about runtime and application launch, 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, users will not use it. At the last WWDC just talked about optimizing the launch time of applications . 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 invisible. 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 correctly understood.


Rantime calls. It is more complicated and mainly applied only to the Swift compiler (since 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” and not “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, table of calls and sending messages. The latter is the only one that is used in Objective-C, and it is used in Swift when compatibility with Objective-C code is required, or when the method is declared as dynamic . Of course, this part of the runtime will be highly optimized and recorded in an 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 static and call table. Static calls are fast, but the compiler cannot use them without 100% confidence in the types. And when the type of our variable is a protocol, the compiler is forced to use calls using tables. This is not too slow, but one millisecond is here, one is there, and now the total execution time grows by more than one second, compared to what could be achieved with pure Swift code. This point is related to the previous protocols, but I think it is better to separate the concern about the number of unused protocols from messing around 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 from the VIPER community is: "where should I take X?" It turns out that on the one hand there are many rules on how to do everything correctly, and on the other, many decisions are based on someone's opinion. These can be complex cases, such as handling CoreData using an NSFetchedResultsController , or UIWebView . But even common cases, such as using UIAlertController , are a topic of discussion. Let's take a look: here the router interacts with our alert, and here it shows a twist. You can answer that this simple alert is a special case of an alert with no action other than closing.


Special cases are not enough to break the rules.

That's right, but why do we have a factory here to create such alerts? As a result, we have a mess even with UIAlertController . Do you want that?


Code generation


Readability matters.

How can this be an architectural problem? It’s just generating heaps of classes from a template instead of writing them by hand. What is the problem here? The problem is that most of the time you read the code, not write it. So you read the template code mixed with your code most of your time. Is it good? I do not think.


Conclusion


I do not intend to discourage you from using VIPER at all. There may well be applications that only benefit from it. However, before you start developing your application, you should ask yourself a couple of questions:


  1. Will this application have a long life cycle?


  2. Are the requirements stable enough? Otherwise, you will encounter endless refactoring even with minor changes.


  3. Are you really testing your apps? Be honest with yourself.

Only if you answered "yes" to all three questions, could VIPER be a good choice for your application.


And finally, the last thing: you have to make your own decisions. Do not just blindly trust any guy from the Medium (or Habr) who says "Use X, X is cool." This guy might be wrong too.


')

Source: https://habr.com/ru/post/321370/


All Articles