Comparaison des architectures Viper et MVVM: comment appliquer les deux



Actuellement, VIPER et MVVM sont les solutions architecturales les plus populaires utilisées dans le développement de grandes applications qui nécessitent une participation au développement de grandes équipes bien testées, prises en charge à long terme et en constante évolution. Dans cet article, nous allons essayer de les appliquer sur un petit projet de test, qui est une liste de contacts utilisateur avec la possibilité d'ajouter un nouveau contact. Cet article a plus de pratique que l'analyse, et il est principalement destiné à ceux qui sont déjà en théorie familiers avec ces architectures et qui aimeraient maintenant comprendre comment cela fonctionne avec des exemples spécifiques. Cependant, une description de base des architectures et leur comparaison est également présente.


Cet article est une traduction de l'article de Rafael Sacchi «Comparaison des architectures MVVM et Viper: quand utiliser l'une ou l'autre» . Malheureusement, à un moment donné de la création de l'article, la «publication» a été mise en place au lieu de la «traduction», vous devez donc écrire ici.

Une architecture bien conçue est essentielle pour assurer un soutien continu à votre projet. Dans cet article, nous examinerons les architectures MVVM et VIPER comme une alternative au MVC traditionnel.

MVC est un concept bien connu de tous ceux qui sont impliqués dans le développement de logiciels depuis un certain temps. Ce modèle divise le projet en trois parties: modèle représentant des entités; View, qui est une interface pour l'interaction avec l'utilisateur; et le contrôleur, chargé d'assurer l'interaction entre la vue et le modèle. C'est l'architecture qu'Apple nous propose d'utiliser dans nos applications.

Cependant, vous savez probablement que les projets comportent de nombreuses fonctionnalités complexes: prise en charge des requêtes réseau, analyse, accès aux modèles de données, conversion des données pour la sortie, réaction aux événements d'interface, etc. En conséquence, vous obtenez d'énormes contrôleurs qui résolvent les tâches ci-dessus et un tas de code qui ne peut pas être réutilisé. En d'autres termes, MVC peut être un cauchemar pour un développeur avec un support de projet à long terme. Mais comment garantir une modularité et une réutilisabilité élevées dans les projets iOS?

Nous examinerons deux alternatives très célèbres à l'architecture MVC: MVVM et VIPER. Les deux sont assez célèbres dans la communauté iOS et ont prouvé qu'ils peuvent être une excellente alternative à MVC. Nous allons parler de leur structure, écrire un exemple d'application et considérer les cas où il vaut mieux utiliser l'une ou l'autre architecture.

Exemple

Nous allons écrire une application avec un tableau de contacts utilisateurs. Vous pouvez utiliser le code de ce référentiel . Dans les dossiers de démarrage, le squelette de base du projet est contenu, et dans les dossiers finaux est une application entièrement terminée.

L'application aura deux écrans: sur le premier il y aura une liste de contacts affichés dans un tableau, dans la cellule il y aura le prénom et le nom du contact, ainsi que l'image de base au lieu de l'image de l'utilisateur.



Le deuxième écran est l'écran pour ajouter un nouveau contact, avec les champs de saisie du prénom et du nom et les boutons Terminé et Annuler.



MVVM

Comment ça marche:

MVVM signifie Model-View-ViewModel . Cette approche diffère de MVC dans la logique de répartition des responsabilités entre les modules.

  • Modèle : Ce module n'est pas différent de celui de MVC. Il est responsable de la création de modèles de données et peut contenir une logique métier. Vous pouvez également créer des classes d'assistance, par exemple, telles qu'une classe de gestionnaire pour gérer les objets dans Model et un gestionnaire de réseau pour traiter les requêtes réseau et l'analyse.
  • Vue : Et ici, tout commence à changer. Le module View de MVVM couvre l'interface (sous-classes des fichiers UIView, .xib et .storyboard), la logique d'affichage (animation, rendu) et la gestion des événements utilisateur (pressions sur les boutons, etc.). View et Controller en sont responsables dans MVC. Cela signifie que les vues que vous avez resteront inchangées, tandis que le ViewController contiendra une petite partie de ce qu'il contenait dans MVC et, par conséquent, diminuera considérablement.
  • ViewModel : C'est maintenant l'endroit où se trouvera la plupart du code que vous aviez précédemment dans ViewController. La couche ViewModel demande des données au modèle (il peut s'agir d'une demande vers une base de données locale ou une demande réseau) et les retransmet à la vue, déjà au format dans lequel elles seront utilisées et affichées. Mais il s'agit d'un mécanisme bidirectionnel, les actions ou données saisies par l'utilisateur passent par le ViewModel et mettent à jour le modèle. Étant donné que le ViewModel garde une trace de tout ce qui est affiché, il est utile d'utiliser le mécanisme de liaison entre les deux couches.


Par rapport à MVC, vous passez d'une architecture qui ressemble à ceci:



Vers la prochaine variante d'architecture:



Dans lequel les classes et sous-classes d'UIView et UIViewController sont utilisées pour implémenter la vue.

Eh bien, maintenant au point. Écrivons un exemple de notre application utilisant l'architecture MVVM.

Application Contacts MVVM

MODÈLE

La classe suivante est un modèle de contact Contact :

import CoreData open class Contact: NSManagedObject { @NSManaged var firstName: String? @NSManaged var lastName: String? var fullName: String { get { var name = "" if let firstName = firstName { name += firstName } if let lastName = lastName { name += " \(lastName)" } return name } } } 


La classe de contact a les champs firstName , lastName , ainsi que la propriété fullName calculée.

VOIR

VIEW comprend: Storyboard principal, avec des vues déjà placées dessus; ContactsViewController, qui affiche une liste de contacts dans un tableau; et AddContactViewController avec une paire d'étiquettes et de champs de saisie pour ajouter le nom et le prénom du nouveau contact. Commençons par le ContactsViewController . Son code ressemblera à ceci:

 import UIKit class ContactsViewController: UIViewController { @IBOutlet var tableView: UITableView! let contactViewModelController = ContactViewModelController() override func viewDidLoad() { super.viewDidLoad() tableView.tableFooterView = UIView() contactViewModelController.retrieveContacts({ [unowned self] in self.tableView.reloadData() }, failure: nil) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { let addContactNavigationController = segue.destination as? UINavigationController let addContactVC = addContactNavigationController?.viewControllers[0] as? AddContactViewController addContactVC?.contactsViewModelController = contactViewModelController addContactVC?.didAddContact = { [unowned self] (contactViewModel, index) in let indexPath = IndexPath(row: index, section: 0) self.tableView.beginUpdates() self.tableView.insertRows(at: [indexPath], with: .left) self.tableView.endUpdates() } } } extension ContactsViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") as? ContactsTableViewCell guard let contactsCell = cell else { return UITableViewCell() } contactsCell.cellModel = contactViewModelController.viewModel(at: (indexPath as NSIndexPath).row) return contactsCell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactViewModelController.contactsCount } } 


Même avec un coup d'œil rapide, il est clair que cette classe implémente pour la plupart des tâches d'interface. Il a également la navigation dans la méthode prepareForSegue (: :) - et c'est exactement le moment qui changera dans VIPER lors de l'ajout d'une couche de routeur.

Examinons de plus près l'extension de classe qui implémente le protocole UITableViewDataSource. Les fonctions ne fonctionnent pas directement avec le modèle de contact de l'utilisateur Contact dans la couche Modèle - à la place, elles reçoivent des données (représentées par la structure ContactViewModel) sous la forme dans laquelle elles seront affichées, déjà formatées à l'aide de ViewModelController.

La même chose se produit dans un circuit, qui commence immédiatement après la création d'un contact. Sa seule tâche est d'ajouter une ligne au tableau et de mettre à jour l'interface.

Vous devez maintenant établir une relation entre la sous-classe de UITableViewCell et ViewModel. Cela ressemblerait à la classe de cellules de la table ContactsTableViewCell :

 import UIKit class ContactsTableViewCell: UITableViewCell { var cellModel: ContactViewModel? { didSet { bindViewModel() } } func bindViewModel() { textLabel?.text = cellModel?.fullName } } 


Et la classe AddContactViewController aussi :

 import UIKit class AddContactViewController: UIViewController { @IBOutlet var firstNameTextField: UITextField! @IBOutlet var lastNameTextField: UITextField! var contactsViewModelController: ContactViewModelController? var didAddContact: ((ContactViewModel, Int) -> Void)? override func viewDidLoad() { super.viewDidLoad() firstNameTextField.becomeFirstResponder() } @IBAction func didClickOnDoneButton(_ sender: UIBarButtonItem) { guard let firstName = firstNameTextField.text, let lastName = lastNameTextField.text else { return } if firstName.isEmpty || lastName.isEmpty { showEmptyNameAlert() return } dismiss(animated: true) { [unowned self] in self.contactsViewModelController?.createContact(firstName: firstName, lastName: lastName, success: self.didAddContact, failure: nil) } } @IBAction func didClickOnCancelButton(_ sender: UIBarButtonItem) { dismiss(animated: true, completion: nil) } fileprivate func showEmptyNameAlert() { showMessage(title: "Error", message: "A contact must have first and last names") } fileprivate func showMessage(title: String, message: String) { let alertView = UIAlertController(title: title, message: message, preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "Ok", style: .destructive, handler: nil)) present(alertView, animated: true, completion: nil) } } 


Et encore une fois, le travail avec l'interface utilisateur se poursuit principalement ici. Notez que AddContactViewController délègue la fonctionnalité de création de contact au ViewModelController dans la fonction didClickOnDoneButton (:) .

VOIR LE MODÈLE

Il est temps de parler de la toute nouvelle couche ViewModel pour nous. Tout d'abord, créez une classe de contacts ContactViewModel qui fournira la vue que nous devons afficher, et les fonctions <et> avec paramètres seront définies pour trier les contacts:

 public struct ContactViewModel { var fullName: String } public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() < rhs.fullName.lowercased() } public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() > rhs.fullName.lowercased() } 


Le code ContactViewModelController ressemblera à ceci:

 class ContactViewModelController { fileprivate var contactViewModelList: [ContactViewModel] = [] fileprivate var dataManager = ContactLocalDataManager() var contactsCount: Int { return contactViewModelList.count } func retrieveContacts(_ success: (() -> Void)?, failure: (() -> Void)?) { do { let contacts = try dataManager.retrieveContactList() contactViewModelList = contacts.map() { ContactViewModel(fullName: $0.fullName) } success?() } catch { failure?() } } func viewModel(at index: Int) -> ContactViewModel { return contactViewModelList[index] } func createContact(firstName: String, lastName: String, success: ((ContactViewModel, Int) -> Void)?, failure: (() -> Void)?) { do { let contact = try dataManager.createContact(firstName: firstName, lastName: lastName) let contactViewModel = ContactViewModel(fullName: contact.fullName) let insertionIndex = contactViewModelList.insertionIndex(of: contactViewModel) { $0 < $1 } contactViewModelList.insert(contactViewModel, at: insertionIndex) success?(contactViewModel, insertionIndex) } catch { failure?() } } } 


Remarque: MVVM ne donne pas une définition exacte de la façon de créer un ViewModel. Lorsque je souhaite créer une architecture plus stratifiée, je préfère créer un ViewModelController qui interagira avec la couche Model et sera responsable de la création des objets ViewModel.

La chose principale qui est très facile à retenir: la couche ViewModel ne doit pas être impliquée dans le travail avec l'interface utilisateur. Pour éviter cela, il est préférable de ne jamais importer UIKit dans un fichier avec ViewModel.

La classe ContactViewModelController demande des contacts au stockage local et essaie de ne pas affecter la couche Model. Il renvoie les données dans le format dont la vue doit être affichée et avertit la vue lorsqu'un nouveau contact est ajouté et que les données changent.

Dans la vie réelle, ce serait une demande de réseau, et non une demande à la base de données locale, mais dans aucun des cas aucun d'entre eux ne devrait faire partie du ViewModel - le travail en réseau et le travail avec la base de données locale devraient être fournis en utilisant leurs propres gestionnaires ( gestionnaires).

C'est tout sur MVVM. Peut-être que cette approche vous semblera plus testable, prise en charge et distribuée que MVC. Parlons maintenant de VIPER et voyons en quoi il diffère de MVVM.

VIPER

Comment ça marche:

VIPER est une implémentation de Clean Architecture pour les projets iOS. Sa structure se compose de: View, Interactor, Presenter, Entity et Router. Il s'agit vraiment d'une architecture très distribuée et modulaire qui vous permet de partager les responsabilités, est très bien couverte par les tests unitaires et rend votre code réutilisable.

  • Vue : une couche d'interface qui implique généralement des fichiers UIKit (y compris UIViewController). Il est compréhensible que dans des systèmes plus distribués, les sous-classes de UIViewController soient liées à View. Dans VIPER, les choses sont presque les mêmes que dans MVVM: View est responsable d'afficher ce que Presenter donne et de transmettre les informations ou actions saisies par l'utilisateur à Presenter.
  • Interacteur : contient la logique métier nécessaire au fonctionnement de l'application. Interactor est responsable de la récupération des données du modèle (demandes réseau ou locales) et sa mise en œuvre n'est en aucun cas liée à l'interface utilisateur. Il est important de se rappeler que les gestionnaires de réseau et locaux ne font pas partie de VIPER, mais sont traités comme des dépendances distinctes.
  • Présentateur : responsable du formatage des données à afficher dans la vue. Dans MVVM dans notre exemple, ViewModelController était responsable de cela. Presenter reçoit des données d'Interactor, crée une instance de ViewModel (une classe formatée pour un affichage correct) et la transmet à View. Il répond également à la saisie des données par l'utilisateur, demande des données supplémentaires à la base de données, ou vice versa, les lui transmet.
  • Entité : prend une partie de la responsabilité de la couche modèle, qui est utilisée dans d'autres architectures. L'entité est un simple objet de données, sans logique métier, géré par un tracteur en ligne et divers gestionnaires de données.
  • Routeur : toute la logique de navigation des applications. Il peut sembler que ce n'est pas la couche la plus importante, mais si vous devez, par exemple, réutiliser la même vue sur l'iPhone et l'application pour iPad, la seule chose qui peut changer est la façon dont vos vues apparaissent à l'écran. Cela vous permet de ne plus toucher les couches sauf le routeur, qui en sera responsable dans chaque cas.


Comparé à MVVM, VIPER présente plusieurs différences clés dans la répartition des responsabilités:

"il a un routeur, une couche séparée responsable de la navigation"

- Les entités sont de simples objets de données, redistribuant la responsabilité d'accéder aux données du modèle à l'interacteur

- Les responsabilités de ViewModelController sont partagées entre l'interacteur et le présentateur

Et maintenant, répétons la même application, mais déjà sur VIPER. Mais pour faciliter la compréhension, nous ne ferons qu'un contrôleur avec des contacts. Vous pouvez trouver le code du contrôleur pour ajouter un nouveau contact dans le projet en utilisant le lien (dossier VIPER Contacts Starter dans ce référentiel ).

Remarque : Si vous décidez de créer votre projet sur VIPER, vous ne devez pas essayer de créer tous les fichiers manuellement - vous pouvez utiliser l'un des générateurs de code, par exemple, comme VIPER Gen ou Generamba (projet Rambler) .

Application Contacts VIPER

VOIR

VIEW est représenté par des éléments de Main.storyboard et de la classe ContactListView. VIEW est très passif; ses seules tâches sont de transférer les événements d'interface à Presenter et de mettre à jour son état, sur notification du Presenter. Voici à quoi ressemble le code ContactListView :

 import UIKit class ContactListView: UIViewController { @IBOutlet var tableView: UITableView! var presenter: ContactListPresenterProtocol? var contactList: [ContactViewModel] = [] override func viewDidLoad() { super.viewDidLoad() presenter?.viewDidLoad() tableView.tableFooterView = UIView() } @IBAction func didClickOnAddButton(_ sender: UIBarButtonItem) { presenter?.addNewContact(from: self) } } extension ContactListView: ContactListViewProtocol { func reloadInterface(with contacts: [ContactViewModel]) { contactList = contacts tableView.reloadData() } func didInsertContact(_ contact: ContactViewModel) { let insertionIndex = contactList.insertionIndex(of: contact) { $0 < $1 } contactList.insert(contact, at: insertionIndex) let indexPath = IndexPath(row: insertionIndex, section: 0) tableView.beginUpdates() tableView.insertRows(at: [indexPath], with: .right) tableView.endUpdates() } } extension ContactListView: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell") else { return UITableViewCell() } cell.textLabel?.text = contactList[(indexPath as NSIndexPath).row].fullName return cell } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return contactList.count } } 


View envoie les événements viewDidLoad et didClickOnAddButton au présentateur. Lors du premier événement, le présentateur demandera des données à Interactor et le second, le présentateur demandera au routeur de basculer vers le contrôleur pour ajouter un nouveau contact.

Les méthodes du protocole ContactListViewProtocol sont appelées depuis Presenter soit lorsqu'une liste de contacts est demandée, soit lorsqu'un nouveau contact est ajouté. Dans les deux cas, les données de la vue contiennent uniquement les informations nécessaires à l'affichage.

La vue contient également des méthodes qui implémentent le protocole UITableViewDataSource qui remplissent la table avec les données reçues.

INTERACTEUR

L'interacteur dans notre exemple est assez simple. Tout ce qu'il fait, c'est demander des données via le gestionnaire de base de données local, et peu importe ce que ce gestionnaire, CoreData, Realm ou toute autre solution utilise. Le code dans ContactListInteractor sera le suivant:

 class ContactListInteractor: ContactListInteractorInputProtocol { weak var presenter: ContactListInteractorOutputProtocol? var localDatamanager: ContactListLocalDataManagerInputProtocol? func retrieveContacts() { do { if let contactList = try localDatamanager?.retrieveContactList() { presenter?.didRetrieveContacts(contactList) } else { presenter?.didRetrieveContacts([]) } } catch { presenter?.didRetrieveContacts([]) } } } 


Une fois qu'Interactor a reçu les données demandées, il en informe Presenter. De plus, en option, Interactor peut transmettre une erreur à Presenter, qui devra ensuite formater l'erreur dans une vue adaptée à l'affichage dans View.

Remarque : Comme vous l'avez peut-être remarqué, chaque couche de VIPER implémente un protocole. En conséquence, les classes dépendent d'abstractions, et non d'une implémentation particulière, répondant ainsi au principe de l'inversion de dépendance (l'un des principes de SOLID).

PRÉSENTATEUR

L'élément le plus important de l'architecture. Toutes les communications entre la vue et le reste des couches (Interactor et Router) passent par le Presenter. Code ContactListPresenter :

 class ContactListPresenter: ContactListPresenterProtocol { weak var view: ContactListViewProtocol? var interactor: ContactListInteractorInputProtocol? var wireFrame: ContactListWireFrameProtocol? func viewDidLoad() { interactor?.retrieveContacts() } func addNewContact(from view: ContactListViewProtocol) { wireFrame?.presentAddContactScreen(from: view) } } extension ContactListPresenter: ContactListInteractorOutputProtocol { func didRetrieveContacts(_ contacts: [Contact]) { view?.reloadInterface(with: contacts.map() { return ContactViewModel(fullName: $0.fullName) }) } } extension ContactListPresenter: AddModuleDelegate { func didAddContact(_ contact: Contact) { let contactViewModel = ContactViewModel(fullName: contact.fullName) view?.didInsertContact(contactViewModel) } func didCancelAddContact() {} } 


Une fois que View est chargé, il en informe Presenter, qui à son tour demande des données via Interactor. Lorsque l'utilisateur clique sur le bouton Ajouter un nouveau contact, View avertit le présentateur, qui envoie une demande d'ouverture de l'écran Ajouter un nouveau contact dans le routeur.

Le présentateur formate également les données et les renvoie à la vue après avoir interrogé la liste de contacts. Il est également responsable de la mise en œuvre du protocole AddModuleDelegate. Cela signifie que Presenter recevra une notification lorsqu'un nouveau contact est ajouté, préparera les données du contact pour l'affichage et le transfert vers View.

Comme vous l'avez peut-être remarqué, Presenter a toutes les chances de devenir assez encombrant. S'il existe une telle possibilité, alors Presenter peut être divisé en deux parties: Presenter, qui ne reçoit que les données, les formate pour l'affichage et les transmet à View; et un gestionnaire d'événements qui répondra aux actions des utilisateurs.

ENTITÉ

Cette couche est similaire à la couche Model dans MVVM. Dans notre application, il est représenté par la classe Contact et les fonctions de définition d'opérateur <et>. Le contenu des contacts ressemblera à ceci:

 import CoreData open class Contact: NSManagedObject { var fullName: String { get { var name = "" if let firstName = firstName { name += firstName } if let lastName = lastName { name += " " + lastName } return name } } } public struct ContactViewModel { var fullName = "" } public func <(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() < rhs.fullName.lowercased() } public func >(lhs: ContactViewModel, rhs: ContactViewModel) -> Bool { return lhs.fullName.lowercased() > rhs.fullName.lowercased() } 


ContactViewModel contient les champs que Presenter remplit (formats) que la vue affiche. La classe Contact est une sous-classe de NSManagedObject contenant les mêmes champs que dans le modèle CoreData.

ROUTEUR

Et enfin, la dernière couche, mais certainement pas en importance,. La responsabilité de la navigation incombe à Presenter et WireFrame. Le présentateur reçoit un événement de l'utilisateur et sait quand effectuer la transition, et WireFrame sait comment et où effectuer cette transition. Pour que vous ne soyez pas confus, dans cet exemple, la couche Router est représentée par la classe ContactListWireFrame et est appelée WireFrame dans le texte. Code ContactListWireFrame :

 import UIKit class ContactListWireFrame: ContactListWireFrameProtocol { class func createContactListModule() -> UIViewController { let navController = mainStoryboard.instantiateViewController(withIdentifier: "ContactsNavigationController") if let view = navController.childViewControllers.first as? ContactListView { let presenter: ContactListPresenterProtocol & ContactListInteractorOutputProtocol = ContactListPresenter() let interactor: ContactListInteractorInputProtocol = ContactListInteractor() let localDataManager: ContactListLocalDataManagerInputProtocol = ContactListLocalDataManager() let wireFrame: ContactListWireFrameProtocol = ContactListWireFrame() view.presenter = presenter presenter.view = view presenter.wireFrame = wireFrame presenter.interactor = interactor interactor.presenter = presenter interactor.localDatamanager = localDataManager return navController } return UIViewController() } static var mainStoryboard: UIStoryboard { return UIStoryboard(name: "Main", bundle: Bundle.main) } func presentAddContactScreen(from view: ContactListViewProtocol) { guard let delegate = view.presenter as? AddModuleDelegate else { return } let addContactsView = AddContactWireFrame.createAddContactModule(with: delegate) if let sourceView = view as? UIViewController { sourceView.present(addContactsView, animated: true, completion: nil) } } } 


Puisque WireFrame est responsable de la création du module, il sera commode de configurer toutes les dépendances ici. Lorsque vous souhaitez ouvrir un autre contrôleur, la fonction qui ouvre le nouveau contrôleur reçoit en argument l'objet qui l'ouvrira et crée un nouveau contrôleur à l'aide de son WireFrame. De plus, lors de la création d'un nouveau contrôleur, les données nécessaires lui sont transférées, dans ce cas uniquement le délégué (Présentateur du contrôleur avec contacts) pour recevoir le contact créé.

La couche Router fournit une bonne opportunité pour éviter l'utilisation de séquences (transitions) dans les storyboards et organiser toute la navigation dans le code. Étant donné que les storyboards ne fournissent pas une solution compacte pour le transfert de données entre les contrôleurs, notre implémentation de navigation n'ajoutera pas de code supplémentaire. Tout ce que nous obtenons n'est que la meilleure réutilisabilité.


Résumé :

Vous pouvez trouver les deux projets dans ce référentiel .

Comme vous pouvez le voir, MVVM et VIPER, bien que différents, ne sont pas uniques. MVVM nous dit qu'en plus de View et Model, il devrait également y avoir une couche ViewModel. Mais rien n'est dit sur la façon dont cette couche doit être créée, ni sur la façon dont les données sont demandées - la responsabilité de cette couche n'est pas clairement définie. Il existe de nombreuses façons de l'implémenter et vous pouvez utiliser n'importe lequel d'entre eux.

VIPER, en revanche, est une architecture assez unique. Il se compose de plusieurs couches, chacune ayant un domaine de responsabilité bien défini et moins que MVVM est influencée par le développeur.

Quand il s'agit de choisir une architecture, il n'y a généralement pas la seule bonne solution, mais je vais quand même essayer de vous donner quelques conseils. Si vous avez un projet volumineux et long, avec des exigences claires et que vous souhaitez avoir la possibilité de réutiliser des composants, VIPER sera la meilleure solution. Une délimitation plus claire des responsabilités permet de mieux organiser les tests et d'améliorer la réutilisation.

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


All Articles