Réusinage de code legacy en Swift : de A à Z
\nTravailler avec du code obsolète (legacy code) est une partie inévitable de la vie du développeur iOS/macOS. Il s'agit souvent de code écrit dans les premières versions de Swift ou même en Objective-C, dépourvu de tests, avec des "nombres magiques" et des UIViewController gigantesques. Dans cet article, nous allons détailler une stratégie étape par étape pour réusiner ce code, le transformant en un projet moderne, maintenable et testable.
1. Préparation et sécurité : avant de toucher au code
\nLa règle la plus importante du réusinage : ne casse pas ce qui fonctionne. Sans tests, tout changement est un saut dans l'inconnu.
\n- \n
- Tests de caractérisation (Characterization Tests) : Écrivez des tests qui figent le comportement actuel. Même si le code semble incorrect, documentez d'abord son fonctionnement. \n
- Système de contrôle de version : Faites un commit avec l'état actuel. Utilisez une branche de fonctionnalité (feature branch). \n
- Outils : Xcode avec
Edit > Refactor(Renommer, Extraire une méthode), ainsi que des linters (SwiftLint) pour respecter un style uniforme. \n
// Exemple de "gel" du comportement pour une ancienne méthode\nclass LegacyCalculator {\n func calculateTotal(items: [Int]) -> Int {\n // Ancienne logique illisible\n var result = 0\n for i in items {\n result += i * 2\n }\n return result + 10\n }\n}\n\n// Test qui fige le comportement\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. Stratégie "Homme de paille" : Variables globales et singletons
\nLe code legacy est tristement célèbre pour ses états globaux et ses singletons. Cela rend le code fortement couplé et imprévisible.
\nProblème :
\nclass NetworkManager {\n static let shared = NetworkManager()\n var baseURL: String = "http://old.api.com"\n func fetchData() { ... }\n}\n\nSolution : Injection de dépendances (Dependency Injection). Créez un protocole et transmettez les dépendances via l'initialiseur.
\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\nCette approche permet de remplacer facilement les services dans les tests et rend le code modulaire.
\n\n3. Lutte contre le "Contrôleur massif" (Massive View Controller)
\nUn classique du legacy iOS : un UIViewController qui gère tout, des requêtes réseau au formatage de l'interface utilisateur. Le réusinage passe par la séparation des responsabilités (SRP).
Étape 1 : Extrayez la logique métier dans un modèle séparé (ViewModel).
\n// Avant, dans le contrôleur\nfunc buttonTapped() {\n let data = fetchData()\n let total = data.reduce(0, +)\n label.text = "Somme : \\(total)"\n}\n\n// Après : ViewModel\nclass MyViewModel {\n func calculateTotal(data: [Int]) -> String {\n let total = data.reduce(0, +)\n return "Somme : \\(total)"\n }\n}\n\n\nÉtape 2 : Externalisez la configuration de l'interface utilisateur dans des méthodes ou des fabriques séparées.
\n- \n
- Créez une classe
ViewBuilderpour les hiérarchies complexes. \n - Utilisez
UICollectionViewDiffableDataSourceà la place de l'anciendataSource. \n
4. Modernisation : d'Objective-C à Swift et aux nouvelles API
\nLe code legacy contient souvent un mélange d'Objective-C et de Swift. Lors du réusinage, il est important de mettre à jour les constructions obsolètes :
\n\n- \n
- Remplacement de
NSErrorparthrows: Le Swift moderne utilise les exceptions pour la gestion des erreurs. \n - Abandon de
!( \n