Refactorización de código legacy en Swift: técnicas avanzadas

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

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                

Blogs

Book Recommendations