Decorator Pattern – Una implementación del Patrón Decorador en kotlin
Hola con tod@s, en este nuevo año vamos a empezar analizando unos patrones de diseño de software, concretamente nos centraremos en el patrón de diseño de software Decorador. Así mismo, implementaremos un ejemplo de un servicio de venta de hosting en kotlin con el objetivo de analizar cómo puede, el patrón Decorador, ayudarnos en esta problemática concreta. ¡Empecemos!
Patrones de Diseño de Software
Los patrones de diseño de software están para facilitarnos la vida y evitar reinventar la rueda cuando diseñamos nuestros programas. Normalmente existen ciertos problemas comunes con los que nos enfrentamos día a día en el mundo de la tecnología y para ello existen, a su vez, soluciones bien definidas que han evolucionado hasta constituirse como conjuntos de buenas prácticas de programación utilizados por desarrolladores en todas las esferas. A este conjunto de buenas prácticas les conocemos como patrones de diseño de software y normalmente describen una problemática, una solución, casos de aplicación y consecuencias.
Si deseas conocer más de patrones de diseño de software, te recomiendo el siguiente enlace: Software Design Patterns.
Decorator Pattern - Patrón Decorador
Existen varios tipos de patrones de diseño de software, entre los cuales podemos nombrar los patrones creacionales, patrones estructurales y los patrones de comportamiento. El Patrón Decorador pertenece al tipo de patrones de diseño estructurales que nos explican cómo podemos ensamblar objetos y clases dentro de estructuras flexibles y eficientes.
El patrón decorador, concretamente, soluciona esta problemática: poder extender la funcionalidad de objetos de manera dinámica o en tiempo de ejecución. La idea principal radica en tomar un objecto cuyo comportamiento esté bien definido y buscar extender este comportamiento sin tocar el objeto como tal y, a su vez, hacerlo de manera dinámica o en tiempo de ejecución obteniendo como resultado versiones extendidas del mismo objeto. Quizá la mejor manera de entender este patrón es con una analogía a la vida real y, que mejor manera, que con un objeto clásico ruso que seguramente hemos visto antes: La Muñeca Matrioshka, aquella muñeca que contiene versiones más pequeñas de sí misma dentro de sí.
Muñeca Matrioshka
Gráfico: thenounproject.com
Implementando el patrón decorador en kotlin
Vamos a modelar un pequeño caso de la vida real haciendo uso del patrón decorador. Imaginemos que vamos a posicionar una nueva empresa de venta de servicios de hosting, para lo cual poseemos la infraestructura y los proveedores adecuados. Tras un estudio previo, decidimos empezar a ofrecer 1 plan de hosting base que contiene unas características comunes que serán la base de cualquier plan de hosting que se ofrezca. Aparte, para usuarios con características más avanzadas, decidimos crear 2 paquetes de características adicionales (Bronce y Oro) que se pueden adicionar a un plan base y, por un precio adicional, el usuario puede adicionar estas características a su plan, contruyendo un plan a medida. En este escenario, el cliente puede comprar un Hosting Base + Características de Bronce o un Hosting Base + Características de Oro o a su vez, un plan completo que incluye Hosting Base + Catacterísticas de Bronce + Características de Oro. Evidentemente, el plan base tiene un precio base y cada paquete de características adiciona un precio también que se suma para darnos un valor total de acuerdo a lo que el cliente haya seleccionado. Podemos mencionar también esta restricción evidente: no se puede seleccionar un paquete cualquiera de características sin antes seleccionar un plan hosting base.
Ahora que conocemos la problemática, vamos a desarrollar en kotlin haciendo uso del patrón decorador para observar lo fácil y elegante que resulta este patrón en la solución de esta problemática.
Diagrama de Clases
Acorde con la problemática descrita, nuestro diagrama de clases quedara de la siguiente manera:
Diagrama de clases - Decorator Pattern
En donde podemos observar las siguientes características:
- Existe un comportamiento base que tiene el objeto y está descrito por los métodos de la interfaz Hosting.
- Existe un objeto instanciado (HostingImpl) que implementa directamente este comportamiento, el cual servirá como nuestro plan de hosting básico.
- Se define la clase abstracta HostingDecorator que implementa e instancia los comportamientos del objeto base.
- Los objetos que implementan el HostingDecorator son las características adicionales del plan de hosting que no son mas que objetos extendidos de la misma clase.
Código Fuente
Vamos a ver como implementamos el diagrama anterior en código fuente en kotlin.
Primero definimos la interfaz Hosting:
interface Hosting {
fun getFeatures(): String
fun getMonthlyPrice(): BigDecimal
}
Definimos, de la misma manera, el objeto que implementa directamente esta funcionalidad, el HostingImpl que nos servirá com el plan de hosting básico:
class HostingImpl: Hosting {
override fun getFeatures(): String {
return """
This is a Hosting Service
Top Features:
- 1 Website
- Linux Servers
- Free SSL
- 1 Free Domain
- Unlimited Subdomains
- 20 GB SSD Storage
- Python/NodeJs compatibility
- 5 Databases
- 24/7 Support
""".trimIndent()
}
override fun getMonthlyPrice(): BigDecimal {
return BigDecimal("5.99")
}
}
El objeto más importante del patrón decorador es el decorator como tal y el que nos permitirá instanciar objetos que aumenten la funcionalidad del objeto base. En nuestro caso, se denomina HostingDecorator y esta definido de la siguiente manera:
abstract class HostingDecorator(open val hosting: Hosting) : Hosting {
override fun getFeatures(): String {
return hosting.getFeatures()
}
override fun getMonthlyPrice(): BigDecimal {
return hosting.getMonthlyPrice()
}
}
De esta manera tenemos el patrón decorador creado, lo que nos resta es definir los objetos que implementarán el patrón y seran objetos extendidos de Hosting.
El primero corresponde a las características de bronce, se denomina BonzeFeatures:
class BronzeFeatures (override val hosting: Hosting) : HostingDecorator(hosting) {
override fun getFeatures(): String {
return super.getFeatures() + "\n" + getBronzeFeatures()
}
override fun getMonthlyPrice(): BigDecimal {
return super.getMonthlyPrice().add(getAdditionalMonthlyValue())
}
private fun getBronzeFeatures(): String {
return """
Bronze Features:
- SSH Access
- 20 GB SSD Additional Storage
- Free CDN Included
""".trimIndent()
}
private fun getAdditionalMonthlyValue(): BigDecimal {
return BigDecimal("2.25")
}
}
El segundo corresponde a las características de oro, se denomina GoldFeatures:
class GoldFeatures (override val hosting: Hosting) : HostingDecorator(hosting) {
override fun getFeatures(): String {
return super.getFeatures() + "\n" + getGoldFeatures()
}
override fun getMonthlyPrice(): BigDecimal {
return super.getMonthlyPrice().add(getAdditionalMonthlyValue())
}
private fun getGoldFeatures(): String {
return """
Gold Features:
- Java/Ruby/Rust compatibility
- Unlimited Websites
- Unlimited Storage
- Free Dedicated IP
- Free Automated Backup
""".trimIndent()
}
private fun getAdditionalMonthlyValue(): BigDecimal {
return BigDecimal("5.99")
}
}
Con lo que tenemos prácticamente todo el problema resuelto en esos modelos. Vamos a probar esta funcionalidad instanciando un método Main que genere un plan personalizado de hosting y sea un plan completo, en este caso sería un plan Hosting Base + Catacterísticas de Bronce + Características de Oro, haremos que nos despliegue las características que incluye el plan y que calcule el valor mensual total:
object Main {
@JvmStatic
fun main(args: Array<String>){
print(" ============= MY CUSTOMIZED HOSTING PLAN =============\n")
val myCustomizedHostingPlan: Hosting = GoldFeatures(BronzeFeatures (HostingImpl()))
print(myCustomizedHostingPlan.getFeatures())
print("\nTOTAL MONTHLY PRICE: " + myCustomizedHostingPlan.getMonthlyPrice() + "\n")
}
}
Donde podemos observar que dentro de un objeto de tipo Hosting envío otro objeto del mismo tipo como parámetro, que a su vez envía otro objeto del mismo tipo como parámetro. Esta instanciación cíclica puede repetirse con n objetos del mismo tipo siempre dejando como último objeto, la implementación directa del objeto base.
El resultado de ejecutar este programa nos muestra:
============= MY CUSTOMIZED HOSTING PLAN =============
This is a Hosting Service
Top Features:
- 1 Website
- Linux Servers
- Free SSL
- 1 Free Domain
- Unlimited Subdomains
- 20 GB SSD Storage
- Python/NodeJs compatibility
- 5 Databases
- 24/7 Support
Bronze Features:
- SSH Access
- 20 GB SSD Additional Storage
- Free CDN Included
Gold Features:
- Java/Ruby/Rust compatibility
- Unlimited Websites
- Unlimited Storage
- Free Dedicated IP
- Free Automated Backup
TOTAL MONTHLY PRICE: 14.23
Process finished with exit code 0
Que no es más que la suma de características de cada objeto, con la sumatoria del valor total. De esta manera hemos implementado y analizado las ventajas de la utilización del patrón decorator dentro de los problemas comunes en el mundo de las TICs.
Este patrón es muy útil, por ejemplo, cuando se desea utilizar una clase u objeto que pertenece a una librería de terceros que usamos como dependencia y de la que no podemos modificar. Entonces usamos este patrón para aumentar su funcionalidad sin dejar de ser un objeto de la misma clase.
Recuerda dejarnos cualquier duda o comentario en el recuadro de la parte baja.
El código fuente de este ejemplo está disponible en el enlace a continuación.
Código fuente: https://github.com/nano-bytes/kotlin-samples/tree/main/decorator-pattern-example.
¡ Feliz 2021 y hasta una próxima entrega !