Einleitung: Warum das Refactoring von Legacy-Code in Swift eine Kunst ist
Die Arbeit mit veraltetem Code in Swift ist eine der schwierigsten und verantwortungsvollsten Aufgaben für iOS/macOS-Entwickler. Legacy-Projekte enthalten oft „God Objects“, übermäßige Abhängigkeiten von UIKit, fehlende Tests und eine chaotische Architektur. Fortgeschrittenes Refactoring ist nicht nur das Umbenennen von Variablen, sondern eine systematische Transformation der Codebasis unter Beibehaltung von 100% Funktionalität. In diesem Artikel behandeln wir Techniken, die helfen, Chaos in eine wartbare Architektur zu verwandeln.
1. Modularisierung: Den Monolithen in Komponenten aufteilen
Der erste Schritt zu zivilisiertem Code ist die Ausgliederung unabhängiger Module. Anstelle eines einzigen riesigen Targets erstellen wir separate Frameworks oder Swift Package Manager Module.
Definition von Modulgrenzen
Wir verwenden das Prinzip „High Cohesion, Low Coupling“. Beispiel: Ausgliederung der Netzwerk-Anfrageschicht, der Geschäftslogikschicht und der UI-Schicht.
// Vorher: Alles in einer ViewController-Dateiclass LegacyViewController: UIViewController { func fetchData() { // URLSession-Code hier } func parseData() { // JSON-Parsing hier }}
// Nachher: Modul Networkpublic class NetworkService { public func fetchData(completion: @escaping (Result<Data, Error>) -> Void) { // Reine Netzwerkschicht }}Migration über Fassaden
Zerstören Sie keinen bestehenden Code: Erstellen Sie eine Fassade, die Aufrufe an das neue Modul delegiert. Ersetzen Sie nach und nach alte Aufrufe.
class LegacyNetworkFacade { private let newService = NetworkService() @objc func oldFetchMethod() { newService.fetchData { result in // Anpassung an die alte API } }}2. Dependency Injection (DI) anstelle globaler Singletons
Legacy-Code leidet oft unter Singletons: UserManager.shared, APIClient.shared. Das macht Tests unmöglich. Wir wechseln zu DI.
Manuelle Injektion über den Initialisierer
// Problemclass OldViewModel { let api = APIClient.shared let user = UserManager.shared}
// Lösungprotocol APIClientProtocol { /* ... */ }protocol UserManagerProtocol { /* ... */ }
class NewViewModel { private let api: APIClientProtocol private let userManager: UserManagerProtocol init(api: APIClientProtocol, userManager: UserManagerProtocol) { self.api = api self.userManager = userManager }}Integration eines DI-Containers (Swinject/Resolver)
Für große Projekte verwenden wir Bibliotheken. Beispiel mit 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. Testen: Refactoring unter dem Schutz von Tests
Ohne Tests ist Refactoring russisches Roulette. Wir erstellen ein „Testgerüst“ (Characterization Tests) für den alten Code.
Characterization Tests für Legacy-Code
Wir zeichnen das aktuelle Verhalten als Tests auf, auch wenn es nicht ideal ist.
class LegacyCalculatorTests: XCTestCase { func testLegacyMultiply() { // Aktuelles Verhalten festhalten let result = LegacyCalculator().multiply(2, 3) XCTAssertEqual(result, 6) // Auch wenn die Logik seltsam ist }}Paralleles Refactoring mit Golden Master
Vergleichen Sie die Ausgabe der alten und der neuen Implementierung. Verwenden Sie XCTAssertEqual zur Überprüfung der Identität.
func testRefactoredMatchesL