Introduction : pourquoi le réusinage de code legacy en Swift est un art
Travailler avec du code obsolète en Swift est l'une des tâches les plus complexes et les plus cruciales pour un développeur iOS/macOS. Les projets legacy contiennent souvent des « objets divins » (God objects), des dépendances excessives à UIKit, une absence de tests et une architecture chaotique. Le réusinage avancé ne consiste pas simplement à renommer des variables, mais à une transformation systématique de la base de code tout en préservant 100 % des fonctionnalités. Dans cet article, nous analyserons les techniques qui vous aideront à transformer le chaos en une architecture maintenable.
1. Modularisation : décomposer le monolithe en composants
La première étape vers un code civilisé est l'extraction de modules indépendants. Au lieu d'une seule cible gigantesque, créez des frameworks distincts ou des modules Swift Package Manager.
Définition des limites du module
Utilisez le principe « Haute cohésion, faible couplage ». Exemple : isolez la couche de requêtes réseau, la couche de logique métier et la couche d'interface utilisateur.
// Avant : tout dans un seul fichier ViewControllerclass LegacyViewController: UIViewController { func fetchData() { // Code URLSession ici } func parseData() { // Parsing JSON ici }}
// Après : module Networkpublic class NetworkService { public func fetchData(completion: @escaping (Result<Data, Error>) -> Void) { // Couche réseau propre }}Migration via des façades
Ne cassez pas le code existant : créez une façade qui délègue les appels au nouveau module. Remplacez progressivement les anciens appels.
class LegacyNetworkFacade { private let newService = NetworkService() @objc func oldFetchMethod() { newService.fetchData { result in // Adaptation à l'ancienne API } }}2. Injection de dépendances (DI) au lieu de singletons globaux
Le code legacy souffre souvent de singletons : UserManager.shared, APIClient.shared. Cela rend les tests impossibles. Passons à l'injection de dépendances.
Injection manuelle via l'initialiseur
// Problèmeclass OldViewModel { let api = APIClient.shared let user = UserManager.shared}
// Solutionprotocol APIClientProtocol { /* ... */ }protocol UserManagerProtocol { /* ... */ }
class NewViewModel { private let api: APIClientProtocol private let userManager: UserManagerProtocol init(api: APIClientProtocol, userManager: UserManagerProtocol) { self.api = api self.userManager = userManager }}Intégration d'un conteneur DI (Swinject/Resolver)
Pour les grands projets, utilisez des bibliothèques. Exemple avec 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. Tests : réusinage sous la protection des tests
Sans tests, le réusinage est une roulette russe. Créez une « ossature de test » (characterization tests) pour l'ancien code.
Tests de caractérisation pour le code legacy
Enregistrez le comportement actuel sous forme de tests, même s'il n'est pas idéal.
class LegacyCalculatorTests: XCTestCase { func testLegacyMultiply() { // Enregistrer le comportement actuel let result = LegacyCalculator().multiply(2, 3) XCTAssertEqual(result, 6) // Même si la logique est étrange }}Réusinage parallèle avec golden master
Comparez la sortie de l'ancienne et de la nouvelle implémentation. Utilisez XCTAssertEqual pour vérifier l'identité.
func testRefactoredMatchesL