Introdução: por que refatorar código legado em Swift é uma arte
Trabalhar com código legado em Swift é uma das tarefas mais complexas e importantes para desenvolvedores iOS/macOS. Projetos legados frequentemente contêm “objetos divinos” (God objects), dependências excessivas do UIKit, ausência de testes e arquitetura caótica. A refatoração avançada não é apenas renomear variáveis, mas uma transformação sistêmica da base de código, mantendo 100% da funcionalidade. Neste artigo, exploraremos técnicas que ajudam a transformar o caos em uma arquitetura sustentável.
1. Modularização: dividindo o monolito em componentes
O primeiro passo para um código organizado é criar módulos independentes. Em vez de um único target gigante, criamos frameworks separados ou módulos do Swift Package Manager.
Definição dos limites do módulo
Usamos o princípio “Alta Coesão, Baixo Acoplamento”. Exemplo: separamos a camada de rede, a camada de lógica de negócios e a camada de UI.
// Antes: tudo em um único arquivo ViewControllerclass LegacyViewController: UIViewController { func fetchData() { // Código URLSession aqui } func parseData() { // Parsing JSON aqui }}
// Depois: módulo Networkpublic class NetworkService { public func fetchData(completion: @escaping (Result<Data, Error>) -> Void) { // Camada de rede limpa }}Migração via fachadas
Não quebre o código existente: crie uma fachada que delega chamadas ao novo módulo. Substitua gradualmente as chamadas antigas.
class LegacyNetworkFacade { private let newService = NetworkService() @objc func oldFetchMethod() { newService.fetchData { result in // Adaptação para a API antiga } }}2. Injeção de Dependências (DI) em vez de singletons globais
Código legado frequentemente sofre com singletons: UserManager.shared, APIClient.shared. Isso torna os testes impossíveis. Migramos para DI.
Injeção manual via inicializador
// Problemaclass OldViewModel { let api = APIClient.shared let user = UserManager.shared}
// Soluçãoprotocol APIClientProtocol { /* ... */ }protocol UserManagerProtocol { /* ... */ }
class NewViewModel { private let api: APIClientProtocol private let userManager: UserManagerProtocol init(api: APIClientProtocol, userManager: UserManagerProtocol) { self.api = api self.userManager = userManager }}Integração de contêiner DI (Swinject/Resolver)
Para projetos grandes, usamos bibliotecas. Exemplo com 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. Testes: refatoração protegida por testes
Sem testes, refatoração é roleta-russa. Criamos um “arcabouço de testes” (characterization tests) para o código legado.
Testes de caracterização para código legado
Registramos o comportamento atual como testes, mesmo que não seja ideal.
class LegacyCalculatorTests: XCTestCase { func testLegacyMultiply() { // Fixamos o comportamento atual let result = LegacyCalculator().multiply(2, 3) XCTAssertEqual(result, 6) // Mesmo que a lógica seja estranha }}Refatoração paralela com golden master
Comparamos a saída da implementação antiga com a nova. Usamos XCTAssertEqual para verificar identidade.
func testRefactoredMatchesL