Hay mucho bombo sobre VIPER en el último año, y todo el mundo está tan inspirado por él. La mayoría de estos artículos son tendenciosos al respecto y tratan de demostrar lo genial que es. No lo es. Tiene al menos la misma cantidad de problemas, si no más, que otros patrones de arquitectura. En este artículo, quiero explicar por qué VIPER no es tan bueno como se anuncia y no es adecuado para la mayoría de sus aplicaciones.
Algunos artículos sobre la comparación de arquitecturas suelen afirmar que VIPER es una arquitectura totalmente diferente a cualquier arquitectura basada en MVC. Esta afirmación no es cierta: es sólo un MVC normal, donde estamos dividiendo el controlador en dos partes: interactor y presentador. La vista sigue siendo la misma, pero el modelo pasa a llamarse entidad. El router merece algunas palabras especiales: Sí, es cierto, otras arquitecturas no promocionan esta parte en su abreviatura, pero sigue existiendo, de forma implícita (sí, cuando llamas a pushViewController
estás escribiendo un simple router) o más explícita (por ejemplo FlowCoordinators).
Ahora quiero hablar de las ventajas que te ofrece VIPER. Usaré este libro como una referencia completa sobre VIPER. Empecemos por el segundo objetivo, ajustarse al SRP (Single Responsibility Principle). Es un poco duro, pero ¿qué mente extraña puede llamar a esto un beneficio? Te pagan por resolver tareas, no por ajustarte a una palabra de moda. Sí, sigues usando TDD, BDD, pruebas unitarias, Realm o SQLite, inyección de dependencias, y todo lo que puedas recordar, pero lo usas para resolver el problema del cliente, no sólo para usarlo.
Otro objetivo es mucho más interesante. La testabilidad es una preocupación muy importante. Merece un artículo independiente ya que mucha gente está hablando de ello, pero sólo unos pocos están realmente probando sus aplicaciones y aún menos lo están haciendo bien.
Una de las principales razones de eso es la falta de buenos ejemplos. Hay muchos artículos sobre la creación de una prueba de unidad con assert 2 + 2 == 4
pero no hay ejemplos de la vida real (por cierto, Artsy está haciendo un excelente trabajo por el código abierto de sus aplicaciones, usted debe echar un vistazo a sus proyectos).
VIPER está sugiriendo para separar toda la lógica en un montón de pequeñas clases con responsabilidades separadas. Puede facilitar las pruebas, pero no siempre. Sí, escribir pruebas unitarias para una clase simple es fácil, pero la mayoría de estas pruebas no prueban nada. Por ejemplo, veamos la mayoría de los métodos de un presentador, son sólo un proxy entre una vista y otros componentes. Puedes escribir pruebas para ese proxy, esto aumentará la cobertura de pruebas de tu código, pero estas pruebas son inútiles. También tienes un efecto secundario: debes actualizar estas pruebas inútiles después de cada edición en el código principal.
El enfoque adecuado para las pruebas debe incluir las pruebas del interactor y del presentador al mismo tiempo porque estas dos partes están profundamente integradas. Además, al separar entre dos clases necesitamos muchas más pruebas en comparación con un caso en el que sólo tenemos una clase. Es una simple сombinatoria: la clase A tiene 4 estados posibles, la clase B tiene 6 estados posibles, así que una combinación de A y B tiene 20 estados, y deberías probarlo todo.
El enfoque correcto para simplificar las pruebas es introducir pureza en el código en lugar de simplemente dividir el estado complicado en un montón de clases.
Puede sonar extraño, pero probar las vistas es más fácil que probar algún código de negocio. Una vista expone el estado como un conjunto de propiedades y hereda la apariencia visual de estas propiedades. Entonces puedes usar FBSnapshotTestCase para hacer coincidir el estado con la apariencia. Todavía no maneja algunos casos de borde como las transiciones personalizadas, pero ¿con qué frecuencia lo implementas?
Diseño de sobreingeniería
VIPER es lo que sucede cuando los antiguos programadores de Java empresarial invaden el mundo de iOS. -n0damage, comentario en reddit
Sinceramente, ¿puede alguien mirar esto y decir: «sí, estas clases y protocolos extra realmente mejoraron mi capacidad de entender lo que está pasando con mi aplicación».
Imagina una tarea sencilla: tenemos un botón que desencadena una actualización desde un servidor y una vista con datos de una respuesta del servidor. Adivina cuántas clases/protocolos se verán afectados por este cambio? Sí, al menos 3 clases y 4 protocolos van a ser cambiados para esta simple función. ¿Nadie se acuerda de cómo Spring empezó con algunas abstracciones y terminó con AbstractSingletonProxyFactoryBean
? Yo siempre quiero una «superclase conveniente de proxy factory bean para proxy factory beans que crean sólo singletons» en mi código.
Componentes redundantes
Como ya he mencionado antes, un presentador suele ser una clase muy tonta que se limita a proxiar las llamadas de la vista al interactor (algo así). Sí, a veces contiene lógica complicada, pero en la mayoría de los casos es sólo un componente redundante.
Cantidad de protocolos «DI-friendly»
Hay una confusión común con esta abreviatura: VIPER está implementando los principios SOLID donde DI significa «inversión de dependencia», no «inyección». La inyección de dependencia es un caso especial del patrón de Inversión de Control, que está relacionado, pero es diferente de la Inversión de Dependencia.
La Inversión de Dependencia trata de separar módulos de diferentes niveles introduciendo abstracciones entre ellos. Por ejemplo, el módulo de interfaz de usuario no debería depender directamente del módulo de red o de persistencia. La inversión de control es diferente, es cuando un módulo (normalmente de una librería que no podemos cambiar) delega algo a otro módulo, que normalmente se proporciona al primer módulo como una dependencia. Sí, cuando implementas la fuente de datos para tu UITableView
estás utilizando el principio IoC. Usar las mismas características del lenguaje para diferentes propósitos de alto nivel es una fuente de confusión aquí.
Volvamos a VIPER. Hay muchos protocolos (al menos 5) entre cada clase dentro de un módulo. Y no son necesarios en absoluto. Presentador e Interactor no son módulos de diferentes capas. Aplicar el principio IoC puede tener sentido, pero hazte una pregunta: ¿con qué frecuencia implementas al menos dos presentadores para una vista? Creo que la mayoría de ustedes han respondido cero. Entonces, ¿por qué es necesario crear este montón de protocolos que nunca vamos a utilizar?
Además, debido a estos protocolos, no puedes navegar fácilmente por tu código con un IDE, porque cmd+click te llevará a un protocolo, en lugar de una implementación real.
Problemas de rendimiento
Es una cosa crucial y hay un montón de gente que simplemente no se preocupan por ello, o simplemente están subestimando un impacto de las malas decisiones de arquitectura.
No voy a hablar del framework Typhoon (que es muy popular para la inyección de dependencia en el mundo ObjC). Por supuesto, tiene un cierto impacto en el rendimiento, especialmente cuando se utiliza la auto-inyección, pero VIPER no requiere que lo utilices. En su lugar, quiero hablar sobre el tiempo de ejecución y el inicio de la aplicación y cómo VIPER está ralentizando su aplicación literalmente en todas partes.
Tiempo de inicio de la aplicación. Raramente se habla de ello, pero es un tema importante, si tu app se lanza muy lentamente los usuarios evitarán usarla. Hubo una sesión en la última WWDC sobre la optimización del tiempo de inicio de la aplicación en el año pasado. TL; DR: El tiempo de inicio de tu aplicación depende directamente del número de clases que tengas. Si tienes 100 clases está bien, nadie lo notará. Sin embargo, si tu aplicación tiene sólo 100 clases, ¿realmente necesitas esta complicada arquitectura? Pero si tu aplicación es enorme, por ejemplo, estás trabajando en la aplicación de Facebook (18k clases) este impacto es enorme, algo alrededor de 1 segundo de acuerdo con la sesión mencionada anteriormente. Sí, el lanzamiento en frío de tu app tardará 1 segundo sólo en cargar todos los metadatos de las clases y nada más, has leído bien.
Despacho en tiempo de ejecución. Es más complicado, mucho más difícil de perfilar y sobre todo aplicable para el compilador Swift solamente (porque ObjC tiene ricas capacidades de tiempo de ejecución y un compilador no puede realizar con seguridad estas optimizaciones). Hablemos de lo que ocurre bajo el capó cuando se llama a algún método (uso «llamar» en lugar de «enviar un mensaje» porque el segundo término no siempre es correcto para Swift). Hay 3 tipos de envío en Swift (de más rápido a más lento): estático, envío de tabla y envío de mensaje. El último es el único tipo que se utiliza en ObjC, y se utiliza en Swift cuando se requiere interop con código ObjC, o cuando declaramos un método como dynamic
. Por supuesto, esta parte del tiempo de ejecución está enormemente optimizada y escrita en ensamblador para todas las plataformas. Pero, ¿y si podemos evitar esta sobrecarga porque el compilador tiene conocimiento de lo que se va a llamar en tiempo de compilación? Es exactamente lo que hace el compilador de Swift con el envío estático y de tabla. El envío estático es rápido, pero el compilador no puede utilizarlo sin confiar al 100% en los tipos de la expresión. Pero cuando el tipo de nuestra variable es un protocolo, el compilador se ve obligado a utilizar el despacho a través de tablas testigo de protocolo. No es literalmente lento, pero 1ms por aquí, 1ms por allá, y ahora el tiempo total de ejecución es más de un segundo superior al que podríamos conseguir usando código Swift puro. Este párrafo está relacionado con el anterior sobre los protocolos, pero creo que es mejor separar las preocupaciones sobre la cantidad imprudente de protocolos no utilizados con un verdadero lío con el compilador con él.
Separación de abstracciones débiles
Debería haber una – y preferiblemente sólo una- forma obvia de hacerlo.
Una de las preguntas más populares en la comunidad VIPER es «¿dónde debo poner X?». Así que, por un lado, hay muchas reglas de cómo debemos hacer las cosas bien, pero por otro lado muchas cosas se basan en la opinión. Pueden ser casos complicados, por ejemplo manejar CoreData con NSFetchedResultsController
, o UIWebView
. Pero incluso casos comunes, como el uso de UIAlertController
, es un tema de discusión. Echemos un vistazo, aquí tenemos un router que maneja una alerta, pero allí tenemos una vista que presenta una alerta. Puedes intentar superar esto con un argumento que es una alerta simple es un caso especial para la alerta simple sin ninguna acción excepto el cierre.
Los casos especiales no son tan especiales como para romper las reglas.
Sí, pero ¿por qué aquí tenemos una fábrica para crear ese tipo de alertas? Entonces, tenemos un lío incluso con un caso tan simple con UIAlertController
. ¿Quieres?
Generación de código
La legibilidad cuenta.
¿Cómo puede ser un problema con una arquitectura? Es simplemente generar un montón de clases de plantillas en lugar de escribirlo yo mismo. Cuál es el problema con eso? El problema es que la mayoría de las veces estás leyendo código (a no ser que trabajes en una empresa de outsourcing de usar y tirar), no escribiéndolo. Por lo tanto, la mayor parte del tiempo estás leyendo un código de tipo boilerplate mezclado con un código real. ¿Es bueno? No lo creo.
Conclusión
No persigo el objetivo de desalentar el uso de VIPER en absoluto. Todavía puede haber algunas aplicaciones que pueden beneficiarse de todo. Sin embargo, antes de empezar a desarrollar tu aplicación deberías hacerte algunas preguntas:
- ¿Esta aplicación va a tener una larga vida útil?
- ¿Son las especificaciones lo suficientemente estables? De lo contrario, puedes acabar con interminables refactorizaciones enormes incluso para pequeños cambios.
- ¿Pruebas realmente tus aplicaciones? Sé honesto contigo mismo.
Sólo si respondes «sí» a todas las preguntas VIPER puede ser una buena opción para tu aplicación.
Finalmente, la última: deberías usar tu propio cerebro para tus decisiones, no confíes ciegamente en algún tipo de Medium o conferencia a la que hayas asistido que esté diciendo: «Usa X, X es genial». Estos tipos también pueden estar equivocados.