
Hola a todos! Mi nombre es Ilya, soy desarrollador de iOS en Tinkoff.ru. En este artículo quiero hablar sobre cómo reducir la duplicación de código en la capa de presentación utilizando protocolos.
Cual es el problema
A medida que crece el proyecto, aumenta la cantidad de duplicación de código. Esto no se hace evidente de inmediato, y se hace difícil corregir los errores del pasado. Notamos este problema en nuestro proyecto y lo resolvimos usando un enfoque, llamémoslo, condicionalmente, rasgos.
Ejemplo de vida
El enfoque se puede usar con varias soluciones arquitectónicas diferentes, pero lo consideraré usando VIPER como ejemplo.
Considere el método más común en el enrutador: el método que cierra la pantalla:
func close() { self.transitionHandler.dismiss(animated: true, completion: nil) }
Está presente en muchos enrutadores, y es mejor escribirlo solo una vez.
La herencia nos ayudaría en esto, pero en el futuro, cuando tengamos más y más clases con métodos innecesarios en nuestra aplicación, o no podamos crear la clase que necesitamos porque los métodos requeridos están en diferentes clases base, aparecerán grandes problemas
Como resultado, el proyecto crecerá en muchas clases base y clases descendientes con métodos superfluos. La herencia no nos ayudará.
¿Qué es mejor que la herencia? Por supuesto la composición.
Puede crear una clase separada para el método que cierra la pantalla y agregarla a cada enrutador en el que se necesita:
struct CloseRouter { let transitionHandler: UIViewController func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } }
Todavía tenemos que declarar este método en el protocolo de entrada del enrutador e implementarlo en el enrutador:
protocol SomeRouterInput { func close() } class SomeRouter: SomeRouterInput { var transitionHandler: UIViewController! lazy var closeRouter = { CloseRouter(transitionHandler: self. transitionHandler) }() func close() { self.closeRouter.close() } }
Resultó demasiado código que simplemente representa la llamada al método de cierre. El programador
Lazy Good no lo apreciará.
Solución de protocolo
Los protocolos vienen al rescate. Esta es una herramienta bastante poderosa que le permite implementar la composición y puede contener métodos de implementación en extensión. Entonces podemos crear un protocolo que contenga el método de cierre e implementarlo en extensión.
Así es como se verá:
protocol CloseRouterTrait { var transitionHandler: UIViewController! { get } func close() } extension CloseRouterTrait { func close() { self.transitionHandler.dismiss(animated: true, completion: nil) } }
La pregunta es, ¿por qué aparece la palabra rasgo en el nombre del protocolo? Es simple: puede especificar que este protocolo implemente sus métodos en extensión y debe usarse como una mezcla con otro tipo para expandir su funcionalidad.
Ahora, veamos cómo se verá el uso de dicho protocolo:
class SomeRouter: CloseRouterTrait { var transitionHandler: UIViewController! }
Si, eso es todo. Se ve genial :). Obtuvimos la composición al agregar el protocolo a la clase del enrutador, no escribimos una sola línea adicional y tuvimos la oportunidad de reutilizar el código.
¿Qué tiene de inusual este enfoque?
Puede que ya hayas hecho esta pregunta. Usar protocolos como un rasgo es bastante común. La principal diferencia es utilizar este enfoque como una solución arquitectónica dentro de la capa de presentación. Como cualquier solución arquitectónica, debe haber sus propias reglas y recomendaciones.
Aquí está mi lista:
- Los rasgos no deben almacenarse ni cambiar de estado. Solo pueden tener dependencias en forma de servicios, etc., que son propiedades de solo obtención.
- Los rasgos no deberían tener métodos que no se implementan en extensión, ya que esto viola su concepto
- Los nombres de los métodos en rasgos deberían reflejar explícitamente lo que hacen, sin estar vinculados al nombre del protocolo. Esto ayudará a evitar colisiones de nombres y aclarará el código.
De VIPER a MVP
Si cambia por completo a usar este enfoque con protocolos, las clases de enrutador e interactor se verán así:
class SomeRouter: CloseRouterTrait, OtherRouterTrait { var transitionHandler: UIViewController! } class SomeInteractor: SomeInteractorTrait { var someService: SomeServiceInput! }
Esto no se aplica a todas las clases; en la mayoría de los casos, el proyecto simplemente tendrá enrutadores e interactores vacíos. En este caso, puede interrumpir la estructura del módulo VIPER y cambiar sin problemas a MVP agregando protocolos de impureza al presentador.
Algo como esto:
class SomePresenter: CloseRouterTrait, OtherRouterTrait, SomeInteractorTrait, OtherInteractorTrait { var transitionHandler: UIViewController! var someService: SomeSericeInput! }
Sí, se pierde la capacidad de implementar el enrutador y el interactor como dependencias, pero en algunos casos este es el caso.
El único inconveniente es transitionHandler = UIViewController. Y de acuerdo con las reglas de VIPER Presenter, no se debe saber nada sobre la capa de Vista y cómo se implementa utilizando qué tecnologías. Esto se resuelve en este caso simplemente: los métodos de transición del UIViewController están "cerrados" por el protocolo, por ejemplo, TransitionHandler. Entonces Presenter interactuará con la abstracción.
Cambiar el comportamiento del rasgo
Veamos cómo puede cambiar el comportamiento en dichos protocolos. Esto será un análogo de la sustitución de algunas partes del módulo, por ejemplo, para pruebas o un código auxiliar temporal.
Como ejemplo, tome un interactor simple con un método que realiza una solicitud de red:
protocol SomeInteractorTrait { var someService: SomeServiceInput! { get } func performRequest(completion: @escaping (Response) -> Void) } extension SomeInteractorTrait { func performRequest(completion: @escaping (Response) -> Void) { someService.performRequest(completion) } }
Este es un código abstracto, por ejemplo. Supongamos que no necesitamos enviar una solicitud, pero solo tenemos que devolver algún tipo de trozo. Aquí vamos al truco: cree un protocolo vacío llamado Mock y haga lo siguiente:
protocol Mock {} extension SomeInteractorTrait where Self: Mock { func performRequest(completion: @escaping (Response) -> Void) { completion(MockResponse()) } }
Aquí, la implementación del método performRequest se ha cambiado para los tipos que implementan el protocolo Mock. Ahora necesita implementar el protocolo Mock para la clase que implementará SomeInteractor:
class SomePresenter: SomeInteractorTrait, Mock { // Implementation }
Para la clase SomePresenter, se llamará a la implementación del método performRequest, ubicado en extensión, donde Self satisface el protocolo Mock. Vale la pena eliminar el protocolo Mock y la implementación del método performRequest se tomará de la extensión habitual a SomeInteractor.
Si usa esto solo para pruebas, es mejor colocar todo el código asociado con la sustitución de la implementación en el objetivo de prueba.
Para resumir
En conclusión, vale la pena señalar los pros y los contras de este enfoque y en qué casos, en mi opinión, vale la pena usar.
Comencemos con los contras:
- Si se deshace del enrutador y el interactor, como se muestra en el ejemplo, se pierde la capacidad de implementar estas dependencias.
- Otra desventaja es el número cada vez mayor de protocolos.
- A veces el código puede no parecer tan claro como el uso de enfoques convencionales.
Los aspectos positivos de este enfoque son los siguientes:
- Lo más importante y obvio, la duplicación se reduce considerablemente.
- La unión estática se aplica a los métodos de protocolo. Esto significa que la determinación de la implementación del método ocurrirá en la etapa de compilación. Por lo tanto, durante la ejecución del programa, no se dedicará más tiempo a la búsqueda de una implementación (aunque este tiempo no es particularmente significativo).
- Debido al hecho de que los protocolos son pequeños "ladrillos", cualquier composición se puede componer fácilmente de ellos. Además en karma para mayor flexibilidad de uso.
- Facilidad de refactorización, no hay comentarios aquí.
- Puede comenzar a utilizar este enfoque en cualquier etapa del proyecto, ya que no afecta a todo el proyecto.
Considerar esta decisión buena o no es un asunto privado para todos. Nuestra experiencia con este enfoque fue positiva y resolvió problemas.
Eso es todo!