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

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

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

Начать курс

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

Работа с устаревшим кодом (legacy code) — неизбежная часть жизни iOS/macOS разработчика. Часто это код, написанный на ранних версиях Swift или даже Objective-C, лишенный тестов, с "магическими числами" и гигантскими UIViewController. В этой статье мы разберем пошаговую стратегию рефакторинга такого кода, превратив его в современный, поддерживаемый и тестируемый проект.

1. Подготовка и безопасность: прежде чем трогать код

Самое главное правило рефакторинга: не сломай то, что работает. Без тестов любое изменение — это прыжок в неизвестность.

  • Окружение тестами (Characterization Tests): Напишите тесты, которые фиксируют текущее поведение. Даже если код кажется неправильным, сначала задокументируйте его работу.
  • Система контроля версий: Сделайте коммит с текущим состоянием. Используйте feature branch.
  • Инструменты: Xcode с Edit > Refactor (Rename, Extract Method), а также линтеры (SwiftLint) для соблюдения единого стиля.
// Пример "заморозки" поведения для старого метода
class LegacyCalculator {
    func calculateTotal(items: [Int]) -> Int {
        // Старая, нечитаемая логика
        var result = 0
        for i in items {
            result += i * 2
        }
        return result + 10
    }
}

// Тест, фиксирующий поведение
func testCalculateTotal() {
    let sut = LegacyCalculator()
    let result = sut.calculateTotal(items: [1, 2, 3])
    XCTAssertEqual(result, 22) // (1+2+3)*2 + 10 = 22
}

2. Стратегия "Мальчик для битья": Глобальные переменные и синглтоны

Legacy код печально известен своими глобальными состояниями и синглтонами. Это делает код жестко связанным и непредсказуемым.

Проблема:

class NetworkManager {
    static let shared = NetworkManager()
    var baseURL: String = "http://old.api.com"
    func fetchData() { ... }
}

Решение: Внедрение зависимостей (Dependency Injection). Создаем протокол и передаем зависимости через инициализатор.

protocol NetworkService {
    func fetchData()
}

class ModernViewModel {
    private let network: NetworkService
    
    init(network: NetworkService) {
        self.network = network
    }
    
    func load() {
        network.fetchData()
    }
}

Такой подход позволяет легко подменять сервисы в тестах и делает код модульным.

3. Борьба с "Толстым контроллером" (Massive View Controller)

Классика iOS-легаси: UIViewController, который отвечает за всё — от сетевых запросов до форматирования UI. Рефакторинг идет через разделение ответственности (SRP).

Шаг 1: Выделите бизнес-логику в отдельную модель (ViewModel).

// Было в контроллере
func buttonTapped() {
    let data = fetchData()
    let total = data.reduce(0, +)
    label.text = "Сумма: \(total)"
}

// Стало: ViewModel
class MyViewModel {
    func calculateTotal(data: [Int]) -> String {
        let total = data.reduce(0, +)
        return "Сумма: \(total)"
    }
}

Шаг 2: Вынесите настройку UI в отдельные методы или фабрики.

  • Создайте класс ViewBuilder для сложных иерархий.
  • Используйте UICollectionViewDiffableDataSource вместо старого dataSource.

4. Модернизация: от Objective-C к Swift и новым API

Часто legacy-код содержит смесь Objective-C и Swift. При рефакторинге важно обновить устаревшие конструкции:

  • Замена NSError на throws: Современный Swift использует исключения для обработки ошибок.
  • Отказ от ! (Implicitly Unwrapped Optionals): Используйте guard let или if let.
  • Property Wrappers: Для UserDefaults или Codable.
// Старый стиль
var name: String!

func setName(_ newName: String) {
    name = newName
}

// Новый стиль
var name: String = ""

func setName(_ newName: String) {
    guard !newName.isEmpty else { return }
    name = newName
}

5. Тестирование рефакторинга: как не откатиться назад

Финальный этап — убедиться, что ваш рефакторинг прошел успешно.

  • Unit-тесты: Покрываем новую логику.
  • Snapshot-тесты: Для проверки UI (библиотека iOSSnapshotTestCase).
  • Code Review: Второй разработчик должен проверить, что код не потерял функциональность.
// Пример теста для отрефакторенного кода
func testViewModelCalculatesCorrectly() {
    let viewModel = MyViewModel()
    let result = viewModel.calculateTotal(data: [5, 10])
    XCTAssertEqual(result, "Сумма: 15")
}

Заключение

Рефакторинг legacy кода на Swift — это не хаотичная переписка, а методичный процесс. Начинайте с малого: напишите тесты, изолируйте зависимости, разбейте "монолиты" на модули. Помните, что цель — не переписать всё за один день, а сделать код чище и безопаснее для будущих изменений. Используйте современные возможности Swift (Protocols, Generics, Result Builders), и ваш проект станет не только поддерживаемым, но и приятным для разработки.

Блоги

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