Introducción: Por qué SOLID es importante para un desarrollador de Swift
El desarrollo para iOS y macOS requiere no solo conocimiento de frameworks, sino también la capacidad de crear una arquitectura mantenible, testeable y escalable. Aquí es donde entran en juego los principios SOLID — cinco reglas fundamentales del diseño orientado a objetos, formuladas por Robert Martin (Tío Bob).
En este artículo, analizaremos cada uno de los cinco principios SOLID con ejemplos de código en Swift, mostraremos antipatrones típicos y propondremos soluciones correctas. Después de leerlo, podrás escribir código que sea fácil de mantener, extender y probar.
1. Single Responsibility Principle (SRP) — Principio de responsabilidad única
Enunciado: Una clase debe tener una sola razón para cambiar. En otras palabras, cada módulo es responsable de exactamente una funcionalidad.
Problema: La clase monstruo
Consideremos un ejemplo típico del desarrollo iOS: la clase UserManager, que hace demasiado:
class UserManager { func fetchUser() -> User { /* solicitud de red */ } func saveUserToDatabase(_ user: User) { /* trabajo con CoreData */ } func formatUserDisplayName(_ user: User) -> String { /* formateo */ } func sendWelcomeEmail(to user: User) { /* envío de email */ }}Aquí la clase es responsable de solicitudes de red, base de datos, formateo de UI y envío de correos. Cambiar cualquiera de estos aspectos implicaría modificar UserManager.
Solución: Separación de responsabilidades
class UserNetworkService { func fetchUser() -> User { /* solo solicitud de red */ }}
class UserDatabaseService { func saveUser(_ user: User) { /* solo trabajo con BD */ }}
class UserFormatter { func displayName(for user: User) -> String { /* solo formateo */ }}
class EmailService { func sendWelcome(to user: User) { /* solo envío de email */ }}Ahora cada clase tiene una responsabilidad clara, lo que simplifica las pruebas y la modificación.
2. Open/Closed Principle (OCP) — Principio de abierto/cerrado
Enunciado: Las entidades de software deben estar abiertas para la extensión, pero cerradas para la modificación.
Problema: Lógica rígida
Supongamos que tenemos una clase para calcular descuentos:
class DiscountCalculator { func calculateDiscount(for order: Order, customerType: String) -> Double { if customerType == "regular" { return order.total * 0.05 } else if customerType == "premium" { return order.total * 0.1 } else if customerType == "vip" { return order.total * 0.2 } return 0 }}Al agregar un nuevo tipo de cliente (gold), habría que modificar la clase existente, lo que viola OCP.
Solución: Protocolos y polimorfismo
protocol DiscountStrategy { func discount(for order: Order) -> Double}
struct RegularDiscount: DiscountStrategy { func discount(for order: Order) -> Double { order.total * 0.05 }}
struct PremiumDiscount: DiscountStrategy { func discount(for order: Order) -> Double { order.total * 0.1 }}
struct VIPDiscount: DiscountStrategy { func discount(for order: Order) -> Double { order.total * 0.2 }}
class DiscountCalculator { func calculateDiscount(for order: Order, using strategy: DiscountStrategy) -> Double { return strategy.discount(for: order) }}Ahora, para agregar un nuevo descuento, basta con crear una nueva estructura que implemente el protocolo DiscountStrategy, sin modificar DiscountCalculator.
3. Liskov Substitution Principle (LSP) — Principio de sustitución de Barbara Liskov
Enunciado: