Arquitectura rápida y limpia como alternativa a VIPER

Introduccion


En este momento, hay muchos artículos sobre VIPER: arquitectura limpia, varias variaciones de las cuales se hicieron populares en algún momento para proyectos de iOS. Si no está familiarizado con Viper, puede leerlo aquí , aquí o aquí .

Me gustaría hablar sobre la alternativa VIPER: Clean Swift. Clean Swift a primera vista parece VIPER, sin embargo, las diferencias se hacen visibles después de estudiar el principio de interacción entre módulos. En VIPER, la interacción se basa en Presenter, transfiere las solicitudes de los usuarios al Interactor para su procesamiento y formatea los datos recibidos para mostrarlos en el Controlador de vista:

imagen

En Clean Swift, los módulos principales, como en VIPER, son View Controller, Interactor, Presenter.

imagen

La interacción entre ellos ocurre en ciclos. La transferencia de datos se basa en protocolos (de nuevo, de manera similar a VIPER), lo que permite futuros cambios en uno de los componentes del sistema para simplemente reemplazarlo por otro. El proceso de interacción en general tiene este aspecto: el usuario hace clic en el botón, View Controller crea un objeto con una descripción y lo envía a Interactor. Interactor, a su vez, implementa un escenario específico de acuerdo con la lógica empresarial, crea un objeto de resultado y lo pasa a Presenter. El presentador forma un objeto con datos formateados para mostrar al usuario y lo envía al controlador de vista. Echemos un vistazo más de cerca a cada módulo Clean Swift con más detalle.

Ver (Controlador de vista)


View Controller, como en VIPER, realiza todas las configuraciones de VIew, ya sea de color, UILabel o configuraciones de fuente de diseño. Por lo tanto, cada UIViewController en esta arquitectura implementa un protocolo de entrada para mostrar datos o responder a las acciones del usuario.

Interactractor


Interactor contiene toda la lógica de negocios. Acepta acciones del usuario desde el controlador, con parámetros (por ejemplo, texto modificado del campo de entrada, presionando un botón) definidos en el protocolo de entrada. Después de resolver la lógica, Interactor, si es necesario, debe transferir los datos para su preparación al Presentador antes de mostrarlos en el ViewController. Sin embargo, Interactor solo acepta solicitudes de View como entrada, a diferencia de VIPER, donde estas solicitudes pasan por Presenter.

Presentador


El presentador procesa los datos para mostrarlos al usuario. El resultado en este caso es el protocolo de entrada de ViewController, aquí puede, por ejemplo, cambiar el formato de texto, traducir el valor de color de enum a rgb, etc.

Trabajador


Para no complicar innecesariamente a Interactor y no duplicar los detalles de la lógica empresarial, puede usar un elemento Worker adicional. En módulos simples, no siempre es necesario, pero en módulos suficientemente cargados le permite eliminar algunas tareas de Interactor. Por ejemplo, la lógica de interacción con la base de datos se puede hacer en el trabajador, especialmente si las mismas consultas de la base de datos se pueden usar en diferentes módulos.

Enrutador


El enrutador es responsable de transferir datos a otros módulos y transiciones entre ellos. Tiene un enlace al controlador, porque en iOS, desafortunadamente, los controladores, entre otras cosas, son históricamente responsables de las transiciones. El uso de segue puede simplificar la inicialización de las transiciones al llamar a los métodos de enrutador desde Prepararse para segue, porque el enrutador sabe cómo transferir datos, y lo hará sin ningún código de bucle adicional de Interactor / Presenter. Los datos se transfieren utilizando los protocolos de almacenamiento de datos de cada módulo implementado en Interactor. Estos protocolos también limitan la capacidad de acceder a los datos del módulo interno desde el enrutador.

Modelos


Modelos es una descripción de las estructuras de datos para transferir datos entre módulos. Cada implementación de la función de lógica de negocios tiene su propia descripción de los modelos.

  • Solicitud: para enviar una solicitud desde el controlador al interactor.
  • Respuesta: la respuesta del interactor para transmitir al presentador con los datos.
  • ViewModel: para la transferencia de datos en un formulario listo para mostrar en el controlador.

Ejemplo de implementación


Echemos un vistazo más de cerca a esta arquitectura usando un ejemplo simple. Serán atendidos por la aplicación ContactsBook de forma simplificada, pero suficiente para comprender la esencia de la forma de la arquitectura. La aplicación incluye una lista de contactos, así como agregar y editar contactos.

Un ejemplo de un protocolo de entrada:

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

Cada controlador contiene una referencia a un objeto que implementa el protocolo Interactor de entrada

 var interactor: ContactListBusinessLogic? 

así como al objeto Router, que debe implementar la lógica de transferencia de datos y conmutación de módulos:

 var router: (NSObjectProtocol & ContactListRoutingLogic & ContactListDataPassing)? 

Puede implementar la configuración del módulo en un método privado separado:

 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 } 

o cree un Singleton Configurator para eliminar este código del controlador (para aquellos que creen que el controlador no debe estar involucrado en la configuración) y no se tienten con el acceso a partes del módulo en el controlador. No hay clase de configurador en la vista del tío Bob y en VIPER clásico. El uso del configurador para el módulo de agregar contacto se ve así:

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

El código del configurador contiene el único método de configuración que es absolutamente idéntico al método de configuración en el controlador:

 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 } } 

Otro punto muy importante en la implementación del controlador es el código en el método de preparación estándar para segue:

 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 lector atento probablemente notó que Router también es necesario para implementar NSObjectProtocol. Esto se hace para que podamos usar los métodos estándar de este protocolo para el enrutamiento cuando se usan segues. Para admitir esta simple redirección, la denominación del identificador de segue debe coincidir con las terminaciones de los nombres de los métodos de enrutador. Por ejemplo, para ir a ver un contacto, hay un segue, que está vinculado a la elección de una celda con un contacto. Su identificador es "ViewContact", aquí está el método correspondiente en el enrutador:

 func routeToViewContact(segue: UIStoryboardSegue?) 

La solicitud para mostrar datos a Interactor también parece muy simple:

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

Pasemos a Interactor. Interactor implementa el protocolo ContactListDataStore, que se encarga de almacenar / acceder a los datos. En nuestro caso, esto es solo una serie de contactos, limitados solo por el método getter, para mostrar al enrutador la inadmisibilidad de cambiarlo desde otros módulos. Un protocolo que implementa la lógica de negocios para nuestra lista es el siguiente:

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

Recibe datos de contacto de ContactListWorker. En este caso, el trabajador es responsable de cómo se descargan los datos. Puede recurrir a servicios de terceros que deciden, por ejemplo, tomar datos del caché o descargarlos de la red. Después de recibir los datos, Interactor envía una Respuesta al Presentador para prepararse para la visualización, para este Interactor contiene un enlace al Presentador:

 var presenter: ContactListPresentationLogic? 

El presentador implementa solo un protocolo: ContactListPresentationLogic, en nuestro caso, simplemente cambia a la fuerza el caso del nombre y apellido del contacto, forma el modelo de presentación DisplayedContact del modelo de datos y lo pasa al controlador para su visualización:

 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) } 

Después de eso, el ciclo finaliza y el controlador muestra los datos, implementando el método de protocolo ContactListDisplayLogic:

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

Así es como se ven los modelos para mostrar contactos:

 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] } } 

En este caso, la solicitud no contiene datos, ya que esta es solo una lista general de contactos, sin embargo, si, por ejemplo, la pantalla de la lista contiene un filtro, el tipo de filtro podría incluirse en esta solicitud. El modelo de respuesta Intrecator contiene la lista de contactos deseada, ViewModel también contiene una serie de datos listos para mostrar: DisplayedContact.

Por qué Clean Swift


Considere los pros y los contras de esta arquitectura. Primero, Clean Swift tiene plantillas de código que facilitan la creación de un módulo. Estas plantillas se pueden escribir para muchas arquitecturas, pero cuando están listas para usar, al menos le ahorran varias horas de tiempo.

En segundo lugar, esta arquitectura, como VIPER, está bien probada, hay ejemplos de pruebas disponibles en el proyecto. Dado que el módulo con el que se produce la interacción es fácil de reemplazar con un código auxiliar, determinar la funcionalidad de cada módulo utilizando protocolos le permite implementar esto sin dolor de cabeza. Si simultáneamente creamos lógica de negocios y las pruebas correspondientes (pruebas de Interactor, Interactor), esto encaja bien con el principio de TDD. Debido al hecho de que el protocolo define la salida y la entrada de cada caso de lógica, es suficiente escribir primero una prueba que determine su comportamiento y luego implementar directamente la lógica del método.

En tercer lugar, Clean Swift (a diferencia de VIPER) implementa un flujo unidireccional de procesamiento de datos y toma de decisiones. Siempre se ejecuta un ciclo: Ver - Interactor - Presentador - Ver, que también simplifica la refactorización, ya que a menudo es necesario cambiar menos entidades. Debido a esto, los proyectos con lógica que a menudo cambia o se complementa son más convenientes para refactorizar utilizando la metodología Clean Swift. Usando Clean Swift, separas las entidades de dos maneras:

  1. Aísle los componentes declarando los protocolos de entrada y salida
  2. Aísle las características mediante el uso de estructuras y la encapsulación de datos en solicitudes / respuestas / modelos de IU independientes. Cada característica tiene su propia lógica y se controla dentro del marco de un proceso, sin cruzarse en un módulo con otras características.

Clean Swift no debe usarse en proyectos pequeños sin una perspectiva a largo plazo, en proyectos prototipo. Por ejemplo, es demasiado costoso implementar una aplicación para el horario de una conferencia de desarrolladores utilizando esta arquitectura. Los proyectos a largo plazo, proyectos con mucha lógica de negocios, por el contrario, encajan bien en el marco de esta arquitectura. Es muy conveniente usar Clean Swift cuando el proyecto se implementa para dos plataformas: Mac OS e iOS, o se planea portarlo en el futuro.

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


All Articles