Refactoring von Legacy-Code in Swift: Von A bis Z
\nDie Arbeit mit Legacy-Code ist ein unvermeidlicher Teil des Lebens eines iOS/macOS-Entwicklers. Oft handelt es sich um Code, der in frühen Versionen von Swift oder sogar Objective-C geschrieben wurde, ohne Tests, mit "magischen Zahlen" und riesigen UIViewController. In diesem Artikel werden wir eine Schritt-für-Schritt-Strategie zum Refactoring solchen Codes durchgehen und ihn in ein modernes, wartbares und testbares Projekt verwandeln.
1. Vorbereitung und Sicherheit: Bevor Sie den Code berühren
\nDie wichtigste Regel des Refactorings: Zerstöre nicht, was funktioniert. Ohne Tests ist jede Änderung ein Sprung ins Ungewisse.
\n- \n
- Charakterisierungstests (Characterization Tests): Schreiben Sie Tests, die das aktuelle Verhalten festhalten. Auch wenn der Code falsch erscheint, dokumentieren Sie zuerst seine Funktionsweise. \n
- Versionskontrollsystem: Erstellen Sie einen Commit mit dem aktuellen Stand. Verwenden Sie einen Feature-Branch. \n
- Werkzeuge: Xcode mit
Bearbeiten > Refactoren(Umbenennen, Methode extrahieren) sowie Linter (SwiftLint) zur Einhaltung eines einheitlichen Stils. \n
// Beispiel für das "Einfrieren" des Verhaltens einer alten Methode\nclass LegacyCalculator {\n func calculateTotal(items: [Int]) -> Int {\n // Alte, unleserliche Logik\n var result = 0\n for i in items {\n result += i * 2\n }\n return result + 10\n }\n}\n\n// Test, der das Verhalten festhält\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. Die "Prügelknabe"-Strategie: Globale Variablen und Singletons
\nLegacy-Code ist berüchtigt für seine globalen Zustände und Singletons. Dies macht den Code stark gekoppelt und unvorhersehbar.
\nProblem:
\nclass NetworkManager {\n static let shared = NetworkManager()\n var baseURL: String = "http://old.api.com"\n func fetchData() { ... }\n}\n\nLösung: Dependency Injection. Erstellen Sie ein Protokoll und übergeben Sie Abhängigkeiten über den Initialisierer.
\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\nDieser Ansatz ermöglicht es, Dienste in Tests einfach auszutauschen und macht den Code modular.
\n\n3. Kampf gegen den "Massiven View Controller"
\nKlassiker des iOS-Legacy: Ein UIViewController, der für alles zuständig ist – von Netzwerkanfragen bis zur UI-Formatierung. Das Refactoring erfolgt durch Aufteilung der Verantwortlichkeiten (SRP).
Schritt 1: Lagern Sie die Geschäftslogik in ein separates Modell (ViewModel) aus.
\n// War im Controller\nfunc buttonTapped() {\n let data = fetchData()\n let total = data.reduce(0, +)\n label.text = "Summe: \\(total)"\n}\n\n// Wurde zu: ViewModel\nclass MyViewModel {\n func calculateTotal(data: [Int]) -> String {\n let total = data.reduce(0, +)\n return "Summe: \\(total)"\n }\n}\n\n\nSchritt 2: Lagern Sie die UI-Konfiguration in separate Methoden oder Fabriken aus.
\n- \n
- Erstellen Sie eine Klasse
ViewBuilderfür komplexe Hierarchien. \n - Verwenden Sie
UICollectionViewDiffableDataSourceanstelle des altendataSource. \n
4. Modernisierung: Von Objective-C zu Swift und neuen APIs
\nOft enthält Legacy-Code eine Mischung aus Objective-C und Swift. Beim Refactoring ist es wichtig, veraltete Konstrukte zu aktualisieren:
\n\n- \n
- Ersetzen von
NSErrordurchthrows: Modernes Swift verwendet Exceptions zur Fehlerbehandlung. \n - Verzicht auf
!( \n