Patrones de diseño en Kotlin con ejemplos: nivel avanzado
A nivel inicial, los desarrolladores aprenden patrones clásicos: Singleton, Factory, Observer. Sin embargo, al construir sistemas complejos y escalables, se requiere un ajuste arquitectónico más fino. En este artículo analizaremos cuatro patrones de diseño avanzados en Kotlin: Composite, Visitor, State y Bridge. Cada patrón se ilustrará con un ejemplo práctico de código y una explicación de cuándo debe aplicarse.
1. Patrón Composite
El patrón Composite permite a los clientes trabajar de manera uniforme con objetos individuales y sus grupos. Es una solución ideal para construir estructuras jerárquicas, como sistemas de archivos, menús de interfaces o escenas gráficas.
En Kotlin, a menudo usamos clases sealed para representar nodos de un árbol. Consideremos un ejemplo de construcción de una estructura "Archivos y Carpetas":
sealed class FileSystemNode(open val name: String) { data class File(override val name: String, val size: Long) : FileSystemNode(name) data class Directory( override val name: String, val children: MutableList<FileSystemNode> = mutableListOf() ) : FileSystemNode(name) { fun add(node: FileSystemNode) { children.add(node) } fun remove(node: FileSystemNode) { children.remove(node) } fun getTotalSize(): Long = children.sumOf { when (it) { is File -> it.size is Directory -> it.getTotalSize() } } }}
// Usofun main() { val root = FileSystemNode.Directory("root") root.add(FileSystemNode.File("readme.txt", 1024)) val subDir = FileSystemNode.Directory("src") subDir.add(FileSystemNode.File("main.kt", 4096)) root.add(subDir) println("Tamaño total: ${root.getTotalSize()} bytes") // 5120}Ventajas: simplifica el código del cliente, soporta operaciones recursivas. Desventajas: puede complicar el sistema si el árbol es demasiado profundo.
2. Patrón Visitor
Visitor permite agregar nuevas operaciones a objetos sin cambiar sus clases. Esto es especialmente útil cuando se tiene una jerarquía de clases estable, pero algoritmos de procesamiento que cambian con frecuencia.
En Kotlin funciona muy bien con clases sealed. Implementemos un ejemplo de cálculo de área para figuras:
sealed class Shapeclass Circle(val radius: Double) : Shape()class Rectangle(val width: Double, val height: Double) : Shape()
interface ShapeVisitor<T> { fun visit(circle: Circle): T fun visit(rectangle: Rectangle): T}
class AreaCalculator : ShapeVisitor<Double> { override fun visit(circle: Circle) = Math.PI * circle.radius * circle.radius override fun visit(rectangle: Rectangle) = rectangle.width * rectangle.height}
// Usofun <T> Shape.accept(visitor: ShapeVisitor<T>): T = when (this) { is Circle -> visitor.visit(this) is Rectangle -> visitor.visit(this)}
fun main() { val shapes = listOf(Circle(5.0), Rectangle(3.0, 4.0)) val calculator = AreaCalculator() shapes.forEach { println("Área: ${it.accept(calculator)}") }}Cuándo aplicarlo: si la estructura de objetos rara vez cambia, pero las operaciones sobre ellos cambian con frecuencia. En Kotlin se puede usar when para casos simples, pero Visitor sigue siendo indispensable para lógica compleja con muchos tipos.
3. Patrón State
El patrón State permite que un objeto cambie su comportamiento dependiendo de su estado interno. Es una alternativa elegante a los engorrosos if-else o switch-case.
En Kotlin podemos usar clases sealed para representar los estados. Ejemplo: un pedido en una tienda en línea