Введение: почему рефакторинг legacy кода на Swift — это искусство
Работа с устаревшим кодом на Swift — одна из самых сложных и ответственных задач iOS/macOS разработчика. Legacy проекты часто содержат «божественные объекты» (God objects), избыточные зависимости от UIKit, отсутствие тестов и хаотичную архитектуру. Продвинутый рефакторинг — это не просто переименование переменных, а системная трансформация кодовой базы с сохранением 100% функциональности. В этой статье мы разберем техники, которые помогут превратить хаос в поддерживаемую архитектуру.
1. Модуляризация: разбиваем монолит на компоненты
Первый шаг к цивилизованному коду — выделение независимых модулей. Вместо одного гигантского таргета создаем отдельные фреймворки или Swift Package Manager модули.
Определение границ модуля
Используем принцип «High Cohesion, Low Coupling». Пример: выделяем слой сетевых запросов, слой бизнес-логики и слой UI.
// До: все в одном файле ViewController
class LegacyViewController: UIViewController {
func fetchData() {
// URLSession код здесь
}
func parseData() {
// JSON парсинг здесь
}
}
// После: модуль Network
public class NetworkService {
public func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
// Чистый сетевой слой
}
}
Миграция через фасады
Не ломайте существующий код: создайте фасад, который делегирует вызовы новому модулю. Постепенно заменяйте старые вызовы.
class LegacyNetworkFacade {
private let newService = NetworkService()
@objc func oldFetchMethod() {
newService.fetchData { result in
// Адаптация под старый API
}
}
}
2. Внедрение зависимостей (DI) вместо глобальных синглтонов
Legacy код часто страдает от синглтонов: UserManager.shared, APIClient.shared. Это делает тестирование невозможным. Переходим на DI.
Ручное внедрение через инициализатор
// Проблема
class OldViewModel {
let api = APIClient.shared
let user = UserManager.shared
}
// Решение
protocol APIClientProtocol { /* ... */ }
protocol UserManagerProtocol { /* ... */ }
class NewViewModel {
private let api: APIClientProtocol
private let userManager: UserManagerProtocol
init(api: APIClientProtocol, userManager: UserManagerProtocol) {
self.api = api
self.userManager = userManager
}
}
Интеграция DI-контейнера (Swinject/Resolver)
Для больших проектов используем библиотеки. Пример с 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. Тестирование: рефакторинг под защитой тестов
Без тестов рефакторинг — это игра в русскую рулетку. Создаем «тестовый каркас» (characterization tests) для старого кода.
Characterization tests для legacy кода
Записываем текущее поведение как тесты, даже если оно неидеально.
class LegacyCalculatorTests: XCTestCase {
func testLegacyMultiply() {
// Фиксируем текущее поведение
let result = LegacyCalculator().multiply(2, 3)
XCTAssertEqual(result, 6) // Даже если логика странная
}
}
Параллельный рефакторинг с golden master
Сравниваем вывод старой и новой реализации. Используем XCTAssertEqual для проверки идентичности.
func testRefactoredMatchesLegacy() {
let input = TestData.sampleInput
let oldResult = LegacyParser().parse(input)
let newResult = NewParser().parse(input)
XCTAssertEqual(oldResult, newResult)
}
4. Миграция на современные протоколы и async/await
Legacy код часто использует делегаты и замыкания. Переход на async/await делает код читаемее.
Из делегата в async/await
// Старый подход
protocol DataLoaderDelegate: AnyObject {
func didLoad(data: Data)
func didFail(error: Error)
}
class LegacyLoader {
weak var delegate: DataLoaderDelegate?
func load() { /* ... */ }
}
// Новый подход
actor ModernLoader {
func load() async throws -> Data {
// async/await реализация
}
}
Адаптер для совместимости
class AsyncToDelegateAdapter: DataLoaderDelegate {
private let continuation: CheckedContinuation<Data, Error>
init(continuation: CheckedContinuation<Data, Error>) {
self.continuation = continuation
}
func didLoad(data: Data) {
continuation.resume(returning: data)
}
func didFail(error: Error) {
continuation.resume(throwing: error)
}
}
Заключение: пошаговый план действий
Рефакторинг legacy кода на Swift — это марафон, а не спринт. Рекомендуемый порядок действий:
- Покрытие тестами — создайте characterization tests для критических участков.
- Модуляризация — выделите независимые слои (сеть, хранение, логика).
- DI вместо синглтонов — внедряйте зависимости через инициализатор.
- Постепенная замена — используйте фасады и адаптеры.
- Миграция на async/await — убирайте делегаты и вложенные замыкания.
Помните: идеальный код — это миф. Цель рефакторинга — сделать код «достаточно хорошим» для дальнейшей разработки. Используйте Xcode инструменты (как Edit > Refactor) и не бойтесь удалять мертвый код. Удачи в очистке legacy!