Посадить свои контроллеры представлений на диету с помощью архитектуры Clean Swift (VIP), часть 1
Если вы только начинаете разработку для iOS (или даже имеете приличный опыт), вы можете столкнуться с ситуациями, когда ваши контроллеры представления становятся довольно большими. Возможно, вы обнаружите, что все настройки представления, представление других контроллеров представления, получение текущего местоположения пользователя и вызовы API определяются и выполняются в контроллере представления.
Мало того, что контроллеры представления становятся непослушными для обновления, если вас попросили написать модульные тесты или тесты пользовательского интерфейса, вы можете почесать голову, пытаясь понять, как в мире поменять местами вызовы API и геолокации, чтобы использовать имитации
вместо настоящего. Это может быть особенно сложно для тех, кто только начинает тестировать свой код, и из этого примера видно, что нужно что-то сделать, чтобы сделать код чище и проще для тестирования.
Как вы, наверное, знаете, приложения для iOS создаются с использованием Модель-представление-контроллер (MVC) архитектура. Если ваши представления представляют собой раскадровки или программный код пользовательского интерфейса, представляемый пользователю, а модели предназначены для бизнес-логики, кажется, что единственное оставшееся место
поставить «все остальное» будет контроллер (или контроллер представления в случае iOS). В сообществе iOS есть шутка, что на самом деле MVC означает Массивный контроллер просмотратак что вы можете видеть, что это происходит естественным образом, когда вы повышаете свои навыки разработки на этой мобильной платформе.
Это не твоя вина
Прежде чем углубиться в один из способов смягчения этой ситуации, я хотел бы поговорить о как а также Почему Бывает. Итак, существует бесчисленное количество причин, которые приводят к этому антипаттерну, но я хотел бы выделить 3, чтобы кратко остановиться на них: определение, образованиеа также опыт.
Определение
А контроллер представления, или, проще говоря, контроллер, можно описать в самых основных терминах как «клей» между логикой представления и логикой бизнес-домена. Более формальные определения могут быть гораздо более конкретными и подробными, но если вы действительно не углубитесь в тему, объяснение того, что делает контроллер представления, на самом деле довольно расплывчато. Если мы подумаем об этом на высоком уровне, возможно, вы
можно сказать, что он соответствует принцип единой ответственности. Однако, копнув немного глубже, мы видим, что современное использование контроллера представления в iOS на самом деле НАМНОГО сложнее. Всего несколько примеров:
- Он не только отвечает за представление пользовательского интерфейса, но также должен принимать и обрабатывать вводимые пользователем данные.
- Настройка представления может включать в себя создание подпредставлений и способов их взаимодействия, настройку ограничений в соответствии с размером экрана, передачу любого фактического содержимого в эти представления из бизнес-логики и обновление этих представлений на основе событий.
- Любые протоколы, которым должен соответствовать контроллер представления для служб или функций.
- Представление или закрытие других контроллеров представления.
- Прямой доступ к своему родителю
UINavigationController
,UITabBarController
или какой-либо другой контроллер представления контейнера.
Эти примеры также не являются обычными, редкими вариантами использования приложения. Наоборот, вы можете найти каждый из этих примеров даже в самых тривиальных проектах.
Образование
Изучение разработки для iOS (включая Swift и/или Objective-C) может оказаться непростой задачей. Из-за этого часто учебные пособия для начинающих и даже собственная документация и примеры кода Apple используют «ярлыки» для краткости, простоты объяснения или сохранения разумной длины учебного пособия / видео. Много раз эти письменные или записанные ссылки будут сопровождаться отказом от ответственности за то, что подход, который они используют, может не использовать лучшие практики, но это, к сожалению, может привести к тому, что разработчики составят вещи далеко не идеальным образом, особенно если он или она новичок. к программированию тоже.
Что еще хуже, эти проблемы могут стать все более и более серьезными «болевыми точками» по мере роста приложения, и обычно в этот момент устранение технического долга требует большого и трудоемкого рефакторинга. Я не возражаю против того, чтобы учебные материалы и документация были как можно более простыми, чтобы не вызывать путаницы у читателя и «перейти к сути», помогая решить
проблема, однако это может привести к нежелательному и непреднамеренному побочному эффекту.
Опыт
Развивая раздел выше, даже старшие инженеры постоянно гуглят информацию на ежедневной основе. Однако то, что вы делаете с этой информацией, больше соответствует намерению, которое я пытаюсь передать, говоря о опыт а не количество лет, в течение которых кто-то занимается программированием. Это различение заключается даже не в том, что один результат поиска является «неправильным», а другой — «правильным», а в большей степени:
- Знание того, какое решение лучше всего сработает в одной ситуации по сравнению с другой.
- Понимание концепции решения и способность применять лучшие практики или проводить рефакторинг в соответствии с потребностями.
- Отточенные навыки «гугл-фу» в том, как структурировать поисковый запрос, чтобы сузить поиск до конкретного варианта использования.
Даже у опытных разработчиков, которые изучают новый язык или фреймворк, поначалу могут возникнуть проблемы, если он или она не знает «жаргон» или терминологию. При таком понимании легко увидеть, что даже с «опытом различения», если вы не знакомы с предметом исследования, у вас нет оснований судить о том, что лучше для конкретной ситуации.
Вернемся к нашим регулярным программам….
Теперь, когда мы подробно рассмотрели проблему, теперь возникает вопрос: Как решить эту проблему? Как намекает заголовок этого поста, мы углубимся в один конкретный шаблон проектирования который недавно приобрел популярность в сфере разработки iOS. Однако было бы упущением не упомянуть сначала о десятки также из других вариантов. С точки зрения архитектуры программного обеспечения принимаемые решения в значительной степени субъективны в зависимости от ситуации, и даже стандартные шаблоны проектирования или фреймворки часто можно комбинировать с другими для получения лучшего результата.
При этом два варианта, которые вы, вероятно, должны использовать в первую очередь, — это классические шаблоны проектирования, первоначально представленные в книге. Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспеченияи ТВЕРДЫЙ принципы. Использование их в качестве руководств может значительно улучшить ваш опыт объектно-ориентированного программирования, охватывающего несколько языков и сред. Кроме того, многие другие парадигмы программирования возникли как популярные альтернативы классической структуре MVC, которая существует в приложениях для iOS. К ним относятся (но определенно не ограничиваются ими):
Чистый Свифт
Чистый Свифт Архитектура, или View-Interactor-Presenter (VIP), является одним из таких шаблонов проектирования, который возник из пепла сломанной реализации MVC экосистемы iOS. Это освобождает контроллер представления, чтобы сделать это: контролировать вид. Как упоминалось выше, это включает в себя две основные обязанности:
- Реагирование на события (сторонние, взаимодействие с пользователем, просмотр крючков жизненного цикла)
- Управляйте тем, что отображается на экране (начальная настройка вида и последующие обновления этого вида)
Проблема массивный контроллер просмотра закрадывается, потому что обычно проще включить различные зависимости контроллера непосредственно в сам класс. Допустим, я хочу инициировать запрос API, когда viewDidAppear(...)
метод называется. Я знаю, что мне нужно предоставить отдельный класс или интерфейс для API, но тогда я просто включаю интерфейс API непосредственно в контроллер представления.
init(myApiClient: ApiClient = ApiClient()) { ... }
viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myApiClient.getList() { ... }
}
Этот пример прост, но необходимо сделать два важных замечания:
Мы раскрываем детали реализации контроллеру представления, о которых ему не нужно знать. В этом случае я не так уж много могу сделать для рефакторинга, но если бы запрос API имел более сложную настройку, он продолжал бы раскрывать все больше и больше деталей реализации, о которых контроллеру представления не нужно знать.
Включение клиента API в контроллер представления — это просто еще одна зависимость в потенциально большом списке зависимостей, подключаемых к тому же классу. Даже если зависимости
включенных посредством необязательных свойств, тестирование становится все более и более сложным, поскольку каждая зависимость должна быть либо включена таким образом, чтобы не конфликтовать с другими зависимостями, либо должна быть имитирована или заглушена.
Было бы намного чище, если бы каждое событие, которое должен обрабатывать контроллер представления, могло быть передано в выход для обработки, и каждый раз, когда представление необходимо обновить, может обрабатываться одним вход. По сути, это то, что делает комбинация View-Interactor-Presenter. События передаются от контроллера представления к интерактору, презентер получает результат проделанной работы и сообщает контроллеру представления, что и как следует обновить в его представлении.
Контроллер представления выход интерактор, интерактор выход ведущий, и, наконец, ведущий выход снова является контроллером представления. Точно так же вход является контроллером представления, ведущего вход интерактор, а контроллер представления вход является ведущим.
Этот цикл связанного контроллера-интерактора-презентатора часто называют сценаи каждый класс в сцене связан со своим вход а также выход через протокол. Имейте в виду, что в приведенном ниже примере я назвал протокол OrdersViewControllerOutput
и переменная, хранящая интерактор в контроллере представления output
. Однако не стесняйтесь называть их так, как считаете нужным, чтобы избежать путаницы. Вы можете назвать их, например, OrdersBusinessLogic
а также interactor
если бы это было понятнее.
ЗаказыInteractor.swift
protocol OrderListViewControllerOutput {
func getOrderList()
}
class OrderListInteractor: OrderListViewControllerOutput {
...
getOrderList() {
...
}
OrdersViewController.swift
class OrderListViewController: ViewController {
var output: OrdersViewControllerOutput
init(output: OrdersViewControllerOutput = OrdersInteractor()) {
...
self.output = output
}
viewDidAppear(_ animated: Bool) {
...
output.getOrderList()
}
В этом рефакторинге я хотел бы отметить два важных момента:
Больше нет необходимости передавать и управлять отдельными зависимостями от контроллера представления, поскольку интерактор будет обрабатывать события и действия, поступающие от контроллера представления. На самом деле вам даже не нужно передавать зависимости в интерактор. Вместо этого можно также извлечь каждую единицу «работы» вместе с ее зависимостями от рабочих классов, которые интерактор может вызывать индивидуально, но это будет обсуждаться во второй части этого поста.
Наличие интерактора, соответствующего протоколу, значительно упрощает его создание для тестирования. Класс фиктивного интерактора может быть передан в контроллер представления, и проверка того, что каждый метод интерактора вызывается контроллером представления, становится тривиальной.
OrderListInteractorMock.swift
class OrderListInteractorMock: OrderListViewControllerOutput {
var getOrdersListCalled = false
var getSomeOtherFunctionCalled = false
func getOrdersList() {
getOrdersListCalled = true
}
func someOtherFunction() {
getSomeOtherFunctionCalled = true
}
}
Передача данных в VIP-цикле
Нам еще многое предстоит осветить в следующих статьях, но последнее, о чем я хотел бы рассказать в части 1, — это то, как данные передаются между различными методами в цикле VIP. Вместо того, чтобы передавать каждый аргумент в метод, мы можем построить модели для нашего сцена. Эти модели просто базовые структуры с целью «упаковки» аргументов в единую структуру данных. Контроллер представления передает запрос интерактору, интерактор передает отклик ведущему, и, наконец, ведущий передает просмотреть модель вернуться к контроллеру представления.
В коде модели выглядят примерно так:
class OrderList {
struct Request {
var start: Int
var end: Int
var count: Int
}
struct Response {
...
}
struct ViewModel {
...
}
}
Сейчас OrderListViewControllerOutput
может обновить сигнатуру своего метода, чтобы принять OrderList.Request
структура.
protocol OrdersViewControllerOutput {
func getOrderList(_ request: OrderList.Request)
}
Передача данных таким образом дает два ключевых преимущества:
Если требуется дополнительный аргумент, вам нужно только обновить
OrderList
модели вместо сигнатуры методов и определений протоколов.Необходимость имитировать простую структуру данных по сравнению с объектами с несколькими аргументами намного проще при написании тестов.
Продолжение следует…
Как я упоминал выше, в следующих статьях мы также будем изучать презентаторов, рабочих, маршрутизаторы, модульное тестирование и многое другое. Я надеюсь, что знакомство с Clean Swift было интригующей и захватывающей темой, и я с нетерпением жду возможности расширить ее в своих следующих нескольких постах. Ознакомьтесь со второй частью и, как всегда, удачного кодирования!