Cuatro reglas para un diseño de software iOS más simple

A fines de la década de 1990, mientras desarrollaba Extreme Programming, el famoso desarrollador de software Kent Beck elaboró ​​una lista de reglas para un diseño de software simple.

Según Kent Beck, un buen diseño de software:

  • Ejecuta todas las pruebas
  • No contiene duplicación
  • Expresa la intención del programador.
  • Minimiza el número de clases y métodos.

En este artículo, discutiremos cómo se pueden aplicar estas reglas al mundo del desarrollo de iOS dando ejemplos prácticos de iOS y discutiendo cómo podemos beneficiarnos de ellas.

Ejecuta todas las pruebas

El diseño de software nos ayuda a crear un sistema que actúa según lo previsto. Pero, ¿cómo podemos verificar que un sistema actuará según lo previsto inicialmente por su diseño? La respuesta es mediante la creación de pruebas que lo validen.

Desafortunadamente, en el universo de desarrollo de iOS, las pruebas se evitan la mayoría de las veces ... Pero para crear un software bien diseñado, siempre debemos escribir el código Swift con la capacidad de prueba en mente.

Analicemos dos principios que pueden simplificar la redacción de pruebas y el diseño del sistema. Y son Principio de Responsabilidad Única e Inyección de Dependencia.

Principio de responsabilidad única (SRP)

SRP establece que una clase debe tener una y solo una razón para cambiar. El SRP es uno de los principios más simples y uno de los más difíciles de acertar. Mezclar responsabilidades es algo que hacemos naturalmente.

Vamos a proporcionar un ejemplo de algún código que es realmente difícil de probar y luego refactorizarlo usando SRP. Luego discuta cómo hizo que el código sea comprobable.

Supongamos que actualmente necesitamos presentar un PaymentViewController desde nuestro controlador de vista actual, PaymentViewController debería configurar su vista dependiendo de nuestro precio de producto de pago. En nuestro caso, el precio es variable dependiendo de algunos eventos de usuarios externos.

El código para esta implementación actualmente tiene el siguiente aspecto:

¿Cómo podemos probar este código? ¿Qué deberíamos probar primero? ¿El descuento de precio se calcula correctamente? ¿Cómo podemos burlarnos de los eventos de pago para probar el descuento?

Escribir pruebas para esta clase sería complicado, deberíamos encontrar una mejor manera de escribirlo. Bueno, primero abordemos el gran problema. Necesitamos desenredar nuestras dependencias.

Vemos que tenemos lógica para cargar nuestro producto. Tenemos eventos de pago que hacen que el usuario sea elegible para un descuento. Tenemos descuentos, un cálculo de descuento y la lista continúa.

Así que intentemos traducirlos simplemente al código Swift.

Creamos un PaymentManager que gestiona nuestra lógica relacionada con los pagos y un Calculador de precios separado que es fácilmente comprobable. Además, un cargador de datos que es responsable de la interacción de la red o la base de datos para cargar nuestros productos.

También mencionamos que necesitamos una clase responsable de administrar los descuentos. Llamémoslo CouponManager y dejemos que también administre cupones de descuento para usuarios.

Nuestro controlador de vista de pago puede verse así:

Podemos escribir ahora pruebas como

  • testCalculatingFinalPriceWithoutCoupon
  • testCalculatingFinalPriceWithCoupon
  • testCouponExists

y muchos otros! Al crear objetos separados ahora, evitamos la duplicación innecesaria y también creamos un código para el que es fácil escribir pruebas.

Inyección de dependencia

El segundo principio es la inyección de dependencia. Y vimos de los ejemplos anteriores que ya usamos inyección de dependencia en nuestros inicializadores de objetos.

Hay dos ventajas principales de inyectar nuestras dependencias como las anteriores. Deja en claro en qué dependencias dependen nuestros tipos y nos permite insertar objetos simulados cuando queremos probar en lugar de los reales.

Una buena técnica es crear protocolos para nuestros objetos y proporcionar una implementación concreta por el objeto real y el objeto simulado como el siguiente:

Ahora podemos decidir fácilmente qué clase queremos inyectar como dependencia.

El acoplamiento apretado hace que sea difícil escribir pruebas. De manera similar, mientras más pruebas escribamos, más usamos principios como DIP y herramientas como inyección de dependencia, interfaces y abstracción para minimizar el acoplamiento.

Hacer que el código sea más verificable no solo elimina nuestro miedo a romperlo (ya que escribiremos la prueba que nos respaldará), sino que también contribuye a escribir un código más limpio.

Esta parte del artículo se refería más a cómo escribir código que será comprobable que a escribir la prueba de unidad real. Si desea obtener más información sobre cómo escribir la prueba de la unidad, puede consultar este artículo donde creo el juego de la vida utilizando el desarrollo basado en pruebas.

No contiene duplicación

La duplicación es el enemigo principal de un sistema bien diseñado. Representa trabajo adicional, riesgo adicional, agrega complejidad innecesaria.

En esta sección, discutiremos cómo podemos usar el patrón de diseño de plantilla para eliminar la duplicación común en iOS. Para que sea más fácil de entender, vamos a refactorizar la implementación de un chat de la vida real.

Supongamos que actualmente tenemos en nuestra aplicación una sección de chat estándar. Surge un nuevo requisito y ahora queremos implementar un nuevo tipo de chat: un chat en vivo. Un chat que debe contener mensajes con un máximo de 20 caracteres y este chat desaparecerá cuando descartemos la vista del chat.

Este chat tendrá las mismas vistas que nuestro chat actual, pero tendrá algunas reglas diferentes:

  1. La solicitud de red para enviar mensajes de chat será diferente.

2. Los mensajes de chat deben ser cortos, no más de 20 caracteres por mensaje.

3. Los mensajes de chat no deben persistir en nuestra base de datos local.

Supongamos que estamos utilizando la arquitectura MVP y actualmente manejamos la lógica para enviar mensajes de chat en nuestro presentador. Intentemos agregar nuevas reglas para nuestro nuevo tipo de chat llamado live-chat.

Una implementación ingenua sería como la siguiente:

Pero, ¿qué sucede si en el futuro tendremos muchos más tipos de chat?
Si continuamos agregando si más que verifica el estado de nuestro chat en cada función, el código se volverá complicado de leer y mantener. Además, es poco comprobable y la verificación de estado se duplicaría en todo el alcance del presentador.

Aquí es donde entra en uso el patrón de plantilla. El patrón de plantilla se usa cuando necesitamos múltiples implementaciones de un algoritmo. La plantilla se define y luego se construye con más variaciones. Use este método cuando la mayoría de las subclases necesiten implementar el mismo comportamiento.

Podemos crear un protocolo para Chat Presenter y separamos métodos que serán implementados de manera diferente por objetos concretos en las fases de Chat Presenter.

Ahora podemos hacer que nuestro presentador se ajuste al IChatPresenter

Nuestro presentador ahora maneja el envío de mensajes llamando a funciones comunes dentro de sí mismo y delega las funciones que se pueden implementar de manera diferente.

Ahora podemos proporcionar Crear objetos que se ajusten a las fases del presentador y configurar estas funciones según sus necesidades.

Si usamos la inyección de dependencia en nuestro controlador de vista, ahora podemos reutilizar el mismo controlador de vista en dos casos diferentes.

Al usar Patrones de diseño realmente podemos simplificar nuestro código iOS. Si desea saber más sobre eso, el siguiente artículo proporciona más explicaciones.

Expresivo

La mayor parte del costo de un proyecto de software está en el mantenimiento a largo plazo. Escribir un código fácil de leer y mantener es imprescindible para los desarrolladores de software.

Podemos ofrecer un código más expresivo mediante el uso de una buena prueba de nomenclatura, uso de SRP y escritura.

Nombrar

La cosa número uno que hace que el código sea más expresivo, y está nombrando. Es importante escribir nombres que:

  • Revelar intenciones
  • Evitar la desinformación
  • Son fácilmente buscables

Cuando se trata de nombrar clases y funciones, un buen truco es usar un sustantivo o una frase nominal para clases y verbos de usuario o nombres de frases verbales para métodos.

Además, cuando se usan patrones de diseño diferentes, a veces es bueno agregar los nombres de patrones como Command o Visitor en el nombre de la clase. Por lo tanto, el lector sabría de inmediato qué patrón se usa allí sin tener la necesidad de leer todo el código para averiguarlo.

Usando SRP

Otra cosa que hace que el código sea expresivo es el uso del Principio de responsabilidad única que se mencionó anteriormente. Puede expresarse manteniendo sus funciones y clases pequeñas y con un solo propósito. Las clases y funciones pequeñas suelen ser fáciles de nombrar, fáciles de escribir y fáciles de entender. Una función debe servir solo para un propósito.

Examen de escritura

Escribir pruebas también aporta mucha claridad, especialmente cuando se trabaja con código heredado. Las pruebas unitarias bien escritas también son expresivas. Un objetivo principal de las pruebas es actuar como documentación por ejemplo. Alguien que lea nuestras pruebas debería poder comprender rápidamente de qué se trata una clase.

Minimizar el número de clases y métodos.

Las funciones de una clase deben permanecer cortas, una función siempre debe realizar una sola cosa. Si una función tiene demasiadas líneas, ese podría ser el caso de que esté realizando acciones que se pueden separar en dos o más funciones separadas.

Un buen enfoque es contar líneas físicas e intentar apuntar a un máximo de cuatro a seis líneas de funciones, en la mayoría de los casos, cualquier cosa que supere ese número de líneas puede ser difícil de leer y mantener.

Una buena idea en iOS es cortar las llamadas de configuración que solemos hacer en las funciones viewDidLoad o viewDidAppear.

De esta manera, cada una de las funciones sería pequeña y mantenible en lugar de una función mess viewDidLoad. Lo mismo también debería aplicarse para el delegado de la aplicación. Deberíamos evitar lanzar cada configuración en el método ondidFinishLaunchingWithOptions y funciones de configuración separadas o incluso mejores clases de configuración.

Con las funciones, es un poco más fácil medir si lo mantenemos corto o largo, la mayoría de las veces solo podemos contar las líneas físicas. Con las clases, usamos una medida diferente. Contamos las responsabilidades. Si una clase tiene solo cinco métodos, no significa que la clase sea pequeña, podría ser que tiene demasiadas responsabilidades solo con esos métodos.

Un problema conocido en iOS es el gran tamaño de UIViewControllers. Es cierto que con el diseño del controlador de vista de Apple, es difícil mantener estos objetos para un único propósito, pero debemos hacer todo lo posible.

Hay muchas maneras de hacer que UIViewControllers sea pequeño, mi preferencia es usar una arquitectura que tenga una mejor separación de las preocupaciones, como VIPER o MVP, pero eso no significa que no podamos mejorarla también en Apple MVC.

Al tratar de separar tantas preocupaciones, podemos llegar a un código bastante decente con cualquier arquitectura. La idea es crear clases de un solo propósito que puedan servir como ayudantes para los controladores de vista y hacer que el código sea más legible y comprobable.

Algunas cosas que pueden evitarse simplemente sin excusa en los controladores de vista son:

  • En lugar de escribir el código de red directamente, debería haber un NetworkManager, una clase responsable de las llamadas de red.
  • En lugar de manipular datos en los controladores de vista, simplemente podemos crear un DataManager, una clase responsable de eso.
  • En lugar de jugar con las cadenas UserDefaults en UIViewController, podemos crear una fachada sobre eso.

En conclusión

Creo que deberíamos componer software a partir de componentes con nombres precisos, simples, pequeños, responsables de una cosa y reutilizables.

En este artículo, discutimos cuatro reglas para el diseño simple de Kent Beck y proporcionamos ejemplos prácticos de cómo podemos implementarlas en el entorno de desarrollo de iOS.

Si te ha gustado este artículo, asegúrate de aplaudir para mostrar tu apoyo. Sígueme para ver muchos más artículos que pueden llevar tus habilidades de desarrollador de iOS al siguiente nivel.

Si tiene alguna pregunta o comentario, siéntase libre de dejar una nota aquí o enviarme un correo electrónico a arlindaliu.dev@gmail.com.