Использование всеми разработчиками команды одного code style не менее важно, чем единая точка зрения на архитектуру приложения и ответственности разных объектов. Наличие соглашений по работе с VIPER-стеком не исключение.
-
Название модуля должно полностью отражать его назначение. Суффикс Module в название включать не следует.
Пример:
MessageFolder
,PostList
,CacheSettings
. -
Все элементы модуля разбиты по подпапкам в рамках одной папки модуля.
Пример:
/NewPostUserStory /NewPostModule /Assembly /Interactor /Presenter /Router /View /ChooseAvatarModule /Assembly /Interactor /Presenter /Router /View
-
Если по итогу написания модуля какие-то из его элементов остались неиспользованными, будь то классы, протоколы или методы, они удаляются.
-
Все хелперы располагаются в подпапке своего слоя.
Пример:
/Interactor /UserInputValidator UserInputValidator.h UserInputValidator.m /PlainObjectMapper PlainObjectMapper.h PlainObjectMapperImplementation.h PlainObjectMapperImplementation.m
- Все методы, с помощью которых слои общаются друг с другом, должны быть синхронными.
**Пример:**
```objc
@interface InteractorInput
- (void)obtainDataFromNetwork;
...
@interface InteractorOutput
- (void)didObtainDataFromNetwork:(NSArray *)data;
...
-
Все методы протоколов, которыми закрыты элементы модуля, должны начинаться с глаголов - это помогает явно указать на то, что каждый из компонентов обладает поведением, а не состоянием.
-
Методы, обозначающие начало действия, должны начинаться с глаголов, выражающих приказ или просьбу (глаголов повелительного наклонения).
-
Методы, обозначающие завершение действия или процесса, констатацию факта должны начинаться с глагола прошедшего времени.
Пример:
- (void)obtainImageForPostId:(NSString *)postId; - (void)processUserInput:(NSString *)userInput; - (void)invalidateCurrentCache;
-
В публичных интерфейсах всех классов и протоколов стараемся использовать forward-declaration, используя
#import
лишь при необходимости.Пример:
#import <Foundation/Foundation.h> #import "PostListViewOutput.h" #import "PostListModuleInput.h" #import "PostListInteractorOutput.h" @protocol PostListViewInput; @protocol PostListRouterInput; @protocol PostListInteractorInput; @class PostListViewModelMapper; @interface PostListPresenter : NSObject <PostListModuleInput, PostListViewOutput, PostListInteractorOutput> @property (nonatomic, weak) id<PostListViewInput> view; @property (nonatomic, strong) id<PostListRouterInput> router; @property (nonatomic, strong) id<PostListInteractorInput> interactor; @property (nonatomic, strong) PostListViewModelMapper *postListViewModelMapper; @end
<ModuleName>Interactor.h / <ModuleName>Interactor.m
-
Интерактор не держит состояния, только зависимости, расположенные в его открытом интерфейсе.
Пример:
@interface PostListInteractor : NSObject <PostListInteractorInput> @property (nonatomic, weak) id<PostListInteractorOutput> output; @property (nonatomic, strong) id<AccountService> accountService; @end
-
Интерактор держит weak-ссылку на презентер. Переменная называется
output
.Пример:
@property (nonatomic, weak) id<PostListInteractorOutput> output;
<Feature>Facade.h / <Feature>Facade.m
В случае, если в интеракторах нескольких модулей есть повторяющаяся логика по использованию сервисов определенным образом, она инкапсулируется в отдельный фасад над сервисами. В качестве примера можно привести объект, реализующий логику пагинации разных лент постов - он умеет запрашивать список новых элементов, сранивать их с ранее закешированными, вычислять смещения и дырки - и многое другое. Благодаря выделению этих связей в отдельную сущность, пагинация может быть достаточно легко подключена для любого модуля списка элементов.
Пример: PagingFacade
.
<ModuleName>InteractorInput.h
Содержит методы для общения с интерактором. Этим протоколом закрыт интерактор с точки зрения презентера.
- (NSArray *)obtainNewsFromCache;
- (void)obtainMessageWithId:(NSString *)messageId;
- (void)performLoginWithUsername:(NSString *)username password:(NSString *)password;
- Если в данном модуле нам нужно самостоятельно решать, в какой момент просить данные из кэша, а в какой - из сети, допустимо явно указывать это интерактору (
obtainFromNetwork/-fromCache
).
<ModuleName>InteractorOutput.h
Содержит методы, при помощи которых интерактор общается с вышестоящим слоем модуля. Этим протоколом обычно закрыт презентер.
- (void)didObtainMessage:(Message *)message;
- (void)didPerformLoginWithSuccess;
- В большинстве случаев в качестве префикса каждого метода используем
did
- это указывает на пассивную роль интерактора, который умеет выполнять ряд действий по запросу и уведомлять об их окончании. - В случае отсутствия единой системы обработки ошибок заводятся пары методов (
didObtainWithSuccess/-withFailure
).
<ModuleName>Presenter.h / <ModuleName>Presenter.m
-
В отличие от всех остальных элементов, презентер обладает состоянием. Оно находится в приватном extension.
-
Презентер держит weak-ссылку на view. Переменная называется
view
.Пример:
@property (nonatomic, weak) id<PostListViewInput> view;
-
Презентер держит strong-ссылку на роутер. Переменная называется
router
.Пример:
@property (nonatomic, strong) id<PostListRouterInput> router;
-
Презентер держит strong-ссылку на интерактор. Переменная называется
interactor
.Пример:
@property (nonatomic, strong) id<PostListInteractorInput> interactor;
-
Если презентеру нужно держать объект, реализующий протокол
ModuleInput
дочернего модуля, переменная называется<OtherModuleName>ModuleInput
.
<ModuleName>State.h / <ModuleName>State.m
В том случае, если текущий модуль обладает каким-либо состоянием, его можно выделить в отдельный объект, не обладающий никаким поведением и выступающий простым хранилищем данных.
Пример:
@interface PostListState
@property (nonatomic, assign) FeedType feedType;
@property (nonatomic, assign) BOOL hasHeader;
@property (nonatomic, strong) NSString *feedId;
@end
<ModuleName>ModuleInput.h
Содержит методы, при помощи которых с модулем могут общаться другие модули или его контейнер.
- При использовании библиотеки ViperMcFlurry наследуется от протокола
<RamblerViperModuleInput>
.
- (void)configureWithPostId:(NSString *)postId;
- (void)updateContentInset:(CGFloat)contentInset;
<ModuleName>ModuleOutput.h
Содержит методы, при помощи которых модуль общается со своим контейнером или другими модулями.
- При использовании библиотеки ViperMcFlurry наследуется от протокола
<RamblerViperModuleOutput>
.
- (void)didSelectMenuItem:(NSString *)menuItem;
- (void)didPerformLoginWithSuccess;
<ModuleName>Router.h / <ModuleName>Router.m
- При использовании библиотеки ViperMcFlurry держит weak-ссылку на
ViewController
, отвечающий за переходы этого модуля. Ссылка представляет собой свойство, закрытое протоколом<RamblerViperModuleTransitionHandlerProtocol>
. Обычно эта переменная называетсяtransitionHandler
<ModuleName>Route.h / <ModuleName>Route.m
Если несколько роутеров в рамках одного приложения реализуют повторяющуюся логику по переходам на один экран - ее можно инкапсулировать в отдельном объекте-маршруте, который будет подключаться к нужным модулям. К примеру, это может пригодиться в случае экрана авторизации, на который можно перейти из разных модулей - настроек, профиля, бокового меню. Благодаря инкапсуляции этой логики в отдельном объекте, мы избавляемся от необходимости в роутере каждого из этих модулей писать один и тот же код.
- (void)openAuthorizationModuleWithTransitionHandler:(id<RamblerViperModuleTransitionHandlerProtocol>)transitionHandler;
<ModuleName>RouterInput.h
Содержит методы переходов на другие модули, которые могут быть вызваны презентером.
- (void)openDetailNewsModuleWithNewsId:(NSString *)newsId
- (void)closeCurrentModule;
- Для консистентности все методы этого протокола начинаются либо на
open-
(открытие какого-либо модуля),close-
(закрытие модуля) илиembed-
(встраивание дочернего модуля в контейнер).
<ModuleName>View.h / <ModuleName>View.m
, <ModuleName>ViewController.h / <ModuleName>ViewController.m
, <ModuleName>Cell.h / <ModuleName>Cell.m
.
-
Все
IBOutlet
'ы иIBAction
'ы (то есть все зависимости и интерфейс) View выносятся в его публичный интерфейс. -
View держит strong-ссылку на презентер. Переменная называется
output
.Пример:
@property (nonatomic, strong) id<PostListViewOutput> output;
<ModuleName>DataDisplayManager.h / <ModuleName>DataDisplayManager.m
Объект, закрывающий логику реализации UITableViewDataSource
и UITableViewDelegate
. Работает только с данными, ничего не знает о конкретных UIView
экрана. Обычно протоколом не закрывается, потому что конкретному экрану чаще всего соответствует одна конкретная реализация DataDisplayManager.
- (id<UITableViewDataSource>)dataSourceForTableView:(UITableView *)tableView;
- (id<UITableViewDelegate>)delegateForTableView:(UITableView *)tableView
withBaseDelegate:(id <UITableViewDelegate>)baseTableViewDelegate;
<ModuleName>CellObjectFactory.h / <ModuleName>CellObjectFactory.m
Зачастую удобно бывает выносить логику по созданию моделей ячеек из DataDisplayManager'а в отдельный объект, который, по сути, преобразует обычные модели в CellObject'ы.
<ModuleName>ViewInput.h
Содержит методы, при помощи которых презентер может управлять отображением или получать введенные пользователем данные.
- (void)updateWithTitle:(NSString *)title;
- (NSString *)obtainCurrentUserInput;
<ModuleName>ViewOutput.h
Содержит методы, при помощи которых View уведомляет презентер об изменениях своего состояния.
- В этом же протоколе находятся и методы, при помощи которых View уведомляет презентер о событиях своего жизненного цикла.
- (void)didTapLoginButton;
- (void)didModifyCurrentInput;
- В большинстве случаев в качестве префикса каждого метода используем
did
- это указывает на пассивную роль View, который умеет выполнять ряд действий по запросу и уведомлять об их окончании.
Примеры:
- (void)didTriggerViewWillAppearEvent;
- (void)didTriggerMemoryWarningEvent;
<ModuleName>Assembly.h / <ModuleName>Assembly.m
- В интерфейс Assembly выносится только метод, конфигурирующий View. Установка всего VIPER-стека - это детали реализации assembly, которые не должны быть известны окружающему миру.
- (PostListViewController *)viewPostListModule;
- (PostListPresenter *)presenterPostListModule;
- Для более удобной автоподстановки у всех методов, создающих стандартные компоненты, указывается префикс
<ComponentName><ModuleName>Module
.
-
Все методы протоколов и конкретных классов обязательно покрываются подробными javadoc-комментариями.
Пример:
@protocol PostListInteractorInput <NSObject> /** Метод возвращает модель поста по определенному индексу @param index Индекс поста в рамках просматриваемой категории @return Пост */ - (PostModelObject *)obtainPostAtIndex:(NSUInteger)index; /** Метод возвращает общее количество постов для текущей ленты @return Количество постов */ - (NSUInteger)obtainOverallPostCount; @end
-
Комментарии пишутся к интерфейсам всех уникальных компонентов модуля (различные хелперы, ячейки).
Пример:
/** Ячейка, используемая для краткого отображения поста в списке */ @interface PostListCell : UITableViewCell ... @end
-
К интерфейсам всех неуникальных компонентов (интерактор, презентер, view), а также к их протоколам, пишется одинаковый комментарий, описывающий предназначение всего модуля.
Пример:
/** Модуль отвечает за отображение списка постов в любой из лент приложения. Используется как embedded-модуль. */ @interface PostListAssembly : TyphoonAssembly ... @end
-
В имплементации классов группы методов различных протоколов разбиваются при помощи
#pragma mark -
.Пример:
@implementation PostListPresenter #pragma mark - PostListModuleInput - (void)configureModuleWithPostListConfig:(PostListConfig *)config { ... } #pragma mark - PostListViewOutput - (void)didTriggerPullToRefreshEvent { ... } #pragma mark - PostListInteractorOutput - (void)didProcessCacheTransaction:(CacheTransactionBatch *)transaction { ... }
- Для каждого компонента модуля создается отдельный тест-кейс с названием вида
<ModuleName>ViewControllerTests.m
. - Стараемся придерживаться правила один тест - одна проверка.
- Для разделения реализации теста на логические блоки используем нотацию given/when/then.
- Тесты методов различных протоколов разбиваются при помощи
#pragma mark -
. - Так как в интерфейсе Assembly объявлены не все методы, для их проверки создается отдельный extension
_Testable.h
. В этом случае мы тестируем приватные методы, но такой подход обусловлен деталями реализации библиотеки Typhoon. В случае написания фабрики вручную, возможно проверить результаты работы компонента через его публичный интерфейс. - Файловая структура модуля в тестах максимально повторяет файловую структуру проекта.