Рефакторинг legacy кода на Swift: от А до Я

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

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

Начать курс

Введение: почему рефакторинг legacy кода на Swift — это необходимость

Каждый iOS/macOS разработчик рано или поздно сталкивается с legacy кодом. Это может быть проект, написанный на Objective-C и частично переведенный на Swift, или приложение, созданное в эпоху Swift 2.0 с использованием Massive View Controller. Такой код часто работает, но его поддержка превращается в ад: любое изменение требует часов тестирования, а добавление новой фичи ломает старую.

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

1. Диагностика: как понять, что legacy код требует рефакторинга

Прежде чем менять код, нужно понять, что именно требует изменений. Основные признаки legacy кода на Swift:

  • Massive View Controller (MVC-антипаттерн) — файлы с 500+ строками, где логика, UI и сетевые запросы смешаны в одном классе.
  • Сильная связанность (tight coupling) — классы напрямую зависят друг от друга, нет протоколов и dependency injection.
  • Отсутствие тестов — если вы боитесь изменить строчку кода, потому что не знаете, что сломается.
  • Глобальные переменные и синглтоны — использование UserDefaults.standard или NotificationCenter.default по всему проекту.
  • Force unwrapping — повсеместное использование ! вместо безопасного извлечения опционалов.

1.1. Инструменты для анализа legacy кода на Swift

Для объективной оценки состояния кода используйте инструменты:

  • SwiftLint — статический анализатор, который выявляет нарушения code style.
  • Xcode Instruments — для поиска утечек памяти и проблем производительности.
  • Code coverage — покажет, сколько кода покрыто тестами (обычно 0-10% в legacy проектах).

Пример типичного legacy кода, который требует рефакторинга:

class LegacyViewController: UIViewController {
    
    @IBOutlet weak var label: UILabel!
    var data: [String] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Смесь логики и UI
        let url = URL(string: "https://api.example.com/data")!
        let data = try! Data(contentsOf: url)
        let json = try! JSONSerialization.jsonObject(with: data) as! [[String: Any]]
        for item in json {
            data.append(item["name"] as! String)
        }
        label.text = data.first
    }
}

Здесь сразу несколько проблем: force unwrapping, синхронный сетевой запрос в главном потоке, смесь логики и представления.

2. Стратегия рефакторинга legacy кода на Swift: пошаговый план

Рефакторинг без стратегии — путь к катастрофе. Используйте подход "боевого сапера": сначала обезопасьте территорию, потом меняйте код.

2.1. Шаг 1: Создайте "защитную сетку" из тестов

Прежде чем что-то менять, напишите тесты для существующего кода. Используйте Characterization Tests — тесты, которые фиксируют текущее поведение системы. Даже если код выглядит ужасно, тесты покажут, что вы не сломали логику.

import XCTest
@testable import LegacyApp

class LegacyViewControllerTests: XCTestCase {
    
    func testViewDidLoad_SetsLabelText() {
        let sut = LegacyViewController()
        sut.loadViewIfNeeded()
        // Проверяем, что label не nil и содержит текст
        XCTAssertNotNil(sut.label.text)
    }
}

Этот тест может быть не идеальным, но он зафиксирует текущее состояние. После рефакторинга тест должен проходить.

2.2. Шаг 2: Разделите ответственность (Single Responsibility)

Выделите из ViewController отдельные сервисы: сетевой слой, парсинг данных, бизнес-логику. Используйте протоколы для слабой связанности.

// Протокол для сетевого слоя
protocol NetworkServiceProtocol {
    func fetchData(completion: @escaping (Result<[[String: Any]], Error>) -> Void)
}

// Реализация
class NetworkService: NetworkServiceProtocol {
    func fetchData(completion: @escaping (Result<[[String: Any]], Error>) -> Void) {
        let url = URL(string: "https://api.example.com/data")!
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            guard let data = data,
                  let json = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
                completion(.failure(NSError(domain: "ParseError", code: 0)))
                return
            }
            completion(.success(json))
        }.resume()
    }
}

// Теперь ViewController использует протокол
class RefactoredViewController: UIViewController {
    
    @IBOutlet weak var label: UILabel!
    private let networkService: NetworkServiceProtocol
    
    init(networkService: NetworkServiceProtocol = NetworkService()) {
        self.networkService = networkService
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        networkService.fetchData { [weak self] result in
            DispatchQueue.main.async {
                switch result {
                case .success(let items):
                    self?.label.text = items.first?["name"] as? String ?? "No data"
                case .failure(let error):
                    self?.label.text = "Error: \(error.localizedDescription)"
                }
            }
        }
    }
}

2.3. Шаг 3: Замените синглтоны на Dependency Injection

Синглтоны — зло legacy кода. Они создают глобальное состояние и делают тестирование невозможным. Внедрите DI через инициализатор или фабрику.

// До рефакторинга
class AnalyticsManager {
    static let shared = AnalyticsManager()
    func logEvent(_ name: String) { /* ... */ }
}

// После рефакторинга
protocol AnalyticsService {
    func logEvent(_ name: String)
}

class RealAnalyticsService: AnalyticsService {
    func logEvent(_ name: String) { /* реальная реализация */ }
}

class MockAnalyticsService: AnalyticsService {
    var loggedEvents: [String] = []
    func logEvent(_ name: String) { loggedEvents.append(name                

Блоги

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