Введение: зачем рефакторить 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
- Xcode Refactor — встроенные инструменты (Rename, Extract Method).
- SwiftLint — автоматический анализ кода на соответствие стилю.
- Periphery — поиск неиспользуемого кода.
- Git