Introducción a los principios SOLID en Swift
\nCada desarrollador de iOS, tarde o temprano, se enfrenta al problema del "código espagueti": las clases se hinchan, los cambios en un lugar rompen toda la aplicación y las pruebas se convierten en una pesadilla. La solución a estos problemas se encuentra en la base del diseño orientado a objetos: los principios SOLID. Estas cinco reglas, formuladas por Robert Martin, ayudan a crear código flexible, mantenible y escalable en Swift. En este artículo, analizaremos cada principio desde la teoría hasta ejemplos prácticos para iOS/macOS.
\nIndependientemente de si escribes en UIKit, SwiftUI o AppKit, SOLID es una base universal. Profundicemos en los detalles.
\n\n1. Single Responsibility Principle (SRP) — Principio de Responsabilidad Única
\nTeoría
\nUna clase debe tener solo una razón para cambiar. En otras palabras, cada módulo es responsable de exactamente una funcionalidad. Si tu ViewController gestiona la UI, maneja solicitudes de red y analiza JSON, está violando el SRP.
\nPráctica en Swift
\nConsideremos un ejemplo típico: cargar y mostrar datos de un usuario.
\n// ❌ Mal: violación del SRP\nclass UserViewController: UIViewController {\n func loadUser() {\n // Solicitud de red\n let url = URL(string: "https://api.example.com/user")!\n URLSession.shared.dataTask(with: url) { data, _, _ in\n let user = try? JSONDecoder().decode(User.self, from: data!)\n DispatchQueue.main.async {\n self.updateUI(with: user)\n }\n }.resume()\n }\n \n func updateUI(with user: User?) {\n // Actualización de la UI\n }\n}\n// ✅ Bien: separamos responsabilidades\nclass UserService {\n func fetchUser(completion: @escaping (User?) -> Void) {\n let url = URL(string: "https://api.example.com/user")!\n URLSession.shared.dataTask(with: url) { data, _, _ in\n let user = try? JSONDecoder().decode(User.self, from: data!)\n completion(user)\n }.resume()\n }\n}\n\nclass UserViewController: UIViewController {\n let userService = UserService()\n \n func loadUser() {\n userService.fetchUser { [weak self] user in\n DispatchQueue.main.async {\n self?.updateUI(with: user)\n }\n }\n }\n \n private func updateUI(with user: User?) {\n // Actualización de la UI\n }\n}\nConclusión: Ahora UserService es responsable solo de la red, y UserViewController solo de la UI. Los cambios en la lógica de red no afectan al controlador.
2. Open-Closed Principle (OCP) — Principio de Abierto/Cerrado
\nTeoría
\nLas clases deben estar abiertas para la extensión, pero cerradas para la modificación. En lugar de cambiar el código existente, agregamos nuevo código mediante herencia o protocolos. En Swift, la herramienta ideal para esto son los protocolos y las extensiones.
\nPráctica en Swift
\nImagina que estamos escribiendo un módulo para calcular descuentos en una tienda en línea.
\n// ❌ Mal: al agregar un nuevo descuento, hay que modificar la clase\nclass DiscountCalculator {\n func calculate(price: Double, type: String) -> Double {\n if type == "seasonal" {\n return price * 0.9\n } else if type == "loyalty" {\n return price * 0.85\n }\n return price\n }\n}\n// ✅ Bien: usamos un protocolo para la extensión\nprotocol DiscountStrategy {\n func applyDiscount(to price: Double) -> Double\n}\n\nstruct SeasonalDiscount: DiscountStrategy {\n func applyDiscount(to price: Double) -> Double {\n price * 0.9\n }\n}\n\nstruct LoyaltyDiscount: DiscountStrategy {\n func applyDiscount(to price: Double) -> Double {\n price * 0.85\n }\n}\n\nclass DiscountCalculator {\n func calculate(price: Double, strategy: DiscountStrategy) -> Double {\n strategy.