Mecanismos de delegación usados en soluciones móviles
“If you prefer to read the English version”
Contexto
Delegation es un mecanismo utilizado en las soluciones para proporcionar extensibilidad y flexibilidad a los diseños. Estas características son claves para que los componentes diseñados cumpla con un atributo de calidad (quality attribute) conocido como Mantenibilidad.
Mantenibilidad es un atributo valorado en las aplicaciones por ser un indicador de la capacidad que tiene una pieza de software para cambiar en el tiempo sin impactar en gran medida el diseño actual y manteniendo su operatividad consistente.
Como sabemos, en un proyecto, es del día a día entrar a modificar y actualizar componentes para agregar una nueva funcionalidad o capacidad, y es una gran ventaja cuando el componente se encuentra diseñado teniendo en mente la mantenibilidad, ya que será menor el esfuerzo y el impacto requerido para extender las responsabilidades y capacidades del mismo. Esto, en últimas se traduce en beneficios para la solución, el proyecto y el equipo.
¿Y cómo se aporta y logra tener un nivel de mantenibilidad deseado en los componentes? Haciendo uso de mecanismos de delegación.
La delegación puede ser aplicada a través de diferentes tipos de patrones de diseño y estrategias. En este artículo me enfocaré en los que comúnmente se podrían necesitar en las soluciones móviles.
En móviles, se cuenta con lenguajes funcionales de programación tales como Swift, Kotlin para iOS y Android respectivamente. Estos lenguajes modernos introducen aspectos funcionales desde su base permitiendo combinar diferentes paradigmas e implementaciones con programación funcional, reactiva e imperativa.
Las estrategias y patrones de diseño que describiré en este artículo no son nuevos, es más, forman parte del conjunto de patrones base descritos en el libro Design Patterns — Elements of Reusable Object-Oriented Software (by GoF), los cuales han sido una herramienta clave en el desarrollo de software desde hace tiempo.
Actualmente, a través de estos lenguajes funcionales podemos implementar dichos patrones ahorrando código repetitivo y de forma más expresiva. Adicionalmente, estos lenguajes promueven la delegación prefiriendo el uso de agregación y/o composición más que la herencia, lo cual en la mayoría de los casos aporta al diseño mayores beneficios.
La primera estrategia que voy a describir para aplicar delegación es el uso del patrón Decorator, pero antes debo aclarar la siguiente palabra clave:
Pieza de software: Hace referencia a una clase, estructura, enum, objeto o a cualquier dependencia externa o interna al proyecto.
Decorator
Case de uso: Tenemos una pieza de software a la cual queremos extenderle las funcionalidades sin modificar la pieza original. Adicional, se quiere evitar heredar dependencias innecesarias.
Este patrón permite extender las responsabilidades de un componente sin modificar su definición base. Ya sea un componen externo (de un tercero) o uno interno de nuestra implementación, este podría ser ajustado con mayores capacidades sin llegar a modificar la implementación del componente.
Implementación: Tanto en Swift como en Kotlin se puede usar el concepto extension functions para implementar este patrón así:
Ejemplo en iOS
Resultado en consola:
Executed base operation in BaseComponent
Executed an extra operation for BaseComponent 🚀
30.25
Ejemplo en Android
Resultado en consola:
Executed base operation in BaseComponent
Executed an extra operation for BaseComponent 🚀
30.25
La delegación en este caso se hace por agregación y no por herencia lo cual es una mejor aprovechamiento en la mayoría de los casos.
Adapter
Caso de uso: Tenemos una pieza de software a la cual queremos ajustar para que cumpla con un determinado contrato sin modificar la pieza original. De tal forma que el ajuste permita a la pieza de software integrarse a una estructura deseada.
Este patrón permite a través de extensión incorporar un cambio al componente para ajustarlo a una estructura deseada sin necesitar modificar la estructura del componente base.
Implementación: En Swift, se recurre al uso de protocol extension para la implementación de este patrón. En Kotlin, se usa delegación de clase a través de extension functions + interfaces así:
Ejemplo en iOS
Resultado en consola:
The base operation in LegacyComponent has been adapted
Ejemplo en Android
Resultado en consola:
The base operation in LegacyComponent has been adapted
Kotlin también proporciona otro tipo de delegación para los casos en que se conforman la misma interface, más adelante se mostrará un ejemplo de dicha estrategia usando”by” keyword.
El patrón Decorator y Adapter parecen ser formas similares de delegación, pero, se diferencian principalmente en que el Decorator busca extender las capacidades sin pretender cambiar la estructura o los contratos que el componente cumpla, mientras que el Adapter lo que busca es extender el componente para que asuma una estructura que lo haga compatible con un contrato determinado.
Strategy
Caso de uso: Tenemos una pieza de software que requiere ser dotada con un conjunto de capacidades extras con diferentes tipos de comportamiento (lógica).
El propósito del patrón Strategy es extender el comportamiento del componente (lógica) más que su forma, por ello se le clasifica como un Behavioral Pattern, es decir, un patrón de comportamiento. Mientras que los patrones anteriores, Decorator y Adapter, son clasificados como Structural Patterns, es decir, patrones estructurales. Me gusta mucho la frase con la que hacen referencia a este hecho en Design Patterns — Elements of Reusable Object-Oriented Software (by GoF):
“A decorator lets you change the skin of an object; a strategy lets you change the guts. These are two alternative ways of changing an object”.
Implementación: En Swift, la implementación de este patrón puede hacer a través de protocols. En Kotlin, la delegación es natural e incorporada por defecto a través de la palabra clave “by” así como se mostrará a continuación:
Ejemplo en iOS
Resultado en consola:
Operation executed by StrategyOne for ClientUsingStrategy
Operation executed by StrategyTwo for ClientUsingStrategy
Ejemplo en Android
Resultado en consola:
Operation executed by StrategyOne for ClientUsingStrategy
Operation executed by StrategyTwo for ClientUsingStrategy
Para finalizar, describiré otra estrategia que en mi opinión más que otro patrón, es la aplicación del patrón Strategy simplificado, es decir, cuando solo se aplica una única opción de estrategia. Este mecanismo es conocido como Delegate y es otra forma de delegación.
Delegate
Ejemplo en iOS
Resultado en consola:
Operation delegated to Manager for the ClientUsingDelegate
Ejemplo en Android
Resultado en consola:
Operation delegated to Manager for the ClientUsingDelegate
Conclusiones
Por supuesto, en móviles podemos emplear muchos otros patrones de diseño. En este artículo me he enfocado en los proporcionan delegación.
Esto patrones pueden ser aplicados en diferentes capas y tipo de artefactos de la aplicación móvil, es decir, podría ser aplicado en Repositories, ViewModels, Uses Cases, DataSources o cualquier otro componente.
También he querido mostrar una forma alternativa de implementarlos, diferente a la usual forma imperativa, esto gracias a las ventajas que ofrecen los lenguajes modernos como Swift y Kotlin.