Сервисная архитектура во Vue 2 | Проектирование класса (примитивы и объекты)

от автора

Это 2 часть цикла статей о сервисной архитектуре во Vue 2. В 1 части я рассказала о том, какие способы выноса логики популярны на данный момент, почему они меня не устраивали, и чего я хотела достичь.

UPD к 1 части

В 1 части не все меня поняли, и думали, что я решаю какую-то конкретную задачу и просили пример или уточняющие данные. Эта серия статей не о конкретной задаче и конкретном примере.

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

Я не приветствую заменять все сервисами и больше не использовать vuex, миксины и т.д., я хотела донести мысль, что каждый инструмент хорош для своей цели. Но если вы чувствуете, что этого инструмента вам недостаточно для какой-то задачи, и что минусы достаточно существенны для какого-то конкретного случая, то возможно вам стоит вынести эту логику в класс и оформить его как сервис для определенной задачи.

Сервис в моем понимании — это отдельный архитектурный слой, который выполняет конкретную задачу (например, работает с товарами: запрашивает их с определенными условиями, обрабатывает, производит поиск).

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

О чем мои статьи? О том, как можно использовать классы во Vue 2. Какое это отношение имеет к сервисной архитектуре? Прямое, это один из способов, как организовать сервис. С этого все и начинается, без практического удобного решения, как эти сервисы встраивать, они не будут появляться в проекте.

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

О встройке класса во Vue 2

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

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

Примитив

Я решила начать с того, чтобы попытаться встроить примитивное свойство из класса в компонент. Логика простая, сам примитив не передашь, он запишется и все, связи нет. Тогда я вспомнила про функцию ref из Vue 3 (docs), где все примитивы они предлагают обернуть в объект по структуре:

{ value: <some_val> }

Итак, пробуем создать класс

class Example {   someString = {     value: 'someString',   }; }  const example = new Example();

Пытаемся встроить свойство в пару компонентов как-то так

<template>   <div>     <span>Value: {{ someString.value }}</span>   </div> </template>
export default {   data() {     return {       someString: example.someString,     };   }, }

Нам же еще нужно это свойство изменить, добавляем кнопку и метод

<template>   ...     <button @click="changeValue">Change value</button>   ... </template>
...   methods: {     changeValue() {       this.someString.value = 'someAnotherString';     },   }, ...

И наконец, проверяем

Окей, это работает, и это оказалось проще, чем я думала
Окей, это работает, и это оказалось проще, чем я думала

Я понимаю, что сейчас можно подумать что-то вроде «Ну это же очевидно, что оно сработает». И когда я попробовала, и оно сработало, я подумала абсолютно то же самое. Но почему-то до этого я никогда не пыталась так сделать, ровно также как и многие другие на самом деле.

Вся эта реактивность во Vue с его геттерами и сеттерами была покрыта некоторой мистикой, хоть я и смотрела видео до этого с объяснениями этой технологии, но все равно не задумывалась о том, что если мы передадим ссылку на объект, то связь будет, ей некуда будет деваться. Хотя если бы в фреймворке сделали бы хотя бы shallow copy, то пришлось бы как-то вертеться.

Хорошо, а если бы мы хотели менять через метод класса? Как бы мы могли сделать это?

Давайте добавим метод

class Example {   ...   changeValue() {     this.someString.value = 'anotherString';   } }

В компонент мы могли бы встроить его как-то так

methods: {   changeValue() {     example.changeValue();   }, },

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

export default {   data() {     return {       ...       changeValue: example.changeValue.bind(example),     };   }, }

Подробнее о том, почему метод встраивается в секцию data я буду говорить в 3-ей части, когда разговор будет идти о разных экземплярах и их уничтожении.

Давайте проверим

Через метод тоже отрабатывает корректно
Через метод тоже отрабатывает корректно
Где смотреть полные примеры?

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

Ссылки будут приведены на локально поднятую документацию.

http://localhost:8081/class/primitive/property.html

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

Давайте сделаем наше свойство для компонента в формате read-only и заставим производить изменения только через методы.

Создадим класс с псевдо-приватным свойством (покажем это визуально) и методом для его изменения

class Example {   _privateString = {     value: 'I\'m private',   }    changePrivateString() {     this._privateString.value = 'My value is changed from method';   } }  const example = new Example();

Но как нам ограничить его изменения внешне, из компонента? Я реализовала это через геттер с Proxy (docs). Добавим геттер

class Example {   ...   get privateString() {     return new Proxy(this._privateString, {       // Запрещаем изменение       set() {         throw new Error('This property is read-only');       },     });   } }

Суть в том, что компонент забирает именно публичный геттер, а не внутреннее приватное свойство. О том, как разрешать компоненту получать только публичные свойства, я буду говорить в 4 части.

В set необязательно кидать exception, достаточно вернуть из сеттера false, но таким образом мы получаем ошибку без нормального описания

Как выглядит ошибка при `return false;`
Как выглядит ошибка при `return false;`
Ошибка с exception, читабельно
Ошибка с exception, читабельно

В двух компонентах заберем публичный геттер

data() {   return {     privateString: example.privateString,   }; },

Но в первом компоненте попытаемся изменить с компонента через присваивание

methods: {   changeValue() {     this.privateString.value = 'Should cause error';   }, },

А во втором компоненте изменим через метод класса

data() {   return {     ...     changeValue: example.changePrivateString.bind(example),   }; },

Давайте проверим

Попытка изменения через компонент приводит к ошибке, через метод все меняется корректно
Попытка изменения через компонент приводит к ошибке, через метод все меняется корректно
Полный пример

Окей, с read-only разобрались, а что если мы хотим, чтобы данные менялись с компонента, но нам нужна дополнительная валидация? Для этого нам нужно изменить наш Proxy, чтобы он не запрещал изменения, а валидировал их.

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

set(obj, prop, value) {   // Запрещаем добавление новых полей   if (prop !== 'value') {     throw new Error('Only accesible property is "value"');   }    // Запрещаем записывать НЕ строку   if (typeof value !== 'string') {     throw new TypeError('Value must be string');   }    // Проводим операцию присваивания   obj[prop] = value;    // Сигнализируем, что ошибки не возникло   return true; },

Я сделала тестовый компонент для проверки, куда добавила несколько присваиваний, вот результат

Через проверку прошло только присваивание полю `value` строкового значения
Через проверку прошло только присваивание полю `value` строкового значения
Полный пример

Объект

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

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

Пример для объекта со статичной структурой

А что если нам нужно добавить поле? Документация Vue подсказывает выход — использовать Vue.set. Работает ли это, если мы вызовем это в классе, а не в компоненте? Давайте проверим.

Сделаем класс со свойством-объектом, импортируем туда Vue, и сделаем метод, где мы добавим новое свойство с помощью set

import Vue from 'vue';  class Example {   testObject = {     oldField: 'I was here from the beginning!'   }    addNewField() {     Vue.set(this.testObject, 'newField', 'I was added recently!');   } }  const example = new Example();

Сделаем пару тестовых компонентов, где мы получим наше свойство и метод. Для теста давайте добавим туда еще дополнительный метод, который будет добавлять новое свойство из компонента с помощью this.$set

export default {   data() {     return {       testObject: example.testObject,       addNewField: example.addNewField.bind(example),     };   },   methods: {     addField() {       this.$set(this.testObject, 'fieldFromComponent', 'I was added from component!');     }   } };

Итак, время истины

Все работает корректно, реактивность не пропадает
Все работает корректно, реактивность не пропадает
Полный пример

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

Окей, а что если наш объект приходит с сервера, и мы хотим сначала инициализировать его как null, а потом уже записать значение? Перезаписать наше свойство в классе мы не можем, так что воспользуемся тем же механизмом, который использовали для работы с примитивом. Т.е. обернем наш объект в еще один объект.

class Example {   obj = {     value: null,   }    fillObj() {     this.obj.value = { field: 'field' };   } }

Выведем в паре компонентов, вызовем метод и проверим

Все меняется корректно
Все меняется корректно
Полный пример

Если мы хотим запретить изменения в объекте из компонента, то тактика такая же, как и с примитивом. Делаем геттер, где отдаем наш объект, обернутый в Proxy, из set кидаем ошибку.

Но во время тестирования я обнаружила любопытную особенность, связанную с this.$set. Так как это достаточно узкий кейс, в статье я на этом останавливаться не буду, welcome на страничку в моей документации, там я это описала.

Пример readonly объекта

С валидацией объекта все абсолютно также, как мы делали для примитива. Но в документации я привела пример того, как можно использовать эту технологию на примере валидации значений в форме. Покажу под катом, какой компонент в результате у меня получился.

Компонент формы
<template>   <div>     <p>       <input         v-model="form.firstName"         placeholder="Имя"         :class="{ 'error': errors.firstName }"       /><br/>       <span v-if="errors.firstName" class="error">         {{ errors.firstName }}       </span>     </p>      <p>       <input         v-model="form.age"         placeholder="Возраст"         :class="{ 'error': errors.age }"       /><br />       <span v-if="errors.age" class="error">         {{ errors.age }}       </span>     </p>   </div> </template>
import ValidatedForm from '@example-services/ValidatedForm';  export default {   data() {     return {       form: ValidatedForm.form,       errors: ValidatedForm.errors,     };   }, }

Обратите внимание, насколько чистый стал компонент. Вся логика связанная с хранением и валидацией ушла в класс, а компонент стал заниматься тем, чем он и должен был — отображением. Т.е. единственное, что решает компонент — это как и когда показывать ошибку пользователю.

Полный пример (и в том числе, как выглядит класс, для того чтобы компонент выглядел так), смотрите в моей документации.

Пример валидации объекта

Напоминаю, что документация разворачивается локально из моего репозитория с помощью команды yarn docs:dev. Ссылка приведена на поднятую документацию.

http://localhost:8081/class/object/validated.html


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

План примерно такой: в 3 части поговорим о массивах и вычисляемых свойствах, в 4 части поговорим об экземплярах класса и удобных функциях, чтобы можно было встраивать класс в компонент проще.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Запустили ли вы мою документацию?
100% Нет, и не хочу 3
0% Нет, но попозже гляну 0
0% Нет, неудобно разворачивать локально 0
0% Да 0
Проголосовали 3 пользователя. Воздержались 2 пользователя.

ссылка на оригинал статьи https://habr.com/ru/post/700964/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *