Так называется глава в книге Роберта Мартина, где описывается концепт:
Quote
Упрощай представление, которое сложно тестировать, а логику выноси в презентаторы
Сегодня я хочу рассказать о том, как это реализовать в типичном фронтенд приложении и покрыть unit-тестами.
К этой заметке я сделал небольшое приложение-стенд для демонстрации идеи. Вы можете запустить и потыкать даже с телефона.
Задача
Предположим, что нужно реализовать экран настроек по следующим требованиям:
- При загрузке настроек показывать скелетон;
- При внесении изменений в настройки показывать кнопки “Сохранить” и “Сбросить”;
- Одна секция “Email уведомления”;
Секция уведомлений состоит из:
- Включение и отключение маркетинговых уведомлений;
- Включение и отключение уведомлений безопасности;
Проблема
- Какая часть приложения отвечает за логику первых двух пунктов?
- Как это покрыть unit-тестами?
Позавчера я бы ответил, что в контейнере, вчера, что в хуке, но сегодня наиболее подходящий для меня вариант это Presentation (FCIS).
Presentation
(state: DomainState) ⇒ ViewState
Определение
Функция, которая принимает на вход бизнес стейт приложения, перерабатывает данные по определённым правилам и возвращает ViewState. Это всегда чистая и полная функция
Она отвечает на вопрос: “Как будет выглядеть viewState при таком-то бизнес стейте?”
- При загрузке показывать скелетон;
- При внесении изменений в настройки показывать кнопки “Сохранить” и “Сбросить”.
Пример
Тест кейсы
Настройки
Если система в состоянии загрузки
√ Пользовать видит скелетон
Если система в состоянии простоя
√ Пользователь видит разделы настроек
Кнопки сохранения и сброса изменений
√ Скрыты по умолчанию
√ Появляются, если в черновик внесли изменения
Пример теста
ViewState
Определение
Сущность, которая описывает, все возможные состояния пользовательского интерфейса. Presentation создаёт и возвращает экземпляр viewStat’а при трансформации бизнес стейта.
Отвечает на вопрос: “Как может выглядеть компонент?”
В React проектах делают интерфейс для определения параметров, которые может принять компонент. ViewState это такое же описание компонента, только он:
- Не содержит коллбеков;
- Используется в Presentation и тестах на него.
Вообще может быть и interface’ом, это выглядит даже чище, но в тестах нельзя проверить присутствие интерфейса. А class можно через instanceof.
Пример
View
Компонент или страница React/Vue/Svelte/… фреймворка
View в данном случае реализован в виде скромного объекта. Компонент содержит только связующий код, тут нет логики, юнитами покрывать нечего.
Мысли
- В Presentation на вход можно передать просто AppState целиком, тогда можно будет формировать вьюстейт на основе feature-флагов проекта, текущей темы, варианта окружения и всё это будет под тестами;
- Внедрение такой сущности ещё дальше отодвигает ваш “чистый код” на TypeScript от фреймворк зависимостей, что должно способствовать упрощению поддержки новых версий библиотек представления, увеличению покрытия юнит-тестами, скорости доставки новых фич;
- Функция settingsState в тесте это одна из фабрик значений по умолчанию;
- Каждому компоненту соответствует свой viewState;
- ViewState может быть и интерфейсом TypeScript, но их неудобно проверять на тип. Придётся для каждого вью стейта класть дескриптор, по которому мы сможем проверить, что это именно он;
- Вместо if (section instanceof SectionViewState) можно использовать switch(section.constructor) case SectionViewState, но TypeScript на начало 2024 не понимает, что в кейсе нужный тип и заставляет дополнительно писать:
<Section viewState={section as SectionViewState} />
- ViewState не содержит onClick’ов и onChang’ей. Он должен отвечать только за представление. Такие коллбэки стоит положить рядом в параметры компонента;
- ViewState это не App/DomainState. Это просто моделька того, что может видеть пользователь;
- В книге Роберта Мартина “Чистая архитектура” в главе “Презентаторы и представления” описаны по своей сути такие же сущности, но немного по-другому названы. После разговора с другом android-разработчиком и после трансляции у Соера мне объяснили, что viewModel жёстко ассоциируется с MVVM, хотя вью модели у Дяди Боба это по сути дата классы, в то время, как в MVVM это вообще другая сущность. Короче, я не хотел бы вносить большей путаницы. И советую всё своё внимание направить именно на концепт;
- И в тэгах проскакивает FCIS (Functional Core Imperative Shell). Я специально их оставил, чтобы позже, можно было бы легко находить статьи по этой концепции. Пока скажу, что для фронтов это по сути Redux. Но сама концепция намного шире и интересней.
Тэги
фронтенды,юнитТестирование,fcis,presenter,viewModel,viewState