- [Из песочницы] Clean swift архитектура как альтернатива VIPER
- Введение
- View (View Controller)
- Interactor
- Presenter
- Worker
- Router
- Models
- Пример реализации
- Почему Clean Swift
- Clean Swift (VIP) iOS Architecture Pattern
- Quick overview
- Example
- View Controller
- Interactor
- Worker
- Presenter
- Router
- Configurator
- Model
- Full schema
- Other
- Key Rules
- Recommendations
- Strengths
- Disadvantages and traps
[Из песочницы] Clean swift архитектура как альтернатива VIPER
Введение
На данный момент существует множество статей про VIPER — clean архитектуру, различные вариации которой в свое время стали популярны для iOS проектов. Если вы не знакомы с Viper, можете прочитать тут, тут или тут.
Я бы хотел поговорить об альтернативе VIPER — Clean Swift. Сlean Swift на первый взгляд похож на VIPER, однако отличия становятся видны после изучения принципа взаимодействия модулей. В VIPER основу взаимодействия составляет Presenter, он передает запросы пользователя Interactor’у для обработки и форматирует полученные от него назад данные для отображения на View Controller:
В Clean Swift основными модулями так же, как и в VIPER, являются View Controller, Interactor, Presenter.
Взаимодействие между ними происходит циклично. Передача данных основана на протоколах (опять же, аналогично VIPER), что позволяет при будущем изменении какого-то из компонентов системы просто подменить его на другой. Процесс взаимодействия в общем виде выглядит так: пользователь нажимает на кнопку, View Controller создает объект с описанием и отправляет его в Interactor. Interactor в свою очередь осуществляет какой-то конкретный сценарий в соответствии с бизнес логикой, создает объект результата и передает его Presenter. Presenter формирует объект с отформатированными для отображения пользователю данными и отправляет его во View Controller. Давайте подробнее рассмотрим каждый модуль Clean Swift подробнее.
View (View Controller)
View Controller, как и в VIPER, выполняет все конфигурации VIew, будь то цвет, настройки шрифта UILabel или Layout. Поэтому каждый UIViewController в данной архитектуре реализует Input протокол для отображения данных или реакции на действия пользователя.
Interactor
Interactor содержит в себе всю бизнес логику. Он принимает действия пользователя от контроллера, с параметрами (например измененный текст поля ввода, нажатие той или иной кнопки) определёнными в Input протоколе. После отработки логики, Interactor при необходимости должен передать данные для их подготовки в Presenter перед отображением в ViewController. Однако Interactor принимает на вход запросы только от View, в отличие от VIPER, где эти запросы проходят Presenter.
Presenter
Presenter обрабатывает данные для показа пользователю. Результат в данном случае — это Input протокол ViewController’a, здесь можно например поменять формат текста, перевести значение цвета из enum в rgb и т.п.
Worker
Чтобы излишне не усложнять Interactor и не дублировать детали бизнес логики, можно использовать дополнительный элемент Worker. В простых модулях он не всегда нужен, но в достаточно нагруженных позволяет снять с Interactor часть задач. Например, в worker может быть вынесена логика взаимодействия с базой данных, особенно, если одни и те же запросы к базе могут использоваться в разных модулях.
Router
Router ответственен за передачу данных другим модулям и переходы между ними. У него есть ссылка на контроллер, потому что в iOS, к сожалению, контроллеры помимо всего прочего исторически ответственны за переходы. При использовании segue можно упростить инициализацию переходов благодаря вызову методов Router из Prepare for segue, потому что Router знает, как передать данные, и сделает это без лишнего кода цикла со стороны Interactor/Presenter. Данные передаются используя протоколы хранилищ данных каждого модуля, реализуемых в Interactor. Эти протоколы также ограничивает возможность доступа к внутренним данным модуля из Router.
Models
Models — это описание структур данных для передачи данных между модулями. Каждая реализация функции бизнес логики имеет свое описание моделей.
- Request — для передачи запроса из контроллера в интерактор.
- Response — ответ интерактора для передачи презентеру с данными.
- ViewModel — для передачи данных в готовом для отображения в контроллере виде.
Пример реализации
Рассмотрим подробнее данную архитектуру на простом примере. Им послужит приложение ContactsBook в упрощенном, но вполне достаточном для понимания сути архитектуры виде. Приложение включает себя список контактов, а также добавление и редактирование контактов.
Пример input протокола:
Каждый контроллер содержит ссылку на объект, реализующий протокол input Interactor
а также на объект Router, который должен реализовывать логику передачи данных и переключения модулей:
Можно реализовать конфигурирование модуля в отдельном приватном методе:
либо создать синглтон Configurator, чтобы вынести этот код из контроллера (для тех кто считает, что контроллер не должен участвовать в конфигурации) и не соблазнять себя доступом до частей модуля в контроллере. Класса конфигуратора нет в представлении дяди Боба и в классическом VIPER. Использование конфигуратора для модуля добавления контакта выглядит так:
Код конфигуратора содержит в себе единственный метод конфигурации, абсолютно идентичный setup методу в контроллере:
Еще одним очень важным моментом в реализации контроллера является код в стандартном методе prepare for segue:
Внимательный читатель скорее всего заметил, что Router также обязан реализовывать NSObjectProtocol. Делается это для того, чтобы мы могли воспользоваться стандартными методами этого протокола для роутинга при использовании segues. Для поддержки такой простой переадресации именование segue identifier должно соответствовать окончаниям названий методов Router. Например, для перехода к просмотру контакта есть segue, который завязан на выбор ячейки с контактом. Его identifier — “ViewContact”, вот соответствующий метод в Router:
Запрос на отображение данных к Interactor тоже выглядит очень просто:
Перейдем к Interactor. Interactor реализует протокол ContactListDataStore, ответственный за хранение/доступ к данным. В нашем случае это просто массив контактов, ограниченный только getter-методом, чтобы показать роутеру недопустимость его изменения из других модулей. Протокол, реализующий бизнес логику для нашего списка, выглядит следующим образом:
Он получает из ContactListWorker данные контактов. За то, каким образом загружаются данные, в этом случае несет ответственность Worker. Он может обратиться к сторонним сервисам, которые решают к примеру, взять данные из кеша или загрузить из сети. После получения данных Interactor отдает ответ (Response) в Presenter для подготовки к отображению, для этого Interactor содержит ссылку на Presenter:
Presenter реализует только один протокол — ContactListPresentationLogic, в нашем случае он просто принудительно меняет регистр имени и фамилии контакта, формирует из модели данных модель представления DisplayedContact и передает это в Controller на отображение:
После чего цикл завершается и контроллер отображает данные, реализуя метод протокола ContactListDisplayLogic:
Вот так выглядят модели для отображения контактов:
В данном случае запрос не содержит данных, так как это просто общий список контактов, однако, если, например экран списка содержал бы фильтр, в данный запрос можно было бы включить тип фильтра. Модель ответа Intrecator содержит в себе нужный список контактов, ViewModel же содержит массив готовых для отображения данных — DisplayedContact.
Почему Clean Swift
Рассмотрим плюсы и минусы этой архитектуры. Во-первых, Clean Swift имеет шаблоны кода, которые упрощают создание модуля. Эти шаблоны можно написать для множества архитектур, но когда они имеются из коробки — это как минимум экономит несколько часов вашего времени.
Во-вторых, данная архитектура, так же как VIPER, хорошо тестируется, примеры тестов доступны в проекте. Так как модуль, с которым происходит взаимодействие, легко заменить заглушкой, определение функционала каждого модуля при помощи протоколов позволяет реализовать это без головной боли. Если мы одновременно создаем бизнес логику и соответствующие к ней тесты (Interactor, Interactor tests), это хорошо вписывается в принцип TDD. Благодаря тому, что выход и вход каждого кейса логики определен протоколом, достаточно просто сначала написать тест, определяющий его поведение, а затем реализовать непосредственно логику метода.
В-третьих, в Clean Swift (в отличие от VIPER) реализуется однонаправленный поток обработки данных и принятия решений. Всегда выполняется только один цикл — View — Interactor — Presenter — View, что в том числе упрощает рефакторинг, так как изменять чаще всего приходится меньше сущностей. Благодаря этому, проекты с логикой, которая часто меняется или дополняется удобнее рефакторить при использовании методологии Clean Swift. Используя Clean Swift вы разделяете сущности двумя путями:
- Изолируете компоненты, декларируя протоколы Input и Output
- Изолируете фичи, используя структуры и инкапсулируя данные в отдельные запросы/ответы/модели UI. Каждая фича имеет свою логику и контролируется в рамках одного процесса, не пересекаясь в одном модуле с другими фичами.
Clean Swift не стоит использовать в небольших проектах без долгосрочной перспективы, в проектах — прототипах. Например, приложение для расписания конференции разработчиков реализовывать с помощью данной архитектуры слишком накладно. Долгосрочные проекты, проекты с большим количеством бизнес логики, напротив, хорошо ложатся в рамки этой архитектуры. Очень удобно использовать Clean Swift, когда проект реализуется для двух платформ — Mac OS и iOS, или планируется портировать его в дальнейшем.
Clean Swift (VIP) iOS Architecture Pattern
Clean Swift (VIP) was first introduced by Raymond Law on his website clean-swift.com. The idea behind it was to tackle the Massive view controller problem while following the main ideas found in Uncle Bob’s Clean Architecture.
Quick overview
When implementing a Clean Swift project your code will be structured around each of your application screens or segments of screens, also known as “scenes”.
In theory, each scene is a structure with around 6 components:
The view controller, interactor, and presenter are the three main components of Clean Swift. They act as input and output to one another as shown in the following diagram.
Example
Imagine a screen with a login button. It’s a Scene that defines a structure with a VIP cycle of View Controller, Interactor, and Presenter. When the user taps the button, the View Controller calls the interactor. The interactor uses the business logic inside to prepare an output (with the use of workers). It then propagates the result to the presenter. The presenter calls the VC’s method to call the router to display a new scene.
View Controller
Defines a scene and contains a view or views.
Keeps instances of the interactor and router.
Passes the actions from views to the interactor (output) and takes the presenter actions as input.
Interactor
Contains a Scene’s business logic.
Keeps a reference to the presenter.
Runs actions on workers based on input (from the View Controller), triggers and passes the output to the presenter.
The interactor should never import the UIKit.
Worker
An abstraction that handles different under-the-hood operations like fetch the user from Core Data, download the profile photo, allows users to like and follow, etc.
Should follow the Single Responsibility principle (an interactor may contain many workers with different responsibilities).
Presenter
Keeps a weak reference to the view controller that is an output of the presenter.
After the interactor produces some results, it passes the response to the presenter. Next, the presenter marshals the response into view models suitable for display and then passes the view models back to the view controller for display to the user.
Router
Extracts this navigation logic out of the view controller.
Keeps a weak reference to the source (View Controller).
Configurator
- Takes the responsibility of configuring the VIP cycle by encapsulating the creation of all instances and assigning them where needed.
Model
Decoupled data abstractions.
Full schema
Other
To avoid memory leaks always pass the view controller to the router and presenter as a weak reference.
I’m not the biggest fan of the official implementation guide. I encourage you to check the sample project of CleanStore. Or just follow the examples above (examples are from my sample project on GitHub).
Key Rules
Keep the file structure (follow the Scene naming).
Use VIP cycle and input/output protocols:
The view controller accepts a user event, constructs a request object, and sends it to the interactor.
The interactor does some work with the request, constructs a response object, and sends it to the presenter.
The presenter formats the data in the response, constructs a view model object, and sends it to the view controller.
The view controller displays the results contained in the view model to the user.
Recommendations
Projects where unit testing is expected.
Long-term and big projects.
Projects with a generous amount of logic.
Projects you want to reuse in the future.
When MVVM, MVP, MVC are not enough or you just hate VIPER but there is a need to introduce a sophisticated architecture.
Native and imperative projects.
Strengths
Easy to maintain and fix bugs.
Enforces modularity to write shorter methods with a single responsibility.
Nice for decoupling class dependencies with established boundaries.
Extracts business logic from view controllers into interactors.
Nice to build reusable components with workers and service objects.
Encourages to write factored code from the start with fast and maintainable unit tests.
Applies to existing projects of any size.
Modular: Interfaces may be easy to change without changing the rest of the system due to using protocol conformant business logic
Independent from the database.
Disadvantages and traps
Many protocols with complicated naming and responsibilities, at first it may be confusing where the protocol is defined.
Large app size due to many protocols.
Despite the fact that there is an official website of Clean Swift architecture, it changes often and implementations may differ between projects.
It’s hard to maintain the separation between VC and presenter. Sometimes the presenter just calls the view methods instead of preparing the UI, so it seems useless and just creates boilerplate.