Refactorización de código legacy en Swift: de la A a la Z
\nTrabajar con código obsoleto (legacy code) es una parte inevitable de la vida del desarrollador iOS/macOS. A menudo es código escrito en versiones tempranas de Swift o incluso Objective-C, carente de pruebas, con "números mágicos" y UIViewController gigantescos. En este artículo, analizaremos una estrategia paso a paso para refactorizar dicho código, transformándolo en un proyecto moderno, mantenible y testeable.
1. Preparación y seguridad: antes de tocar el código
\nLa regla más importante de la refactorización: no rompas lo que funciona. Sin pruebas, cualquier cambio es un salto a lo desconocido.
\n- \n
- Pruebas de caracterización (Characterization Tests): Escriba pruebas que capturen el comportamiento actual. Incluso si el código parece incorrecto, primero documente su funcionamiento. \n
- Sistema de control de versiones: Haga un commit con el estado actual. Use una rama de características (feature branch). \n
- Herramientas: Xcode con
Edit > Refactor(Rename, Extract Method), así como linters (SwiftLint) para mantener un estilo uniforme. \n
// Ejemplo de "congelación" del comportamiento para un método antiguo\nclass LegacyCalculator {\n func calculateTotal(items: [Int]) -> Int {\n // Lógica antigua y poco legible\n var result = 0\n for i in items {\n result += i * 2\n }\n return result + 10\n }\n}\n\n// Prueba que captura el comportamiento\nfunc testCalculateTotal() {\n let sut = LegacyCalculator()\n let result = sut.calculateTotal(items: [1, 2, 3])\n XCTAssertEqual(result, 22) // (1+2+3)*2 + 10 = 22\n}\n\n\n2. Estrategia "El chivo expiatorio": Variables globales y singletons
\nEl código legacy es tristemente conocido por sus estados globales y singletons. Esto hace que el código esté fuertemente acoplado y sea impredecible.
\nProblema:
\nclass NetworkManager {\n static let shared = NetworkManager()\n var baseURL: String = "http://old.api.com"\n func fetchData() { ... }\n}\n\nSolución: Inyección de dependencias (Dependency Injection). Creamos un protocolo y pasamos las dependencias a través del inicializador.
\nprotocol NetworkService {\n func fetchData()\n}\n\nclass ModernViewModel {\n private let network: NetworkService\n \n init(network: NetworkService) {\n self.network = network\n }\n \n func load() {\n network.fetchData()\n }\n}\n\nEste enfoque permite intercambiar fácilmente los servicios en las pruebas y hace que el código sea modular.
\n\n3. Lucha contra el "Controlador masivo" (Massive View Controller)
\nUn clásico del legacy en iOS: un UIViewController que se encarga de todo, desde solicitudes de red hasta el formateo de la UI. La refactorización se realiza mediante la separación de responsabilidades (SRP).
Paso 1: Extraiga la lógica de negocio en un modelo separado (ViewModel).
\n// Estaba en el controlador\nfunc buttonTapped() {\n let data = fetchData()\n let total = data.reduce(0, +)\n label.text = "Suma: \\(total)"\n}\n\n// Ahora: ViewModel\nclass MyViewModel {\n func calculateTotal(data: [Int]) -> String {\n let total = data.reduce(0, +)\n return "Suma: \\(total)"\n }\n}\n\n\nPaso 2: Extraiga la configuración de la UI en métodos o fábricas separadas.
\n- \n
- Cree una clase
ViewBuilderpara jerarquías complejas. \n - Use
UICollectionViewDiffableDataSourceen lugar del antiguodataSource. \n
4. Modernización: de Objective-C a Swift y nuevas APIs
\nA menudo, el código legacy contiene una mezcla de Objective-C y Swift. Al refactorizar, es importante actualizar las construcciones obsoletas:
\n\n- \n
- Reemplazar
NSErrorporthrows: El Swift moderno usa excepciones para el manejo de errores. \n - Eliminar
!( \n