Une architecture propre et rapide comme alternative à VIPER

Présentation


À l'heure actuelle, il existe de nombreux articles sur VIPER - architecture propre, dont diverses variantes sont devenues à la fois populaires pour les projets iOS. Si vous n'êtes pas familier avec Viper, vous pouvez le lire ici , ici ou ici .

Je voudrais parler de l'alternative VIPER - Clean Swift. Clean Swift à première vue ressemble à VIPER, cependant, les différences deviennent visibles après avoir étudié le principe de l'interaction entre les modules. Dans VIPER, l'interaction est basée sur Presenter, il transfère les demandes des utilisateurs à l'Interactor pour le traitement et formate les données reçues de celui-ci pour les afficher sur le View Controller:

image

Dans Clean Swift, les modules principaux, comme dans VIPER, sont View Controller, Interactor, Presenter.

image

L'interaction entre eux se produit par cycles. Le transfert de données est basé sur des protocoles (encore une fois, comme VIPER), ce qui permet de futurs changements dans l'un des composants du système pour le remplacer simplement par un autre. Le processus d'interaction ressemble en général à ceci: l'utilisateur clique sur le bouton, View Controller crée un objet avec une description et l'envoie à Interactor. Interactor, à son tour, implémente un scénario spécifique conformément à la logique métier, crée un objet de résultat et le transmet à Presenter. Le présentateur forme un objet avec des données formatées pour être affichées à l'utilisateur et les envoie au contrôleur de vue. Examinons de plus près chaque module Clean Swift plus en détail.

Afficher (Afficher le contrôleur)


View Controller, comme dans VIPER, effectue toutes les configurations Vew, que ce soit la couleur, les paramètres de police UILabel ou Layout. Par conséquent, chaque UIViewController de cette architecture implémente un protocole d'entrée pour afficher les données ou répondre aux actions de l'utilisateur.

Interactractor


Interactor contient toute la logique métier. Il accepte les actions utilisateur du contrôleur, avec des paramètres (par exemple, le texte modifié du champ de saisie, en appuyant sur un bouton) définis dans le protocole d'entrée. Après avoir élaboré la logique, Interactor, si nécessaire, doit transférer les données pour leur préparation au Presenter avant de les afficher dans le ViewController. Cependant, Interactor n'accepte que les demandes de View en entrée, contrairement à VIPER, où ces demandes passent par Presenter.

Présentateur


Le présentateur traite les données à afficher à l'utilisateur. Le résultat dans ce cas est le protocole d'entrée de ViewController, ici vous pouvez, par exemple, changer le format du texte, traduire la valeur de la couleur de enum en rgb, etc.

Ouvrier


Afin de ne pas compliquer inutilement Interactor et de ne pas dupliquer les détails de la logique métier, vous pouvez utiliser un élément Worker supplémentaire. Dans les modules simples, il n'est pas toujours nécessaire, mais dans les modules suffisamment chargés, il vous permet de supprimer certaines tâches d'Interactor. Par exemple, la logique d'interaction avec la base de données peut être effectuée dans le programme de travail, en particulier si les mêmes requêtes de base de données peuvent être utilisées dans différents modules.

Routeur


Le routeur est responsable du transfert des données vers d'autres modules et des transitions entre eux. Il a un lien avec le contrôleur, car dans iOS, malheureusement, les contrôleurs, entre autres, sont historiquement responsables des transitions. L'utilisation de segue peut simplifier l'initialisation des transitions en appelant les méthodes du routeur à partir de Préparer pour segue, car le routeur sait comment transférer des données, et le fera sans aucun code de boucle supplémentaire d'Interactor / Presenter. Les données sont transférées à l'aide des protocoles d'entrepôt de données de chaque module implémenté dans Interactor. Ces protocoles limitent également la possibilité d'accéder aux données du module interne à partir du routeur.

Les modèles


Modèles est une description des structures de données pour le transfert de données entre les modules. Chaque implémentation de la fonction de logique métier a sa propre description des modèles.

  • Demande - pour envoyer une demande du contrôleur à l'interacteur.
  • Réponse - la réponse de l'interacteur à transmettre au présentateur de données.
  • ViewModel - pour le transfert de données sous une forme prête à être affichée dans le contrôleur.

Exemple d'implémentation


Examinons de plus près cette architecture à l'aide d'un exemple simple. Ils seront servis par l'application ContactsBook de manière simplifiée, mais tout à fait suffisante pour comprendre l'essence de la forme architecturale. L'application comprend une liste de contacts, ainsi que l'ajout et la modification de contacts.

Un exemple de protocole d'entrée:

protocol ContactListDisplayLogic: class { func displayContacts(viewModel: ContactList.ShowContacts.ViewModel) } 

Chaque contrôleur contient une référence à un objet qui implémente le protocole d'interaction d'entrée

 var interactor: ContactListBusinessLogic? 

ainsi qu'à l'objet Router, qui devrait implémenter la logique de transfert de données et de commutation de module:

 var router: (NSObjectProtocol & ContactListRoutingLogic & ContactListDataPassing)? 

Vous pouvez implémenter la configuration du module dans une méthode privée distincte:

 private func setup() { let viewController = self let interactor = ContactListInteractor() let presenter = ContactListPresenter() let router = ContactListRouter() viewController.interactor = interactor viewController.router = router interactor.presenter = presenter presenter.viewController = viewController router.viewController = viewController router.dataStore = interactor } 

ou créez un configurateur singleton pour supprimer ce code du contrôleur (pour ceux qui pensent que le contrôleur ne devrait pas être impliqué dans la configuration) et ne pas se tenter avec l'accès à des parties du module dans le contrôleur. Il n'y a pas de classe de configurateur dans la vue de l'oncle Bob et dans VIPER classique. L'utilisation du configurateur pour le module d'ajout de contact ressemble à ceci:

 override func awakeFromNib() { super.awakeFromNib() AddContactConfigurator.sharedInstance.configure(self) } 

Le code du configurateur contient la seule méthode de configuration qui est absolument identique à la méthode de configuration dans le contrôleur:

 final class AddContactConfigurator { static let sharedInstance = AddContactConfigurator() private init() {} func configure(_ control: AddContactViewController) { let viewController = control let interactor = AddContactInteractor() let presenter = AddContactPresenter() let router = AddContactRouter() viewController.interactor = interactor viewController.router = router interactor.presenter = presenter presenter.viewController = viewController router.viewController = viewController router.dataStore = interactor } } 

Un autre point très important dans la mise en œuvre du contrôleur est le code de la méthode de préparation standard pour la séquence:

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let scene = segue.identifier { let selector = NSSelectorFromString("routeTo\(scene)WithSegue:") if let router = router, router.responds(to: selector) { router.perform(selector, with: segue) } } } 

Un lecteur attentif a probablement remarqué que le routeur est également requis pour implémenter NSObjectProtocol. Ceci est fait afin que nous puissions utiliser les méthodes standard de ce protocole pour le routage lors de l'utilisation de séquences. Pour prendre en charge cette redirection simple, la dénomination de l'identificateur de séquence doit correspondre aux terminaisons des noms de méthode du routeur. Par exemple, pour passer à l'affichage d'un contact, il existe une séquence qui est liée au choix d'une cellule avec un contact. Son identifiant est "ViewContact", voici la méthode correspondante dans Router:

 func routeToViewContact(segue: UIStoryboardSegue?) 

La demande d'affichage des données dans Interactor semble également très simple:

 private func fetchContacts() { let request = ContactList.ShowContacts.Request() interactor?.showContacts(request: request) } 

Passons à Interactor. Interactor implémente le protocole ContactListDataStore, qui est responsable du stockage / accès aux données. Dans notre cas, il s'agit simplement d'un tableau de contacts, limité uniquement par la méthode getter, pour montrer au routeur l'inadmissibilité de le changer à partir d'autres modules. Un protocole implémentant la logique métier de notre liste est le suivant:

 func showContacts(request: ContactList.ShowContacts.Request) { let contacts = worker.getContacts() self.contacts = contacts let response = ContactList.ShowContacts.Response(contacts: contacts) presenter?.presentContacts(response: response) } 

Il reçoit les données de contact de ContactListWorker. Dans ce cas, le travailleur est responsable de la façon dont les données sont téléchargées. Il peut se tourner vers des services tiers qui décident, par exemple, de prendre des données du cache ou de les télécharger depuis le réseau. Après avoir reçu les données, Interactor envoie une réponse au présentateur pour préparer l'affichage, car cet Interactor contient un lien vers le présentateur:

 var presenter: ContactListPresentationLogic? 

Le présentateur implémente un seul protocole - ContactListPresentationLogic, dans notre cas, il modifie simplement de force la casse du prénom et du nom du contact, forme le modèle de présentation DisplayedContact à partir du modèle de données et le transmet au contrôleur pour affichage:

 func presentContacts(response: ContactList.ShowContacts.Response) { let mapped = response.contacts.map { ContactList .ShowContacts .ViewModel .DisplayedContact(firstName: $0.firstName.uppercaseFirst, lastName: $0.lastName.uppercaseFirst) } let viewModel = ContactList.ShowContacts.ViewModel(displayedContacts: mapped) viewController?.displayContacts(viewModel: viewModel) } 

Après cela, le cycle se termine et le contrôleur affiche les données, implémentant la méthode du protocole ContactListDisplayLogic:

 func displayContacts(viewModel: ContactList.ShowContacts.ViewModel) { displayedContacts = viewModel.displayedContacts tableView.reloadData() } 

Voici à quoi ressemblent les modèles d'affichage des contacts:

 enum ShowContacts { struct Request { } struct Response { var contacts: [Contact] } struct ViewModel { struct DisplayedContact { let firstName: String let lastName: String var fullName: String { return firstName + " " + lastName } } var displayedContacts: [DisplayedContact] } } 

Dans ce cas, la demande ne contient pas de données, car il ne s'agit que d'une liste de contacts générale, cependant, si, par exemple, l'écran de liste contient un filtre, le type de filtre peut être inclus dans cette demande. Le modèle de réponse Intrecator contient la liste de contacts souhaitée, ViewModel contient également un tableau de données prêt à être affiché - DisplayedContact.

Pourquoi nettoyer Swift


Considérez les avantages et les inconvénients de cette architecture. Tout d'abord, Clean Swift dispose de modèles de code qui facilitent la création d'un module. Ces modèles peuvent être écrits pour de nombreuses architectures, mais lorsqu'ils sont prêts à l'emploi - cela permet au moins d'économiser plusieurs heures de votre temps.

Deuxièmement, cette architecture, comme VIPER, est bien testée, des exemples de tests sont disponibles dans le projet. Étant donné que le module avec lequel l'interaction se produit est facile à remplacer par un stub, la détermination de la fonctionnalité de chaque module à l'aide de protocoles vous permet de l'implémenter sans problème. Si nous créons simultanément la logique métier et les tests correspondants (Interactor, Interactor tests), cela correspond bien au principe du TDD. Étant donné que la sortie et l'entrée de chaque cas de logique sont définies par le protocole, il suffit juste d'écrire un test qui détermine d'abord son comportement, puis d'implémenter directement la logique de la méthode.

Troisièmement, Clean Swift (contrairement à VIPER) met en œuvre un flux unidirectionnel de traitement des données et de prise de décision. Un seul cycle est toujours exécuté - View - Interactor - Presenter - View, ce qui simplifie également le refactoring, car il est souvent nécessaire de changer moins d'entités. Pour cette raison, les projets dont la logique change souvent ou est complétée sont plus pratiques à refactoriser en utilisant la méthodologie Clean Swift. En utilisant Clean Swift, vous séparez les entités de deux manières:

  1. Isolez les composants en déclarant les protocoles d'entrée et de sortie
  2. Isolez les fonctionnalités en utilisant des structures et en encapsulant les données dans des modèles de requêtes / réponses / UI distincts. Chaque fonctionnalité a sa propre logique et est contrôlée dans le cadre d'un processus, sans se croiser dans un module avec d'autres fonctionnalités.

Clean Swift ne doit pas être utilisé dans de petits projets sans perspective à long terme, dans des projets prototypes. Par exemple, il est trop coûteux d'implémenter une application pour le calendrier d'une conférence de développeurs utilisant cette architecture. Les projets à long terme, les projets avec beaucoup de logique métier, au contraire, s'inscrivent bien dans le cadre de cette architecture. Il est très pratique d'utiliser Clean Swift lorsque le projet est mis en œuvre pour deux plates-formes - Mac OS et iOS, ou qu'il est prévu de le porter à l'avenir.

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


All Articles