Предупреждение

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

Предыстория

Примерно год назад фронтенд нашего проекта, в качестве фреймворка для которого используется Vue.js 2, дошел до состояния, хорошо описываемого словами "проще сжечь".

Дело было вот в чем: изначально, при выборе стека технологий, мы ориентировались на знакомые и простые, на наш взгляд, в освоении фреймворки. Мое предвзятое отношение к React, которое, к слову, со временем только усугубилось, не позволило выбрать его в качестве основы. С Angular версий 2+ и TypeScript никто из нас тогда не был знаком, а Ember мы сочли слишком сложным для того, чтобы быстро можно было сделать нормально работающий прототип, да и найти специалистов, знающих его, не так уж легко, хотя именно этот фреймворк я считаю лучшим выбором при наличии достаточно сильной команды и приличного бюджета. Так проект стартовал с базовым набором в виде Vue.js, VuetifyVuex и Vuelidate.

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

Точкой невозврата для нас стал момент выхода плагина Vue Composition API для Vue 2. К тому времени наш нетипизированный корабль уже трещал по всем швам, и решено было убить сразу нескольких зайцев одним выстрелом: полностью перевести проект на TypeScript, внедрить Vue Composition API и решить проблемы валидации сложных форм.

А проблемы были. Vuelidate - отличная библиотека с массой готовых валидаторов и остается таковой до тех пор, пока мы имеем дело с, так сказать, плоскими формами - формами, не использующими вложенных компонентов, также требующих валидации. Vuelidate работает на уровне компонента, требует наличия миксина, содержит массу правил, которые нам не были нужны, но все равно тянулись в составе пакета, поддержка TypeScript и Composition API на тот момент отсутствовала, и мы не могли более с этим миритьcя. К примеру, для того, чтобы по нажатию на кнопку "отправить" выделить все неправильно заполненные поля, приходилось серьезно извернуться, особенно при валидации переиспользуемых компонентов. Еще одной проблемой было отсутствие привязки текстового сообщения об ошибке к каждому конкретному правилу. В результате, для каждого поля приходилось создавать computed-свойство, что раздувало компонент до неприличных размеров. Ключевая, на мой взгляд, проблема всех подобных библиотек в том, что они пытаются охватить максимальный спектр задач, и, в результате, все равно не могут решить их все так, как нужно пользователю, при этом лишаются так необходимой универсальному решению легкости и гибкости.

Пример валидации для простого компонента с двумя полями
<template>
  <form @submit.prevent="login">
    <div>
      <input
        placeholder="E-mail"
        id="email"
        v-model="email"
        maxlength="100"
        @change="$v.email.$touch()"
      />

      <div style="color: red" v-if="emailError">{{ emailError }}</div>
    </div>

    <div>
      <input
        placeholder="Password"
        type="password"
        id="password"
        v-model="password"
        maxlength="100"
        @change="$v.password.$touch()"
      />

      <div style="color: red" v-if="passwordError">{{ passwordError }}</div>
    </div>

    <div>
      <button type="submit">Login</button>
    </div>
  </form>
</template>

<script>
import { validationMixin } from 'vuelidate';
import { required, email, minLength } from 'vuelidate/lib/validators';

export default {
  mixins: [validationMixin],

  data: () => ({
    email: '',
    password: ''
  }),

  computed: {
    emailError() {
      const field = this.$v.email;

      if (!field.$dirty) return '';
      else if (!field.required) return 'Enter your e-mail.';
      else if (!field.email) return 'Invalid e-mail format.';
      else return '';
    },

    passwordError() {
      const field = this.$v.password;

      if (!field.$dirty) return '';
      else if (!field.required) return 'Enter password.';
      else if (!field.minLength) return 'Password must contain at least 8 characters.';
      else return '';
    }
  },

  methods: {
    login() {
      this.$v.$touch();

      if (this.$v.$invalid) return;

      // do something
    }
  },

  validations: {
    email: {
      required,
      email
    },
    password: {
      required,
      email,
      minLength: minLength(8)
    }
  }
};
</script>

Поиск решения

Решение, на наш взгляд, состояло в переходе от использования Vuex и Vuelidate к использованию моделей с поддержкой валидации. Почему наши поиски закончились ничем, и как мы самостоятельно написали альтернативу - подробно расскажу в одной из следующих публикаций. С точки зрения модели, правило для валидации поля получилось довольно простым:

type ValidationRule = (value: any, data: Data) => boolean | string;

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

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

Многим, полагаю, знакома так называемая философия UNIX или UNIX way, которая часто сводится единственному правилу: программа должна выполнять одну задачу, но делать это хорошо. Именно такой хотелось видеть библиотеку валидации.

Таким образом, сформировался некоторый набор требований, которым должен был соответствовать кандидат:

  • возможность возвращать не только true/false, но и строку с текстом ошибки;

  • не иметь лишних зависимостей;

  • иметь поддержку TypeScript;

  • не содержать ничего лишнего;

  • иметь удобный интерфейс для расширения функциональности.

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

Над названием долго думать не пришлось, если tsfv - аналог v8n с типами, то новое решение получило название v9s по аналогии. Первый вариант был готов буквально через два дня и в основе своей уже содержал цепочки: каждое правило заключалось в свой экземпляр единственного библиотечного класса, содержащий ссылку следующий элемент цепи, а внешние правила можно было указывать при помощи вызова метода use() этого же класса. Также имелась возможность указать альтернативную цепочку, благодаря методу or(), которая отрабатывала в случае провала проверки основной, и инвертировать результат вызовом not(). На выходе можно было получить функцию, которая принимала на вход значение, а возвращала boolean | string.

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

На тот момент создание функции для проверки выглядело примерно так:

import v9s from '@/vendor/v9s';

function integer(value: string) {
  return /^[0-9]+$/.test(value);
}

const check = v9s
	.use(integer, 'The value must be an integer string.')
	.minLength(1, 'Enter a value.')
	.check;

check('1234'); // returns: true
check('1.22'); // returns: 'The value must be an integer string.'
check(''); // returns: 'Enter a value.'

Что внешне не отличается от конечного результата. На этом активная разработка самой библиотеки прекратилась и началось ее активное использование в течение 10 месяцев. За это время было произведено тестирование в реальных условиях, устранено большинство ошибок, добавлен новый полезный функционал, а вот времени на выделение четырех файлов в отдельный проект все никак не находилось. И только сейчас, когда стало понятно, что у этой разработки только два варианта: забыться в глубинах собственных проектов или быть представленной на суд общественности - я собрал-таки волю в кулак и пустился во все тяжкие взялся за подготовку проекта к публикации. Здесь стоит отметить, что сразу после создания текущая версия уже была помещена в приватный репозиторий на GitHub (не показывать же такой стыд людям).

Особенности решения

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

Первая и наиболее важная, определяющая особенность: v9s производит проверку единственного поля, а не всей схемы. Да, поле может быть и объектом или массивом, но проверка полей этого объекта (элементов массива) не должна быть задачей конкретной цепочки.

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

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

Модификаторы

Кроме, собственно, правила, можно передать и функцию-модификатор, которая преобразует значение для последующих правил в цепочке. Проще всего будет разобрать на примере.

Предположим, что значением поля должна быть целочисленная строка, значение числа в которой находится в пределах от 10 до 100. Есть два способа решения данной задачи при помощи v9s:

Решение "в лоб":

import v9s from 'v9s';

function integer(value: string) {
  return /^[0-9]+$/.test(value);
}

function min(minimum: number, value: string) {
  return Number(value) >= minimum;
}

function max(maximum: number, value: string) {
  return Number(value) <= maximum;
}

const check = v9s
  .use(max.bind(undefined, 100), 'The value is greater than 100.')
  .use(min.bind(undefined, 10), 'The value is less than 10.')
  .use(integer, 'The value must be an integer string.')
  .minLength(1, 'Enter a value.')
	.check;

Решение с использованием модификатора:

import v9s from 'v9s';

function integer(value: string) {
  return /^[0-9]+$/.test(value);
}

function stringToNumber(value: string) {
  return Number(value);
}

const check = v9s
  .lte(100, 'The value is greater than 100.')
  .gte(10, 'The value is less than 10.')
  .use(integer, 'The value must be an integer string.', stringToNumber)
  .minLength(1, 'Enter a value.')
	.check;

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

Контекст

Большинство форм, имеющих сложность чуть большую, чем форма ввода логина и пароля, содержат в себе и некоторые условия. Скажем, для заполнения анкеты физическому лицу требуется ввести ФИО, тогда как юридическому - наименование организации. Как уже было сказано выше: v9s производит проверку единственного поля, а не всей схемы, но это не значит, что мы не можем использовать состояние всего объекта валидации для корректной проверки единственного поля. Представим следующую схему:

interface Form {
  isOrg: boolean;
  orgName: string;
  fullHumanName: string;
}

В зависимости от состояния флага isOrg мы должны разрешить или запретить оставить соответствующие поля пустыми.

// старотовое состояние объекта
const formData = {
  isOrg: false,
  orgName: '',
  fullHumanName: ''
};

// проверяем, что поле orgName не пустое, только если это юр. лицо
const checkOrgName = v9s
	.use((value: string, context: Form) => context.isOrg && value.length)
	.check;

// проверяем, что поле fullHumanName не пустое, только если это физ. лицо
const checkFullHumanName = v9s
	.use((value: string, context: Form) => !context.isOrg && value.length)
	.check;

checkOrgName(formData.orgName, formData); // returns: false
checkFullHumanName(formData.fullHumanName, formData); // returns: true

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

Интернационализация

Вместо строки с сообщением об ошибке, можно передать функцию, возвращающую строку:

enum Lang {
  en,
  ru
}

const lang: Lang = Lang.en;

// функция возвращает локализованный текст ошибки
function minLengthError() {
  switch (lang) {
  case Lang.ru:
    return 'Введите значение.';
  default:
    return 'Enter a value.';
  }
}

const check = v9s.minLength(1, minLengthError).check;

check(''); // returns: 'Enter a value.'
lang = Lang.ru;
check(''); // returns: 'Введите значение.'

Подготовка к публикации

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

v9s.use(integer).or(v9s.eq(true).boolean()).string().check;

Не слишком очевидно, что or() относится к string(), не так ли? Также среди встроенных правил были такие как min() и max() - абсолютно идентичные gte() и lte(), что противоречило поставленной цели, да и, вообще, не имеет смысла. Подобных недоработок накопилось достаточно, в основном, в силу того, что многие возможности просто не были востребованы нами. В результате, процесс написания модульных тестов вылился еще и в рефакторинг. Кто-то справедливо может заметить, что писать тесты нужно сразу же, но давайте будем честны: тесты отнимают время, и сразу ими покрывают, обычно, только критичные участки, а с v9s больших проблем в процессе эксплуатации у нас не возникало.

Предыдущий пример после рефакторинга; уже не вызывает приступов мигрени:

v9s.use(integer).string().or(v9s.eq(true).boolean()).check;

Далее пришла пора слегка облегчить жизнь пользователям IDE и добавить описания всего и вся: ох и нудная же работа. Многие не раз читали про самодокументирующийся код. Что ж, пока речь идет о внутреннем проекте, где все участники знают общую архитектуру и принципы именования, отсутствие комментариев, описывающих функции/классы/типы/константы/подставить нужное, можно пережить. Но как только вы хотите дать доступ к вашему проекту стороннему разработчику, то очень полезно дать ему подсказки, касающиеся публичного интерфейса, которые могут позволить ему, не открывая страничку с документацией (если она вообще есть), с ходу понять - что делает та или иная часть вашего проекта. И каждый раз, когда в IDE будет всплывать автоматическая подсказка не только с сигнатурой метода, но и с его описанием - разработчик будет мысленно благодарить вас за проделанную работу. По крайней мере, я всегда ценю такую заботу о пользователях. Написание этих комментариев отняло у меня два дня жизни - столько же, сколько я потратил на написание первого прототипа библиотеки.

Тут мне пришла в голову светлая мысль: проверить работоспособность полученного npm-пакета. Разочарование не заставило себя долго ждать: Node.js версии 14+ имеет встроенную поддержку модулей без каких-либо экспериментальных флагов. На выходе bili был ES-модуль, и такой модуль нельзя было напрямую использовать в проекте с CommonJS-модулями. Node.js требовала соответствующего типа модуля в .js-файле или CommonJS-модуля в файле с расширением .cjs. Попытка перенастроить bili так, чтобы в .js-файле лежал CommonJS-модуль привела к тому, что теперь результат нельзя было использовать в проекте с ES-модулями ("type": "module"). В конечном итоге, решено было заменить bili, который не умеет генерировать файлы с расширениями cjs и mjs, на rollup.js, фронтендом для которого и является bili.

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

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

Конечно, если делать "по уму", то есть по мере разработки писать тесты, комментарии и документацию, то процесс подготовки занял бы значительно меньше времени, но проблема в том, что никто не планировал изначально тратить на это ресурсы, ведь не было задачи опубликовать, а объяснить коллегам принципы работы библиотеки не составило труда.

Выводы

Если 20 лет назад сообщество было счастливо вообще самому факту наличия исходных текстов под свободной лицензией в открытом доступе (кто видел код XINE, тот над бананами Angular не смеется), то сейчас требования сильно возросли. Программистам-одиночкам стало гораздо сложнее представить собственные разработки общественности. Я уверен, что во время наших поисков готовой библиотеки, где-то там, в закоулках GitHub, было подходящее решение, но его автор не смог или не захотел оформить его подобающим образом. Что касается конкретно мира разработки JavaScript, то разнообразие даже среди таких базовых вещей как сборщики проектов, заставляет меня, как разработчика, слишком много времени тратить на непродуктивные задачи. Конечно, можно подготовить конфиг на все случаи жизни для любимого сборщика и использовать его во всех своих проектах, но, к сожалению, даже эта схема не работает. В один прекрасный момент мой шаблоный конфиг для Webpack просто перестал работать - оказалось, что вышла, на тот момент новая, 4 версия, несовместимая с 3. А потом на горизонте возник весь такой компактный и модный rollup.js... Хотел бы я сказать, что решение лежит в стандартизации, но все знают шутку про стандарты. Однако я точно знаю, что нам пора прекращать писать монструозные комбаны по типу all-in-one и пора начать серьезней относиться к зависимостям наших проектов. Шутки про размер node_modules - вовсе не шутки. Мы часто не думаем, что происходит под капотом, когда вбиваем npm i <package-name> и жмем Enter, а потом решаем опубликовать свой пакет. Я считаю, что нужно серьезней относиться к выбору зависимостей для проекта и чаще думать над архитектурой. Возможно, тогда мы сможем переиспользовать, комбинировать и расширять чужие решения с меньшим страхом сотворить чудовище Франкенштейна.

Я не знаю решения описанных выше проблем (может, кто-то знает?), как и не знаю переспектив для v9s. Cложно говорить о каких-либо перспективах столь малого проекта. Мне он не кажется идеальным, там есть над чем подумать в плане внутреннего устройства, но пока работает так. Также имеются мысли собрать в отдельный пакет наиболее полезные правила, как это сделали разработчики Toi. Буду рад любой конструктивной критике как проекта, так и статьи, и посильной помощи в поддержке. Аргументированное мнение на тему нужно/не нужно также приветствуется. Если кто-то захочет внести правки в документацию - также буду крайне благодарен. На этом, пожалуй, все. Сейчас в работе документация к моделям, о которых речь пойдет в другой раз.

U.P.D.: Хочется выразить благодарность пользователю @tzlom за указание на досадную ошибку проектирования, которая существенно ограничивала возможности решения. Теперь можно указать любой тип для сообщения об ошибке, кроме boolean и Function:

import v9s from 'v9s';

interface ErrorMessage {
  code: number;
  critical: boolean;
  text: string;
}

const check = v9s
    .lte<ErrorMessage>(100, { code: 1, critical: false, text: 'Too big' })
    .gte(100, { code: 0, critical: true, text: 'Too small' })
    .check;

check(20); // returns: true
check(5); // returns: { code: 0, critical: true, text: 'Too small' }
check(110); // returns: { code: 1, critical: false, text: 'Too big' }

Комментарии (25)


  1. pankraty
    21.08.2021 22:25
    -2

    v9s means validations

    But... Why??

    В случае с v8n, это хоть как-то можно обосновать созвучием - ValidEIGHTioN - лично мне такой способ именования кажется притянутым за уши, но тут уж на вкус и цвет. Но что такое Vi-nine-es и почему это means validation - загадка для меня.


    1. defaultvoice
      21.08.2021 22:31

      Потому что 9 букв пропущено


      1. pankraty
        21.08.2021 22:33
        +1

        О4ь. С5о з0а п7е.


        1. defaultvoice
          21.08.2021 22:37
          +2

          В4а п8а ;)


    1. TitaniumLexa
      21.08.2021 23:11

      Также как l10n и i18n являются сокращенной формой для localization и internationalization.
      А так скорее для пафосного названия.


  1. Vadiok
    22.08.2021 08:05

    Вопрос не совсем по теме. Почему вы решили перейти на Rollup.js, какие его преимущества перед Webpack'ом?


    1. Devoter Автор
      22.08.2021 08:52
      -1

      Rollup на выходе генерирует более компактный код.


  1. GerrAlt
    22.08.2021 10:37

    Возможно вкусовщина, но мне показалось не удобным читать цепочку снизу вверх - такая последовательность сборки правил чем-то обусловлена (кроме "так получилось")?


    1. Devoter Автор
      22.08.2021 11:04

      Соглашусь - так не слишком удобно читать. А причина довольно проста: это был самый простой способ внутренней реализации. Разворачивание цепочки происходит с конца, так так под капотом там односвязный список. Но, по опыту могу сказать, что привыкаешь к такому довольно быстро.


  1. tzlom
    22.08.2021 11:19

    Обещали "unix way" а оказывается что решение решает только ваши проблемы. Сама идея что результат проверки это true|false|string не правильная- я могу хотеть валидатор который выводит ошибку красиво, или отдаёт её через API который использует коды ошибок, можно придумать кучу вариантов когда ошибка будет чем угодно кроме строки.

    При этом решение тривиальное - бомж вариант true|any , тру 1337 вариант который добавит сложностей в типовом использовании - Valid | any. Найти унифицированное решение предлагаю вам (это можно сделать не испортив API)


    1. Devoter Автор
      22.08.2021 12:15

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

      Отобразить красиво или через API - это, пожалуйста, снаружи - ничего не мешает обернуть вызов. Что касается кодирования ошибки или какого-то более сложного типа, то да, в этом смысл, но не все так однозначно. Добавить тип number - быть может, было бы разумно (issues на гитхаб доступны для всех), а вот any - уже не так удобно, ведь это ломает смысл использования TS. Определить тип Valid как некоторый шаблонный тип, который, по дефолту равен true - можно, но тогда нужно еще требовать передавать внутрь либо функцию сравнения, которая будет возвращаеть булевый результат в виде валидно/не валидно - выполнение цепочки ведь должно быть управляемым, и мы хотим остановиться при первом невалидном результате, вернув именно его ошибку. Получается, что смысла делать положительную проверку отличной от true нет смысла, потому что положительная проверка - единственный результат, а вместо строки, да, можно использовать <T>, но тут, опять же, github открыт - будет запрос - будет результат какой-то. А обернуть положительную проверку в объект или что-то иное, опять же, можно и нужно снаружи.

      В более унифицированном варианте сигнатура у check должна получиться примерно такой, если я правильно понимаю ваши требования:

      type Error<T> = T extends boolean ? never : T;
      
      function check<T = string, E extends Error<T> = Error<T>>(value: any, context: any = {}): boolean | E;

      Можно и попроще сделать:

      function check<T = string>(value: any, context: any = {}): true | T;

      но тогда будет шанс того, что отличить результат с ошибкой (`true`) от корректного значения типа T (тоже true) будет нельзя.

      Не понимаю только, почему вы мне предлагаете искать какое-то еще решение? Если оно у вас есть - поделитесь. Если нет - мы не на экзамене, и у меня достаточно забот помимо. Имеющееся решение я описал, считаю вашу критику вполне объективной и буду рад вашему вкладу в улучшение решения.


      1. Devoter Автор
        22.08.2021 12:50

        Все-таки удалось вам моего червяка разбудить внутреннего. Поэкспериментировал, получилось вот такое решение, как наиболее адекватное:

        type Error<T> = T extends boolean ? never : T extends Function ? never : T;
        function check<T = string, E extends Error<T> = Error<T>>(value: any, context: any): boolean | E;

        Таким образом, если задать в качестве типа T boolean или Function - то мы увидим, что тип значения never. Потому что параметр message у нас опционален, и еще может быть фабрикой. А если мы message не указываем, то возвращает true или false.

        Правда, там возникает проблема с базовым экземпляром, но тут можно подумать.


        1. tzlom
          22.08.2021 14:52

          Я на самом деле не настоящий TS разработчик, поэтому я забыл что TS не умеет работать с типами, а только выводит и проверяет их, так что действительно "validated | any" смысла не имеет, хотя сама идея всё ещё та же.
          У Вас кстати та же проблема:

          check(value: any, context: any = {}): boolean | string

          Результат надо сверять с типом, т.к. "Failed" это вполне себе string который кастится в true

          Можно конечно прибегнуть к монаде Maybe но тут опять таки проблема что в TS я не встречал корректной реализации Maybe оформленной пакетом, плюс по сути нужна монада обратная Maybe

          Так что я быстро накидал аналог : Playground Link


          1. Devoter Автор
            22.08.2021 18:52

            По правде говоря, я - тоже не настоящий TS разработчик, так, периодически приходится. Что касается каста string в boolean, то он происходит, только если делать нестрогое сравнение (==), а строгое сравнение работает без приведения типа (===). То, что вы накидали по ссылке, я уже встречал, но мне такой подход не сильно нравится, и вот почему: мы создаем экземпляр класса на каждое правило, причем не разово, а при каждом вызове каждого правила. Если вспомнить, что вся цепочка - это проверка лишь одного поля, которая должна повторяться при каждом изменении значения, то штука выглядит довольно накладной. Вместо списка мы получили массив, что, в принципе, почти равноценная замена, даже имеет плюс в том, что можно легко выстроить цепочку привычным образом слева-направо. А вот реализация правил выходит уже не такой удобной - дополнительная фабрика, да и, внешние правила, получается, должны знать о ValidationResult, то есть появляется ненужная, как по мне, связь, ведь сами правила довольно просты. Впрочем, последний пункт можно попытаться обыграть, понятно, что это - набросок.

            Думаю, не сильно ошибусь, если скажу, что вы пишите на Haskell или Scala - от решения просто веет ФП. Не подумайте только, что я что-то против имею, просто, как мне кажется, несмотря на наличие всяких функциональных примочек в JS - он плохо переваривает чисто функциональные решения, map, forEach, reduce, и т.д. сильно медленнее for, и, полагаю, что для такой маленькой, но много раз вызываемой части лучше подумать немного об оптимизации. Впрочем, я тут сам хорош - вместо массива использую односвязный список и тоже создаю экземпляры Validator, но только одиножды - при создании цепочки.

            Посмотрел на ваше решение, взвесил за и против, и, пожалуй, не вижу большой проблемы в том, чтобы исключить типs boolean и Function из ErrorType. Субъективно, это - меньшее зло, чем генерировать массу экземпляров на каждый вызов.

            И, должен сказать спасибо за веское замечание об изначально неверном выборе `boolean | string` - уже переписал с использованием дженериков, завтра на свежую голову подправлю статью.


            1. tzlom
              22.08.2021 20:29

              Я не очень понял почему вашу цепочку можно создать один раз а мой валидатор надо пересоздавать каждый раз, они оба stateless так что проблем быть не должно. И разумеется это быстрее чем создавать их каждый раз. Мне кажется современные JS движки способны это нормально оптимизировать при помощи JIT (да и VM исполнение не должно быть слишком медленным тоже ). Так же можно оптимизировать это просто не вызывая валидатор чаще 10Гц - все равно физически на такое среагировать не успеешь. Да и бесит когда начинаешь например вводить email а форма говорит что он не валидный после первого же символа.

              Я на C++ пишу оптимизированную математику, и по опыту - баланс выразительность/оптимизированность надо загибать в выразительность- оптимизировать узкое место проще чем сделать код человекопонятным.


              1. Devoter Автор
                22.08.2021 21:01

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

                static DidPassed<T>(){
                	return new ValidationResult<T>(true)
                }

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

                Да и бесит когда начинаешь например вводить email а форма говорит что он не валидный после первого же символа.

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

                Я на C++ пишу оптимизированную математику, и по опыту - баланс выразительность/оптимизированность надо загибать в выразительность- оптимизировать узкое место проще чем сделать код человекопонятным.

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

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

                Позволил себе немного переписать ваш вариант таким образом, чтобы он больше соответствовал моему видению. Минус, по-моему, тут только в том, что нужно явно каждый раз передавать значение сообщения, уже не получится просто автоматом как true/false использовать.


                1. tzlom
                  23.08.2021 22:21

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

                  Строгий тип и простота (конечно это спорные утверждения). Ну и мне кажется в большинстве случаев я бы хотел сообщение об ошибке а не просто true/false , а трюки с === в API я не очень люблю - это не очевидно, люди склонны ошибаться в таких вещах и подобные ошибки не очень хорошо отлавливаются.

                  Я прогнал пару онлайн бенчмарков - браузер способен создавать миллионы подобных объектов в секунду (разумеется на телефонах будет медленнее, но нам нужно то пара сотен максимум), так что производительность не то чтобы проблема.


                  1. Devoter Автор
                    24.08.2021 00:51

                    Я тут тоже пару экспериментов поставил и получил в результате что-то вроде версии 2.0, которая как раз возвращает объект, как у вас, но делает это только в конце, и функцию, которая преобразует в тот вид, что был у меня ранее для тех случаев, когда удобно получать именно значение, и даже порядок правил теперь прямой без массива. Осталось документацию обновить. А что касается строгого равно, так в js это нынче норма, лично я его везде "по умолчанию" использую, как и const вместо let. Без оного и null от undefined не отличить, так что я бы сказал, что в мире js что-то вроде true | ErrorType - тоже норма, но я тут пораскинул мозгами и решил, что кто-то может ожидать именно строго один тип объекта, и преобразовать объект в смешение типов проще, чем наоборот. Так что ещё раз спасибо за разъяснения.


  1. user_man
    22.08.2021 15:00
    -1

    Так сказать, детям на заметку:

    Если на создание валидатора у автора ушла неделя, а потом созданным пользовались разработчики в течении 10 месяцев, то отношение затрат на библиотечку к общему ресурсу времени проекта получается катастрофически низким, что-то вроде день-два на человеко-год, в зависимости от количества разработчиков.

    Немного по другому - затраты в размере долей процента от общих некоторые (придумайте для них подходящее название) считают непозволительной роскошью. В результате весь проект годами несёт тяжкий груз бардака и массу неудобств, и всё из-за того, что эти "некоторые" считают, что лишние доли процентов затрат - это дорого!

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

    И каждый раз очередной молодой разработчик начинает понимать эти грабли только после нескольких подобных проектов. Это если вообще начинает. Ну и если всё же приблизится к пониманию, то выдаёт вот такие, как в статье, велосипеды, всего за неделю. Единственное, что удивляет - почему ему никто не настучал по рукам с криком "это дорого"? Видимо деменция сделала своё чёрное дело, атрофировала даже то, что уже и так было атрофировано, ну и возражать было "нечем". Как ни удивительно но только в таком случае можно ожидать прогресса. То есть: бардак = двигатель прогресса. А маразм - апостол его.


    1. Devoter Автор
      22.08.2021 17:12

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


      1. user_man
        23.08.2021 15:02

        а какую альтернативу вы бы посоветовали?

        1. Велосипеды строить нужно.

        2. Вариант, указанный вами в начале статьи как предпочтительный, считаю приемлемым приближением направления, которое ещё лет 5 может давать полезные плоды.

        3. Но язык должен быть Java, а не Java-Script. Как на сервере, так и на клиенте.

        Поскольку бизнес и глубина познания есть принципиально антагонистические явления (бизнес хочет только готовое), найти нишу для расширения глубин познания непросто. Плюс стандартная ситуация дома - жена, дети, кредиты, и все оптом хотят много денег. Отсюда следует, что скорее всего и вам тоже не стоит думать о глубинах и выбирать путь в эту сторону. Но если вы (самоуверенно) относите себя к исчезающе малому меньшинству и считаете, что все условия для вас сложились благополучно, тогда дерзайте и ставьте цель на срок более 5-ти лет. Правда тогда вам стоит забыть о фреймворках и прочей стружке из под примитивного (и тупого) сверла. Вдали вас ждут модели, генераторы, автоматический вывод и прочая computer science. Хотя там вы встретитесь с ещё одной проблемой - как объяснить обезьяне, что нужно сделать, что бы ей стало хорошо. И здесь даже дело не в нахождении способа обучения, а в том, что обезьяна сама не знает, что такое "хорошо".


  1. gameplayer55055
    22.08.2021 16:23

    Поэтому я предпочитаю писать свой мини фреймворк с типизацией и валидацией.

    Для опен морс проектов это идеальное решение, а от для хуяк и в продакшн время деньги, от и приходится искать библиотеки среди миллиона их


  1. eshimischi
    23.08.2021 02:38

    Есть вот такая вещь, с набором дополнительных разных инструментов - все в README - Zod , проще, чище, JSON поддержка и 9кб размер и тд. Плюс добавлю практическую задачу: валидация полей форм для Vue3 - Vee-Validate+Zod


    1. Devoter Автор
      23.08.2021 05:47

      Даже не знаю - что вам ответить, вроде как вся статья посвящена тому - почему такие решения, как Zod, не всем подходят, но за ссылку спасибо - буду иметь ввиду и эту библиотеку, однако размер пакета там не 9кб, а 470кб. А вот почему не подходят всякие vee-validate, я, вроде бы, достаточно ясно описал. Далеко не всегда удобно привязывать валидацию к представлению, для больших и сложных форм, состоящих из нескольких компонентов лучше делать это на уровне данных.

      По всей видимости, мыслю свою мне донести в статье не удалось - надо учиться внятней писать.


      1. eshimischi
        23.08.2021 13:21
        +1

        9кб в минифицированном виде имелось ввиду.. По поводу vee-validate повторюсь, я привел пример практического использования Zod в тандеме с валидатором форм. Про "удобство" вопрос спорный, но вступать в дискуссия сейчас не буду, ведь весь Ваш опыт с вашим проектом - это лично ваш опыт и ничей другой..