Introduction to MVP, MVC, MVVM and VIPER. What is common between them and what is the difference.
Do everything in MVC, and it turns out ugly? Doubt whether to switch to MVVM? Have you heard about VIPER, but are not sure whether it is worth it?
In this article, I will briefly review some of the popular architectural patterns in iOS and compare them in theory and in practice. You will find more information when clicking on the links indicated in the text.
Mastering patterns can be addictive, so be careful: in
In the end, you may be asking yourself more questions than before reading this article, for example:
- Who should own network requests: Model or Controller?
- How can I transfer the Model to the ViewModel of the new View?
- Who creates the new VIPER module: Router or Presenter?
Why it is worth taking care of the choice of architecture?
Because if you do not do this, then one day, debugging a huge class with dozens of different methods and properties, you will not be able to find and correct mistakes in it. Naturally, such a class is difficult to keep in mind as a whole, so you will always lose sight of any important details. If you are already in this situation, then it is very likely that:
- this class is a descendant of UIViewController;
- data is stored directly in UIViewController;
- UIView subclasses are not responsible for anything;
- Model is just a container for data;
- you are not doing unit tests.
And this can happen even if you follow Apple's recommendations and implement their
Cocoa MVC pattern , so don't be upset. “Apple”
MVC is not all right, but we will return to it later.
')
And now let's define the
signs of a good architecture:
- balanced distribution of responsibilities between entities with hard roles;
- testability Usually it follows from the first feature (without panic, this is easy with the appropriate architecture);
- ease of use and low maintenance cost.
Why distribution?
Distribution reduces the load on the brain when we try to figure out how this or that entity works. If you think that the more you develop, the better the brain will adapt to understanding complex concepts - you are right. But everything has a limit, and it is reached rather quickly. Thus, the easiest way to reduce complexity is to divide responsibilities between multiple entities on the
basis of shared responsibility .
Why testability?
The testability of the architecture determines how easy it will be for us to write unit tests, and more often if we can write them in principle. Is it worth testing at all? As a rule, it is not a question for those who have
failed unit tests after adding a new functionality or after refactoring some class subtleties. This means that the tests have
saved the developers from detecting problems in the runtime. What could have happened to the application already on the users device, and the correction would be possible only
in a week .
Why ease of use?
Everything is clear, but it is worth noting that the best code is the code that has never been written. And the less code you have, the fewer errors. Therefore, the desire to write less code does not mean that the developer is lazy. And choosing the smartest solution, you should always consider the cost of its support.
The basics of MV (X)
Today we have many options for architectural design patterns:
The first three of them involve assigning application entities to one of 3 categories:
- Models — responsible for the domain data or data access layer that manipulates data, for example, the Person class or PersonDataProvider ;
- Views - responsible for the presentation layer ( GUI ); For the iOS environment, this is all that begins with the UI prefix;
- Controller / Presenter / ViewModel - an intermediary between Model and View ; In general, he is responsible for Model changes, reacting to user actions performed on the View , and updates the View using changes from the Model .
Having shared entities, we can:
- understand them better;
- reuse them (mostly applicable to View and Model );
- test them separately from each other.
Let's start with the MV (X) patterns and return to VIPER later.MVC
As it was before
Before discussing Apple’s MVC vision, let's look at the
traditional version .
In the traditional MVC
View does not store state in itself.
The controller simply renders the
View when the
Model changes. For example, a web page is completely reloaded after you click on a link to go to another location. Although it is possible to implement traditional MVC in the iOS environment, this does not make much sense due to the architectural problem: all three entities are closely related, each entity
knows about the other two. This greatly reduces the ability to reuse each of the elements. For this reason, we will not even try to write an example of canonical MVC.
Traditional MVC seems inapplicable to modern iOS development.Apple's MVC
Expectations
Controller is an intermediary between
View and
Model , therefore, the last two are not aware of the existence of each other. Therefore, the
Controller is difficult to reuse, but this, in principle, suits us, since we must have a place for that tricky business logic that does not fit into the
Model .
In theory, everything looks very simple, but you feel that something is wrong, right? You have probably heard that people decipher MVC as
Massive View Controller . In addition,
ViewController unloading has become an important topic for iOS developers. Why does this happen if Apple just took the traditional MVC and improved it a little?
Reality
Cocoa MVC encourages you to write
Massive View Controller, because the controller is so involved in the
View life cycle that it is difficult to say that it is a separate entity. Although you still have the opportunity to ship some of the business logic and data transformation in the
Model , when it comes to shipping work in
View , you have few options. In most cases, the entire responsibility of the
View is to send actions to the controller. As a result, everything ends with the View Controller becoming a delegate and data source, as well as a place to start and cancel server requests and, in general, everything.
How many times have you seen this code:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell userCell.configureWithUser(user)
View- cell is configured directly from the
Model . Thus, the principles of MVC are violated, but such code can be seen very often, and, as a rule, people do not understand that this is wrong. If you strictly follow MVC, you must configure the cell inside the controller and not transfer the
Model to the View, which will increase the
Controller even more.
Cocoa MVC is reasonably decoded as Massive View Controller.The problem is not obvious until it comes to
unit tests (I hope that in your project it still comes). Since the View Controller is closely connected with the
View , it becomes difficult to test, and you have to go in a sophisticated way, replacing the
View with Mock objects and simulating their life cycle, as well as writing the View Controller code so that the business logic is maximally separated from the code view layout.
Let's look at a simple example from the playground:
import UIKit struct Person {
The MVC build can be performed in a “presenting” View Controller.It seems to be hard to test, right? We can allocate the generation of greetings to the new
GreetingModel class and test it separately, but we cannot test the presentation logic (even though there is not much of it in the example) inside the
GreetingViewController without calling the
View life cycle methods directly (
viewDidLoad, didTapButton ), which can lead to loading all UIView, and this is bad for unit tests.
In fact, testing UIViews on one simulator (for example, iPhone 4S) does not guarantee that it will work properly on other devices (for example, iPad), so I recommend removing the
Host Application tick from the unit test configuration and running it on simulator, not including the application itself.
The interaction between View and Controller is not really amenable to testing with unit tests .After all this, it may seem that Cocoa MVC is a rather bad choice of pattern. But let's evaluate it in terms of
features of a good architecture , defined at the beginning of the article:
- distribution : View and Model are actually separated, but View and Controller are closely related;
- testability : due to poor distribution you are likely to test only the Model ;
- ease of use : the least amount of code among other patterns. In addition, it looks understandable, so even an inexperienced developer can easily support it.
Cocoa MVC is a smart choice if you are not willing to invest a lot of time in your architecture and feel that the higher service cost pattern is not affordable for your small project or startup.
Cocoa MVC is the best architectural pattern in terms of speed of development.MVP
Implementing Cocoa MVC Promises
Doesn't it look like an apple MVC? In fact - very, and his name -
MVP (option with a passive
View ). But does this mean that Apple's MVC is in fact MVP? No, it is not, because, as you remember,
View and
Controller are closely related there, while the MVP intermediary
Presenter is not related to the View Controller life cycle.
The View can be easily replaced by
Mock objects , so
Presenter has no layout code, but it is responsible for updating the
View with new data and state.
- What if I tell you that the UIViewController is a View .
From the MVP point of view, the UIViewController subclasses are actually
View , not
Presenter . This distinction provides excellent testability, which comes at the expense of development speed, because you have to manually link data and events between
View and
Presenter , as can be seen in the example below.
import UIKit struct Person {
Important note regarding assembly
MVP is the first pattern to reveal an assembly problem that occurs due to the presence of three
really separate layers. Since we do not need
View to know about the
Model , it is wrong to build in the presenting View Controller (which is actually
View ), therefore, it needs to be done elsewhere. For example, you can create a
Router service that will be responsible for building and presenting a
View-to-View . This problem occurs not only in MVP, it also needs to be addressed in
all subsequent patterns .
Let's look at the
signs of a good architecture for MVP:
- distribution : most responsibility is shared between Presenter and Model , and View does nothing;
- testability : excellent, we can test most of the business logic through inaction View;
- ease of use : in our unrealistically simple example, the amount of code is twice as large as MVC, but at the same time the idea of ​​MVP is very simple.
MVP in iOS means excellent testability and a lot of code.MVP
With Blackjack and Binding
There is another option MVP - MVP with a supervisory controller. It includes the direct binding of
View and
Model , while the
Presenter (supervisory controller) still handles the actions of the
View and is able to modify it.
But, as we learned earlier, the vague division of responsibility is bad in itself, as well as the close connection between
View and
Model . And I see no point in writing an example for bad architecture.
MVVM
The newest of the MV (X) species.
MVVM is the newest of the MV (X) patterns, so let's hope that it appeared with all the problems inherent in MV (X).
In theory, the Model-View-ViewModel looks very good.
View and
Model are already familiar to us, as is the
View Model as an intermediary.
It is very similar to MVP:
- MVVM treats the View Controller as a View ;
- there is no close relationship between View and Model .
In addition, he does the binding as a supervising version of MVP, but not between
View and
Model , but between
View and
View Model .
So what is a
View Model in iOS? This is a UIKit
independent View view and its state.
View Model causes changes in the
Model and independently updated with the already updated
Model . And since the binding occurs between the
View and the
View Model , the first one, respectively, is also updated.
Binding
I mention them, starting with the MVP part, but let's take a closer look at them. Bindings are available out of the box for developing OS X, but they are not in the arsenal of an iOS developer. Of course, we have KVO and Notifications, but they are not as comfortable as binding.
Therefore, provided that we do not want to write them ourselves, you can choose:
Today, in fact, when you hear MVVM, you think about ReactiveCocoa, and vice versa. Although you can do MVVM with simple bindings, ReactiveCocoa (or his classmates) will allow you to squeeze everything from the MVVM pattern.
There is one bitter truth about FRP frameworks: great power comes with great responsibility. It is very easy to break everything when you write
reactively . In other words, if something went wrong, you can spend a lot of time debugging the application. Just take a look at this call stack.
In our simple example, a reactive framework or even KVO is redundant. We explicitly ask the
View Model to update it using the
showGreeting method, and use a simple property for the callback
greetingDidChange function to learn about the changes.
import UIKit struct Person {
And back to our assessment of
signs of good architecture :
- distribution : from our tiny example, this is unclear, but in fact in MVVM View has more responsibilities than View from MVP. Because the first one updates its state with the View Model by setting up binders, while the second sends all events to the Presenter and does not update itself (this is done by the Presenter);
- testability : View Model does not know anything about the view, it allows us to easily test it. The view can also be tested, but since it depends on UIKit, you can just skip this;
- ease of use : the same amount of code as in our MVP example, but in a real application, where you have to send all the events from View to Presenter and update the View manually, MVVM will be much slimmer (if you use binding).
MVVM is a very attractive pattern, as it combines the advantages of the aforementioned approaches and does not require additional code for updating the View in connection with the side view bindings. However, testability is still at a good level.VIPER
Building experience from Lego cubes, transferred to the design of iOS applications
VIPER is our last candidate, which is especially interesting because it is not from the category MV (X).
By now you should already agree that the division of responsibilities is very good. VIPER takes another step towards the separation of duties and instead of the usual three layers offers
five .
- Interactor contains the business logic associated with data ( Entities ): for example, creating new instances of entities or getting them from the server. For these purposes, you will use some Services and Managers, which are viewed more as external dependencies, and not as part of the VIPER module.
- Presenter contains UI-related business logic (but is UIKit-independent) that calls methods in an Interactor .
- Entities are simple data objects that are not a data access layer, because it is the responsibility of the Interactor layer.
- Router is responsible for transitions between VIPER modules .
In principle, the VIPER module can be a single screen or a whole user story of your application (for example, authentication can be on one screen or several related screens). It’s up to you how small your “lego-blocks” will be.
If we compare VIPER with MV (X) -type patterns, we will see several differences in the distribution of responsibilities:
- The logic from the Model (data interaction) is shifted to Interactor , and there are Entities , data structures that do nothing;
- from the Controller , Presenter , ViewModel , the UI view duties moved to the Presenter , but without the possibility of changing the data;
- VIPER is the first template that tries to solve the problem of navigation, there is a Router for this.
The fact that MV (X) -patterns do not solve the routing problem does not mean that it does not exist for iOS applications.In the example, there is no routing or interaction between modules, since these topics are not covered at all by the MV (X) -parts.
import UIKit struct Person {
And yet, once again return to the
signs .
- Distribution Undoubtedly, VIPER is a champion in the distribution of responsibilities.
- Testability There is nothing surprising: better distribution - better testing.
- Ease of use . As you may have guessed, the first two benefits come at the cost of escort. You will have to write a huge number of interfaces for classes with minor responsibilities.
So what about Lego?
When using VIPER, it may seem to you that you are building the Empire State Building from Lego cubes, and this suggests that you
have problems . Maybe you took up VIPER too early and it is worth looking at something simpler. Some people ignore it and continue to shoot from a cannon on sparrows. I assume that they believe that their applications will benefit from VIPER sometime in the future, even if the cost of service is now unreasonably high. If you think it's worth it, then I recommend you try
Generamba , a tool for generating VIPER skeletons. Although personally it seems to me that this is akin to using an
automatic sight for shooting from the same gun instead of a
slingshot .
Conclusion
We looked at several architectural patterns, and I hope that you have found answers to some of your questions. I have no doubt that you understood that
there is no “silver bullet” among the patterns, and the choice of architecture is a matter of weighing compromises in your particular situation.
It seems to me quite natural to combine several architectures in one application. For example, you started with MVC, but having understood that a particular screen (use case) became too difficult to maintain with MVC, you switched to MVVM, but only for that particular screen. Because in fact there is no need to refactor other screens for which MVC work perfectly, especially since both architectures are easily compatible.
Make it as simple as possible, but not simpler. (c) Albert Einstein
An English version is available here . The slides that I presented at NSLondon are available here .Bogdan Orlov,
iOS developer in Badoo