Попрощайтесь с Massive View Controller
Мы все это видели; UIViewController
, у которого на протяжении всей жизни выросли руки и ноги.
Основные обязанности UIViewController
:
- Представление данных пользователю
- Реагирование на взаимодействие с пользователем
Чего следует избегать UIViewController
:
- Выполнение бизнес-логики
- Поддержание состояния данных
- Обработка и выполнение навигации
- Наличие большого количества [частных] зависимостей
Это может выглядеть примерно так:
import Foundation import UIKit final class MainViewController: UIViewController { private let dataManager = DataManager() private let appManager = AppManager.shared var mainData: MainData { didSet { tableView.reloadData() } } private let tableView = UITableView() public override func viewDidLoad() { super.viewDidLoad() appManager.doThing { self.tableView.reloadData() } dataManager.getData { result in let sortedData = result.sorted() self.mainData = sortedData } } } extension MainViewController: UITableViewDataSource, UITableViewDelegate { ... }
- Частные/неявные зависимости
- Хранение логики и управление состоянием данных
- Выполнение собственной бизнес-логики (сортировка результата)
- Действует как источник данных
tableView
(содержит логику для удаления из очереди и настройки ячейки).
UIViewController
работает на уровне пользовательского интерфейса вашего приложения; он очень тесно взаимодействует с UIKit и другими библиотеками Apple.
Модульное тестирование любой логики, выполняемой в UIViewController, практически невозможно, потому что вывод UIViewController — это то, что отображается пользователю. Это затрудняет проверку того, что любые изменения в коде приводят к ухудшению поведения или взаимодействия с пользователем.
Чтобы проверить логику, выполняемую в UIViewController, вам может потребоваться пройти через иерархию пользовательского интерфейса.
Вы можете писать тесты пользовательского интерфейса для проверки определенных сценариев, но тесты пользовательского интерфейса:
- Требуется больше времени для запуска — более медленный цикл обратной связи во время разработки
- Включите больше шаблонного кода и кода установки (например, что произойдет, если вашему UIViewController потребуются данные из API?)
- Не способны охватить все крайние случаи в вашей логике.
Как правило, использование тестов пользовательского интерфейса для проверки бизнес-логики не рекомендуется.
Разложение
Сейчас у нашего MainViewController
много дел.
Вам не нужно заранее выбирать шаблон дизайна! Главное внимание должно быть уделено приведению нашего MainViewController
в более чистое и пригодное для тестирования состояние, чтобы можно было уверенно вносить любые дальнейшие изменения в функциональность или архитектуру. Этот процесс называется декомпозиция, и в качестве руководства используется Принцип единой ответственности (наряду с остальным SOLID).
Разложение:
- Снижает сложность и размер кода, делая его более читабельным и управляемым.
- Улучшает модульность и связность кода: каждый компонент имеет четкое и конкретное назначение и не зависит ни от чего внешнего.
- Это повышает возможность повторного использования и ремонтопригодность кода: каждый компонент можно использовать в разных контекстах и легко модифицировать или обновлять, не затрагивая другие.
- Это облегчает тестирование и отладку кода: каждый компонент можно протестировать и проверить независимо. Ошибки можно изолировать и исправлять быстрее.
Определите и внедрите свои зависимости.
Разрешите изменять свои зависимости для контроля ввода в модульных тестах.
Для начала достаточно простого инициализатора.
Мы можем использовать protocol
s для передачи имитационных/тестовых двойных объектов, которые будут действовать как интерфейс.
import Foundation import UIKit final class MainViewController: UIViewController { private let dataProvider: DataProvider private let appManager: AppManagerInterface init( dataProvider: DataProvider, appManager: AppManagerInterface ) { // Set our dependencies here ... } }
Это делает MainViewController
более поддающимся тестированию, а также делает его зависимости прозрачными.
Понять, почему важны прозрачные зависимости.
let mainViewController = MainViewController( dataProvider: DummyDataProvider(), appManager: DummyAppManager() )
Тем не менее, было бы здорово протестировать подготовку данных изолированно, в частности сортировку.
Модель представления
Давайте проверим нашу логику данных изолированно.
Наш новый класс MainListViewModel
будет отвечать за получение и сортировку данных.
protocol MainListProvider { func getMainList(completion: @escaping (MainList) -> Void) } final class MainListViewModel: MainListProvider { private let dataProvider: DataProvider init( dataProvider: DataProvider ) { ... } func getMainList(completion: @escaping (MainList) -> Void) { dataProvider.getData { result in completion(result.sorted()) } } }
и обновите наш MainViewController
import Foundation import UIKit final class MainViewController: UIViewController { private let listProvider: MainListProvider private let appManager: AppManagerInterface var list: MainList { didSet { tableView.reloadData() } } init( listProvider: MainListProvider, appManager: AppManagerInterface ) { // Set our dependencies here ... } public override func viewDidLoad() { super.viewDidLoad() appManager.doThing { self.tableView.reloadData() } listProvider.getMainList { result in self.list = result } } }
Теперь, с логикой сортировки из MainViewController
в MainListViewModel
, мы можем быстро написать отдельные модульные тесты для MainListViewModel
, чтобы проверить поведение сортировки.
Несмотря на то, что это небольшое изменение кода, оно снимает ответственность с MainViewController
и позволяет тестировать существующий функциональный модуль.
Мы можем легко отслеживать регрессии, если нам нужно ввести дополнительные функции.
Следующие шаги
Примените тот же принцип ко всем другим действиям внутри вашего MainViewController
, например:
- DataSource: отдельная утилита для сопоставления элементов из
MainList
вMainListTableViewCell
. - Переместите поведение для обработки событий табличного представления в
MainListViewModel
или создайте отдельный объект, соответствующийUITableViewDelegate
. - Вынести поведение для навигации между страницами в отдельный класс; Вы можете позаимствовать поведение
Router
из шаблона проектированияVIPER
. Подробнее о VIPER читайте здесь. - Изучите возможные шаблоны проектирования и соглашения, подходящие для вашей кодовой базы и команды.
Заключение
Мы взяли пример раздутого контроллера представления, определили, почему это проблематично, и рассмотрели способы его улучшения, чтобы сделать его более тестируемым и удобным в сопровождении.