Как реализовать Presenter и не нарушить SOLID при проектировании приложения на Vue.js.
Разработчикам26 мая 2022

Плагины во Vue: как их писать, и в чем их польза

Александр ШапоровFrontend-разработчик

В этой статье рассмотрим, как соблюсти паттерн MVP, не нарушить SOLID и найти подходящие инструменты для реализации Presenter при проектировании приложения на Vue.js.

Представим, что мы разрабатываем to-do приложение. Данные хранятся на бэкенде, приложение запрашивает их по сети, затем  их как-то форматирует и преобразовывает. Эти преобразованные данные нужно будет выводить на страницу, а также предоставить пользователю рычаги для их изменения.

Основная сложность выделения Presenter в паттерне MVP при разработке на Vue

Попробуем выделить основные части приложения:

  • M — Model — Бэкенд с данными и методами для изменения;
  • V — View — Фронтенд с разноцветными кнопками и чекбоксами;
  • P — Presenter — Часть приложения между фронтендом и бэкендом, для промежуточной обработки.

О Presenter давайте подробнее. Он должен:

  1. Отправлять данные на бэкенд;
  2. Запрашивать данные с бэкенда;
  3. Предоставлять методы для форматирования данных, приходящих с бэкенда.
  4. Инкапсулировать внутри себя повторяющуюся логику, которую мы не хотим делегировать Модели напрямую.

В сложном приложении Presenter является не только посредником между бэкендом и фронтендом, но также позволяет инкапсулировать ту повторяющуюся логику, которую мы реализуем на фронтенде, вместо того чтобы запрашивать ее по сети или реализовывать прямо в интерфейсе . 

В идеальном мире, View-компонент занимается только лишь отрисовкой готовых данных, а все подготовительные процедуры выполняет Presenter

Пример решения задачи без Presenter

Чтобы стало понятнее, что такое «повторяющаяся логика», и почему не все следует хранить на бэкенде, приведем пример.

Мы хотим, чтобы у премиум-пользователя, обычного пользователя, и демо-пользователя были свои приветственные сообщения на главной странице. Это приводит нас к примерно такой конструкции:

Пока еще такой участок кода можно расширять — при появлении новых ролей понадобится лишь добавить новый пункт в свитч. 

Тем не менее, если эту логику хранить прямо в компоненте главной страницы, то уже сейчас от кода будет заметен легкий «душок». Наш компонент начинает не только отображать данные как компонент View, но и интерпретировать их, реализовывать какую-то логику по преобразованию и форматированию, что характерно для Presenter. На данном этапе, скорее всего, метод реализующий эту логику, будет computed свойством компонента с именем вроде welcomingPageTitle.

 

О нарушениях SOLID и росте проблемы

Проходит неделя, и заказчик ставит новую задачу с такими требованиями:

Запускаем новую секцию. На главной странице будет отображаться маскот, типа скрепки Clippy из Microsoft Office, который будет давать пользователю подсказки: 

1. Демо-пользователю зарегистрироваться;

2. Обычному пользователю купить премиум доступ;

3. Премиум-пользователю продлить подписку;

Задание приняли, идем выполнять!


Стало чуть сложнее, но если мы действуем по принципу «вижу цель, не вижу препятствий», то дальше копипастим кусок логики из другого компонента, и чуть-чуть переписываем под свой случай. Здесь мы уже начинаем обращаться к состоянию приложения, но самая большая проблема в том, что мы, во-первых, повторяем сами себя, то есть нарушаем DRY, во-вторых, у нашего компонента слишком много причин для изменений, а это нарушает Single Responsibility Principle — принцип единой ответственности, первый пункт SOLID. 

Масштаб проблемы увеличивается, но ее все еще никто не локализовал.

О невозможности масштабировать ПО


Проходит еще неделя, и заказчик сообщает, что логику нужно изменить: 

  1. Демо-пользователю предлагаем сначала купить премиум, а если он уже посещал страницу с оффером — зарегистрироваться бесплатно;
  2. Обычному пользователю показываем два варианта: перейти на страницу с премиум, если он не посещал ее, а если посещал — настаиваем, чтобы вернулся и купил;
  3. Премиум-пользователю напоминаем про окончание подписки, только если он видит это сообщение впервые за сессию.

Логики становится слишком много, но мы пытаемся как-то расширить имеющуюся систему:

На этом этапе код становится совсем плохим: появляются вложенные условия, сложные нечитаемые конструкции, и все это прямо внутри View. Возникает соблазн дописать этот вариант до рабочего результата, а сверху оставить предупреждение: «Работает — не лезь!»

Мы же будем умнее, и перепишем систему. 

Повторяющаяся логика и суть Presenter


Чтобы провести рефакторинг, нужно выделить повторяющуюся логику. В нашем случае нам нужна такая система, которая:

  1. Глобальная и единственная;
  2. Имеет доступ к состоянию приложения;
  3. Умеет принимать решения в зависимости от этого состояния; 
  4. Умеет возвращать какой-то контент в зависимости от запроса.

В идеале внутри View это должно выглядеть так:

Обратите внимание на вызов get(“title”). Мы просто говорим нашей системе, что хотим получить заголовок, и она его отдает каким-то неизвестным компоненту образом. Именно такую логику и следует выносить в Presenter. Такие операции ограничены только лишь нуждами приложения, поэтому выносить их на бэкенд нецелесообразно. При этом такие запросы могут повторяться с незначительными изменениями в разных компонентах по всему приложению. Если эту логику не выделить, мы будем дублироваться.

 

Пример модуля Presenter

А что, если мы заменим наши getters из хранилища состояния на лямбда-функции, возвращающие булевые значения?

Такие функции называются «предикатами». Предикат всегда возвращает true или false, и обычно по своему смыслу отвечает на вопрос. Например «Посещал ли пользователь страницу с премиумными подписками?» Их имена обычно начинаются с is, has, can и т.д.

Также создаем массив объектов, которые будет хранить набор предикатов и соответствующее значение, подходящее под эти условия. Например в случае с текстом:

Важно: если мы занесем данные в порядке, при котором самое сложное условие будет в конце, нам будет достаточно лишь вернуть первый прошедший проверку вариант с конца. 

Это немного напоминает логику работы каскада в CSS, из всех возможных стилей мы выбираем наиболее специфичный.

Можем взять эту идею за основу системы, а кого заинтересовало — можете предложить варианты как это сделать понятнее, быстрее или эффективнее, написав нам на почту.

Также у нас будет метод get, который принимает место в нашей «библиотеке», и возвращает content самого подходящего объекта с данными.  

Пример использования:

Идея этого класса взята не из воздуха, а с реального проекта. Автор статьи писал чуть более сложный вариант этой системы для коммерческого приложения. 

В дальнейшем этот же алгоритм применялся для поиска конфигурационных файлов, и даже целых компонентов.

Практика: как воплотить Presenter в жизнь с помощью плагинов

Класс, который мы написали, пока что не будет работать. Его нужно соответствующим образом подключить.

  

1. Наш класс будет синглтоном, подключаемым один раз при создании приложения;

2. Наш класс должен иметь доступ к $store и $router . Для того чтобы в его методах проверки можно было использовать значения текущего пути, а также данные приложения;

3. Наш класс должен быть доступен через this.$presenter. 

Вот тут мы и вспоминаем про плагины. Для примера, Vue Router — это плагин.

  • Плагин является синглтоном, который подключается один раз;
  • Плагин может быть любым классом, объектом, функции. Эта сущность может иметь доступ к другим сущностям приложения;
  • Плагин доступен в любом компоненте под тем ключом, под каким мы его регистрируем.

Класс раздающий контент по условиям назовем как Compositor. Класс плагина — как Presenter. Compositor будет доступен как Presenter.compositor. Это нужно, чтобы мы могли сделать несколько различных классов, к которым будем обращаться как presenter.compositor, presenter.payments, presenter.stupidityStuff и т.д.

Создадим файл PresenterPlugin.js

Здесь есть трюк, который выходит за рамки документации Vue.

Мы будем экспортировать не объект, а функцию возвращающую объект. Это нужно для того, чтобы заранее закрыть в замыкании конкретный инстанс контроллера, а не создавать его через new внутри этого объекта.

Далее открываем main.js и подключаем.

Вот что происходит:

1. Функция presenterPlugin принимает в себя инстанс класса Presenter;

2. Эта функция возвращает объект с методом install, принимающим App;

3. install вызывается с помощью use.

На данном этапе, мы просто сделали обертку над одной строчкой кода:

О чем не напишут в документации

К сожалению, наш плагин по функционалу все еще не дотягивает до роутера или хранилища состояния.

Мы не сможем использовать наш Presenter внутри метода setup, а также внутри методов render в функциональных компонентах и внутри script setup.

Все дело в том, что там this не будет указывать на конкретный компонент, а значит и this.$presenter не сработает и выдаст ошибку.

Официальная документация об этом ничего не говорит, так что можете либо читать дальше, либо открывать исходники роутера и искать ответ самостоятельно.
 

Дорабатываем плагин

Открываем файл с плагином, и дописываем:

Pro tip: чтобы избежать конфликта имен, можно использовать символы вместо строковых ключей в provide/inject.

Теперь мы можем в любом компоненте вызвать наш хук usePresenter и получить тот же результат, что и при использовании useStore или useRouter! 


В этой статье мы в очередной раз (а кто-то, возможно, впервые) разобрали паттерн MVP, и лучше поняли, как его применять в контексте фронтенд разработки на Vue. А также, надеюсь, было полезно узнать о некоторых неочевидных возможностях плагинов во Vue. 

Другие статьи

Ко всем статьям

Интересные статьи и кейсы
от Work Solutions

Нажимая кнопку «Подписаться», я даю согласие на обработку персональных данных

Спасибо за подписку!