Рефакторинг legacy кода на Swift: продвинутые техники

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему рефакторинг 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 — это марафон, а не спринт. Рекомендуемый порядок действий:

  1. Покрытие тестами — создайте characterization tests для критических участков.
  2. Модуляризация — выделите независимые слои (сеть, хранение, логика).
  3. DI вместо синглтонов — внедряйте зависимости через инициализатор.
  4. Постепенная замена — используйте фасады и адаптеры.
  5. Миграция на async/await — убирайте делегаты и вложенные замыкания.

Помните: идеальный код — это миф. Цель рефакторинга — сделать код «достаточно хорошим» для дальнейшей разработки. Используйте Xcode инструменты (как Edit > Refactor) и не бойтесь удалять мертвый код. Удачи в очистке legacy!

Блоги

Рекомендаций по книгам