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

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

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

Начать курс

Введение: зачем рефакторить legacy код на Swift?

Работа с унаследованным (legacy) кодом — неизбежная часть жизни iOS/macOS разработчика. Проекты, написанные на ранних версиях Swift (2.x, 3.x) или даже на Objective-C, часто содержат монолитные классы, дублирование логики и отсутствие модульности. Без рефакторинга такой код становится источником багов и замедляет добавление новых функций.

В этой статье мы разберем практические стратегии рефакторинга legacy кода на Swift. Вы узнаете, как безопасно улучшать архитектуру, не нарушая существующую функциональность. Все примеры применимы как для iOS, так и для macOS приложений.

1. Анализ и подготовка: с чего начать?

Прежде чем менять код, необходимо понять его текущее состояние. Без анализа вы рискуете сломать работающий функционал.

1.1. Покрытие тестами (Characterization Tests)

Первый шаг — написать характеризационные тесты. Они фиксируют текущее поведение кода, даже если оно кажется неправильным. Только после этого можно безопасно вносить изменения.

// Пример legacy функции: UserManager.swift
class UserManager {
    func fetchUser(by id: Int) -> String {
        // Устаревшая реализация с force unwrap
        let dict = ["id": id, "name": "John"] as [String : Any]
        return dict["name"] as! String
    }
}

// Характеризационный тест
import XCTest
class UserManagerTests: XCTestCase {
    func testFetchUserReturnsName() {
        let manager = UserManager()
        let result = manager.fetchUser(by: 1)
        XCTAssertEqual(result, "John")
    }
}

1.2. Инвентаризация "плохих практик"

Составьте список проблемных паттернов в коде:

  • Force unwrapping (!) — главный источник crash'ей.
  • Глобальные синглтоны — затрудняют тестирование.
  • Магические числа и строки — снижают читаемость.
  • Длинные методы (более 50 строк) — нарушают SRP.
  • Отсутствие типов (использование Any).

2. Пошаговый рефакторинг: от простого к сложному

Рефакторинг legacy кода лучше проводить маленькими шагами. Каждое изменение должно быть обратимым и сопровождаться тестами.

2.1. Избавляемся от force unwrapping

Замените ! на безопасные конструкции: guard let или if let.

// До рефакторинга
class NetworkService {
    func loadData() -> Data {
        let url = URL(string: "https://api.example.com")!
        return try! Data(contentsOf: url)
    }
}

// После рефакторинга
class NetworkService {
    enum NetworkError: Error {
        case invalidURL
        case dataLoadingFailed
    }
    
    func loadData() throws -> Data {
        guard let url = URL(string: "https://api.example.com") else {
            throw NetworkError.invalidURL
        }
        do {
            return try Data(contentsOf: url)
        } catch {
            throw NetworkError.dataLoadingFailed
        }
    }
}

2.2. Декомпозиция "божественных" классов

Legacy-классы часто нарушают принцип единственной ответственности (SRP). Разделите их на мелкие сущности.

// Проблема: класс делает всё
class ProfileViewController: UIViewController {
    func fetchProfile() { /* сетевой запрос */ }
    func parseJSON() { /* парсинг */ }
    func updateUI() { /* обновление UI */ }
    func saveToCoreData() { /* сохранение */ }
}

// Решение: разделение на модули
protocol ProfileRepository {
    func fetchProfile(completion: @escaping (Result<Profile, Error>) -> Void)
}

class ProfileNetworkRepository: ProfileRepository {
    func fetchProfile(completion: @escaping (Result<Profile, Error>) -> Void) {
        // Логика сетевого запроса
    }
}

class ProfileViewModel {
    private let repository: ProfileRepository
    
    init(repository: ProfileRepository) {
        self.repository = repository
    }
    
    func loadProfile() {
        repository.fetchProfile { [weak self] result in
            DispatchQueue.main.async {
                // Обновление UI через binding
            }
        }
    }
}

2.3. Замена синглтонов на DI (Dependency Injection)

Синглтоны делают код жестко связанным. Внедрение зависимостей (DI) улучшает тестируемость.

// До: синглтон
class Logger {
    static let shared = Logger()
    func log(_ message: String) { print(message) }
}

class DataManager {
    func process() {
        Logger.shared.log("Processing data")
    }
}

// После: DI через инициализатор
protocol Logging {
    func log(_ message: String)
}

class ConsoleLogger: Logging {
    func log(_ message: String) { print(message) }
}

class DataManager {
    private let logger: Logging
    
    init(logger: Logging) {
        self.logger = logger
    }
    
    func process() {
        logger.log("Processing data")
    }
}

// Теперь легко подменить логгер в тестах
class MockLogger: Logging {
    var lastMessage: String?
    func log(_ message: String) { lastMessage = message }
}

3. Миграция с Objective-C на современный Swift

Если ваш legacy проект содержит Objective-C файлы, миграция должна быть постепенной.

3.1. Используйте bridging header

Добавьте новые Swift-файлы в проект с Objective-C. Swift-классы будут доступны через @objc атрибуты.

// Objective-C класс: OldDataProvider.h
@interface OldDataProvider : NSObject
- (NSString *)getData;
@end

// Swift-обертка
@objc class SwiftDataProvider: NSObject {
    private let oldProvider = OldDataProvider()
    
    @objc func fetchData() -> String {
        return oldProvider.getData() ?? ""
    }
}

3.2. Постепенное переписывание

Не пытайтесь переписать всё сразу. Выберите один модуль (например, слой данных) и переведите его на Swift. Убедитесь, что тесты проходят.

4. Инструменты и best practices