Introducción: por qué la refactorización de código legacy en Swift es un arte
Trabajar con código obsoleto en Swift es una de las tareas más complejas y responsables para un desarrollador iOS/macOS. Los proyectos legacy a menudo contienen «objetos divinos» (God objects), dependencias excesivas de UIKit, falta de pruebas y una arquitectura caótica. La refactorización avanzada no es solo renombrar variables, sino una transformación sistémica de la base de código manteniendo el 100% de la funcionalidad. En este artículo analizaremos técnicas que ayudarán a convertir el caos en una arquitectura mantenible.
1. Modularización: dividiendo el monolito en componentes
El primer paso hacia un código civilizado es la creación de módulos independientes. En lugar de un solo target gigante, creamos frameworks separados o módulos de Swift Package Manager.
Definición de los límites del módulo
Usamos el principio «Alta Cohesión, Bajo Acoplamiento». Ejemplo: separamos la capa de solicitudes de red, la capa de lógica de negocio y la capa de UI.
// Antes: todo en un mismo archivo ViewControllerclass LegacyViewController: UIViewController { func fetchData() { // Código URLSession aquí } func parseData() { // Análisis JSON aquí }}
// Después: módulo Networkpublic class NetworkService { public func fetchData(completion: @escaping (Result<Data, Error>) -> Void) { // Capa de red limpia }}Migración mediante fachadas
No rompa el código existente: cree una fachada que delegue las llamadas al nuevo módulo. Reemplace gradualmente las llamadas antiguas.
class LegacyNetworkFacade { private let newService = NetworkService() @objc func oldFetchMethod() { newService.fetchData { result in // Adaptación a la API antigua } }}2. Inyección de dependencias (DI) en lugar de singletons globales
El código legacy a menudo sufre de singletons: UserManager.shared, APIClient.shared. Esto hace imposible realizar pruebas. Pasamos a DI.
Inyección manual mediante inicializador
// Problemaclass OldViewModel { let api = APIClient.shared let user = UserManager.shared}
// Soluciónprotocol APIClientProtocol { /* ... */ }protocol UserManagerProtocol { /* ... */ }
class NewViewModel { private let api: APIClientProtocol private let userManager: UserManagerProtocol init(api: APIClientProtocol, userManager: UserManagerProtocol) { self.api = api self.userManager = userManager }}Integración de un contenedor DI (Swinject/Resolver)
Para proyectos grandes usamos bibliotecas. Ejemplo con Resolver:
import Resolver
extension Resolver: ResolverRegistering { public static func registerAllServices() { register { NetworkService() as NetworkServiceProtocol } register { UserDefaultsStorage() as StorageProtocol } }}
class RefactoredViewModel { @Injected private var network: NetworkServiceProtocol @Injected private var storage: StorageProtocol}3. Pruebas: refactorización bajo la protección de pruebas
Sin pruebas, la refactorización es jugar a la ruleta rusa. Creamos un «marco de pruebas» (characterization tests) para el código antiguo.
Characterization tests para código legacy
Registramos el comportamiento actual como pruebas, incluso si no es ideal.
class LegacyCalculatorTests: XCTestCase { func testLegacyMultiply() { // Fijamos el comportamiento actual let result = LegacyCalculator().multiply(2, 3) XCTAssertEqual(result, 6) // Incluso si la lógica es extraña }}Refactorización paralela con golden master
Comparamos la salida de la implementación antigua y la nueva. Usamos XCTAssertEqual para verificar la identidad.
func testRefactoredMatchesL