Рефакторинг 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), и ваш проект станет не только поддерживаемым, но и приятным для разработки.