Desarrollo modular o camino hacia allá, no hacia atrás


Cómo llegamos a un nuevo enfoque para trabajar con módulos en la aplicación iOS RaiffeisenBank.

El problema


En las aplicaciones de Raiffeisenbank, cada pantalla consta de varios módulos que son lo más independientes posible entre sí. "Módulo" lo llamamos un componente visual que tiene su propia idea. Al diseñar una aplicación, es muy importante escribir la lógica para que los módulos sean independientes y puedan agregarse o eliminarse fácilmente sin tener que recurrir a la refactorización.

Qué dificultades enfrentamos:


Destacando la abstracción sobre los patrones arquitectónicos
Ya en la primera etapa de desarrollo, quedó claro que no queríamos estar atados a un patrón arquitectónico específico. MVC es bueno si necesita mostrar una página con alguna información. Al mismo tiempo, la interacción con el usuario es mínima o nada. Por ejemplo: la página "sobre la empresa" o "acuerdo de usuario". VIPER es una buena herramienta para módulos complejos que tienen su propia lógica de trabajo con servicios, enrutamiento y mucho de todo.

El problema de la interacción y la encapsulación.
Cada patrón arquitectónico tiene su propia estructura de construcción y sus propios protocolos, que imponen restricciones para trabajar con el módulo. Para abstraer el módulo, debe resaltar las principales interfaces de interacción de entrada / salida .

Destacando la lógica de enrutamiento
Un módulo como unidad visual no debe ni puede saber dónde y cómo se muestra. Uno y el mismo módulo debe y puede implementarse como una unidad independiente en cualquier pantalla o como una composición. La responsabilidad de esto no se puede culpar al módulo en sí.

Solución anterior: // Mal negocio


La primera solución que escribimos en Objective-C, y se basó en NSProxy. El problema de la encapsulación del patrón arquitectónico se resolvió mediante la defensa, que se determinó por las condiciones dadas, es decir, la entrada / salida del módulo, lo que hizo posible que las llamadas al módulo sean proxy a su entrada y recibir mensajes a través de la salida , si corresponde.

Fue un paso adelante, pero surgieron nuevas dificultades:

  • La interfaz proxy no garantizaba la implementación del protocolo de entrada ;
  • La salida tenía que ser descrita, incluso si no era necesaria;
  • Era necesario agregar la propiedad de salida a la interfaz de entrada .

Además de NSProxy, también implementamos el enrutamiento al observar la idea de ViperMcFlurry: creamos una categoría en ViewController , que comenzó a crecer a medida que aparecían diferentes opciones para mostrar el módulo en la pantalla. Por supuesto, dividimos la categoría, pero aún estaba lejos de ser una buena solución.

En general ... el primer panqueque está lleno de bultos, quedó claro que debe resolver el problema de manera diferente.

Solución: // Final


Al darnos cuenta de que no había nada más con NSProxy , tomamos marcadores y fuimos a dibujar. Como resultado, aislamos el protocolo RFModule :

@objc protocol RFModule { var view: ViewController { get } var input: AnyObject? { get } var output: AnyObject? { get set } var transition: Transitioning { get set } } 

A propósito abandonamos los tipos asociados a nivel de protocolo, y había una buena razón para esto: en ese momento, el 90% del código estaba en Objective-C. Interoperabilidad entre módulos ObjC ← → Swift no sería posible.

Para poder seguir utilizando genéricos y garantizar el uso de módulos de tipo, introdujimos la clase Módulo que satisface el protocolo
RFModule :

 final class Module<I: Any, O: Any>: RFModule { public typealias Input = I public typealias Output = O public var setOutput: ((O?) -> Void)? //... public var input: I? { get { return inputObjc as? I} set { inputObjc = newValue as AnyObject } } public var output: O? { get { return outputObjc as? O} set { outputObjc = newValue as AnyObject } } @objc(input) public weak var inputObjc: AnyObject? @objc(moduleOutput) public weak var outputObjc: AnyObject? { didSet{ setOutput?(output) } } } @objc protocol RFModule { var view: ViewController { get } @objc(input) var inputObjc: AnyObject? { get } @objc(moduleOutput) var outputObjc: AnyObject? { get set } var transition: Transitioning { get set } } public extension RFModule { public var input: AnyObject? { return inputObjc } public var output: AnyObject? { get { return outputObjc } set { outputObjc = newValue} } } 

Entonces tenemos un módulo escrito. Y de hecho, Swift usa la clase Módulo , y en el Objective-C RFModule . Además, resultó ser una herramienta conveniente para combinar tipos en el lugar donde necesita crear matrices: por ejemplo, TabContainer .

Dado que la DI para crear el módulo está en el ámbito de UserStory, y asignar el valor de salida en el lugar donde se utilizará no puede describir un simple seter. "SetOutput" es, en esencia, una defensa que, en la etapa de asignación de salida, la pasará a la persona responsable, dependiendo de la lógica del módulo.

 class SomeViewController: UIViewController, ModuleInput { weak var delegate: ModuleOutput } class Assembly { func someModule() -> Module<ModuleInput, ModuleOutput> { let view = SomeViewController() let module = Module<ModuleInput, ModuleOutput>(view: view, input: view) { [weak view] output in view?.delegate = output } return module } } ... let assembly: Assembly let module = assembly.someModule() module.output = self 

La transición es un protocolo cuyas implementaciones, como su nombre lo indica, son responsables de la lógica de mostrar y ocultar el módulo.

 protocol Transitioning { var destination: ViewController? { get } // should be weak func perform(_ completion: (()->())?) // present func reverse(_ completion: (()->())?) // dissmiss } 

Para la visualización es causada - realizar , para ocultar - invertir . A pesar del hecho de que hay un destino en el protocolo y al principio parece que debería haber una fuente . De hecho, la fuente puede no ser, y su tipo no siempre es ViewController . Por ejemplo, si necesitamos que el módulo se abra en una nueva ventana, esta es Window , y si necesitamos incrustar , necesitamos AND parent: ViewController AND container: UIView .

 class PresentTransition: Transitioning { weak var source: ViewController? weak var destination: ViewController? ... func perform(_ completion: (()->())?) { source.present(viewController: self.destinaton) } } 

Por lo tanto, nos deshicimos de la idea de escribir extensiones en el ViewController y describimos la lógica de cómo mostramos nuestros módulos en varios objetos. Esto nos dio flexibilidad en el enrutamiento, es decir ahora podemos mostrar cualquier módulo tanto de forma independiente como en un complejo, así como variar entre cómo se muestra todo en la pantalla: en la ventana (Ventana), Presente, en la navegación (presionar para navegar), incrustar, en la cortina (cubierta) .

¿Eso es todo?


Hay una cosa más que es inquietante hasta ahora. Para tener la oportunidad de elegir fácilmente la forma en que se muestra el módulo y eliminar esta lógica, pagamos la pérdida de la capacidad de establecer las propiedades de apariencia. Por ejemplo, si lo mostramos en Navigation, necesitamos especificar qué color debe ser barTintColor ; o, si mostramos el módulo en la cortina, es necesario establecer el color del controlador .

Hasta ahora, hemos resuelto este problema con la apariencia sin tipo: cualquier propiedad, y la transición al abrir el módulo conduce al tipo con el que funciona, y si tiene éxito, le quita las propiedades necesarias.

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


All Articles