Pull to refresh

Comments 24

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

Чисто философски - параметр валидации должен отвечать на один вопрос - «валидна ли вся модель или нет?». Это на случай, если возникнет желание использовать для одних случаев валидации true, а для другого false - везде использовать true для валидных параметров, а false для невалидных параметров.

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

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

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

А про кэш - проблем быть не должно, если он одноразовый - закончилась обработка запроса - кэш забыт.

А про кэш - проблем быть не должно, если он одноразовый - закончилась обработка запроса - кэш забыт.

Я примерно это имел ввиду:

Без кэша
  1. Проверить валидность.

С кэшем
  1. Сформировать ключ для кэша. Так как библиотека абстрактная, в имени ключа будет присутствовать вся-вся информация, которая доступна: и само значение, которое, нужно перегнать в строку, если это не строка (проверка/сериализация добавит ещё несколько операций и отожрёт ещё немного больше вычислительных ресурсов), и функция-валидатор, которую придётся тоже сериализовать для участия в ключе кэша, иначе значение «привет», которое проверяется на длину строки перепишет кэш проверки для значения «привет», которое проверяется на наличие русских букв.

  2. Проверить наличие ключа в кэше.

  3. Не нашлось — проверить валидность.

  4. Записать в кэш.

в имени ключа будет присутствовать вся-вся информация

Можно попросить вас привести пример? Я не понял, как это может выглядеть.

А так в целом выглядит неплохо.

Встречный вопрос: как будет выглядеть запись в кэше валидации для проверки слова «привет» на то, что оно длинее 1-го символа и короче 10-ти — по вашему мнению?

Если данное валидно, то ничего не записано, а если не валидно, то строка с текстом, например: «Параметр (такой-то) должен быть длиннее 1 символа и короче 10. В данный момент его длина: (Len)»

в имени ключа будет присутствовать вся-вся информация, которая доступна

Но мой вопрос был не о тексте сообщения. Вы сказали слово «ключ» в который вы собрались записать «всё-всё». Хотелось бы увидеть пример этого «всего-всего» в ключе?

Строка с текстом, которую вы описали — это то, что я называл ключом кэша. Вы как раз описали ситуацию, когда для генерации этой строки используется вся-вся доступная информация: и параметр и то, как он валидируется. К слову, я с самого начала перечислял, что я имел ввиду под «вся-вся» (если перечитаете ещё раз, то увидите). Так что, мой пример «ключа» от вашего примера «строки с текстом» не отличается.

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

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

type Test = (value: any) => string;

class Field {
  test: Test;
}

class Form {
  fields: Record<string, Field>

  constructor(testsObj: Record<string, >) {
    for (const key in testsObj) {
      this.fields[key] = new Field(testsObj[key])
    }
  }
}

const form = new Form(convertSchemaToTestFuncs({
  name: d.string()
}))

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

Пример convertSchemaToTestFuncs для desy, для yup

В этом примере у вас сильная привязка к представлению данных через формы HTML и к TypeScript. У меня есть иерархичные данные с кириллицей в ключах. По поводу иерархии, например, название ключей:

{
круг: {радиус:5, материал: "анаптаниум"},
прямоугольник: {длина, ширина, плотность},
производитель: "ооо пандора"
}

Это, чтобы не вводить частности, если это описание какой-то детали.

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

зависит от того как мы получаем эти данные.

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

Если каким-то другим образом(например из пользовательского JSON) то такой функциональности сейчас не предусмотрено(да и вряд ли кому-то понадобиться). И самое простое что приходит в голову это следующий код:

const data = getData()

const mySchema = {
  'круг': d.object({
     'радиус': d.number(),
     'материал': d.string()
   })
}

const errors = Object.entries(mySchema).reduce((errors, [key, schema]) => {
  const error = schema.validate(data[key])
  if (error !== '') {
    errors[key] = error
  }
  return errors
}, {})

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

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

Но выделю несколько пунктов которых в ней не хватает лично мне:

  1. Производительности при нахождении ошибок. $mol_data выбрасывает исключение, что сказывается на его performance. Ну и как я написал выше это не совсем правильно с моей точки зрения - валидация данных ожидаемая нами операция, а такие действия не должны выбрасывать исключения за исключением(извини за повторение) непредусмотренных проблем

  2. Не хватает строгости проверок по умолчанию. Я бы хотел видеть максимально строгие проверки, что пока не реализовано в mol. Например в desy по умолчанию строка проверяется на то что она не пустая, а в $mol_data нет, desy в объекте могут быть только известные ключи, а в $mol_data нет

  3. Не хвататет удобств к которым привык в yup и zod. Например проверки что значение всегда true или расширяемости кастомными проверками(возможно $mol_data_pipe можно использовать для этого, но мне показалось что он про преобразование типов)

  4. Наверно самая большая проблема для меня - понятность кода. В моей desy эта цель стояла на первом месте. Скажу честно код $mol_data выглядит немного космически со всеми фишками из mol-а. При этом mol кажется для меня довольно сложной экосистемой, а эта библиотека связана с ней довольно сильно.

Существует вот такой обширный замер производительности большинства известных библиотек для валидации. Самые быстрые (например typebox) используют кодогенерацию. Насколько я понимаю к $mol он не имеет никакого отношения, кроме схожести названий https://moltar.github.io/typescript-runtime-type-benchmarks/

Так же вот тут можно найти ссылку на дипломную работу, в которой описывается создание библиотеки для валидации https://valibot.dev/guides/introduction/

  1. Самая быстрая всё же suretype, но она тяжёлая и делает eval.

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

  3. Довольно странно требовать от всех строк быть не пустыми. Тем более по умолчанию. А какая разница что там в неизвестных ключах? Ключей может быть 100500, а приложение работает только с 5 из них. Зачем ему падать, когда меняется состав ключей, которые ему не интересны?

  4. Кастомный парсер - это любая унарная функция ($mol_data_value). Вот прям совсем любая, хоть String, хоть parseFloat, хоть даже Date. $mol_data_pipe нужен, чтобы собрать один строготипизированный парсер из цепочки парсеров, где выход одного парсера идёт на вход следующего.

  5. Хз, что там за сильная связь такая. В npm либа на 2кб без зависимостей.

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

  1. Асинхронная валидация, да та самая, которая редко используется. Рано или поздно в больших приложениях вы с ней столкнетесь

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

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

  4. Валидация одного значения в зависимости от других других

  1. Можете привести пример когда может потребоваться асинхронная валидация? В голову приходит только работа с DB при серверном сценарии, но такую проверку обычно делают на отдельном уровне бизнес логики, а не при первоначальной валидации данных. Пока кажется что если потребовался асинхронный запрос - то это явный сигнал что проверка связана с логикой, а не форматом данных(например занято ли имя), а такие проверки я бы делал не через библиотеку

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

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

const addressSchema = d.object({
  street: d.string()
})
const user = d.object({
  name: d.string()
  adress: addressSchema
})
  1. Напишите пожалуйста каким-бы вы хотели это видеть? Для себя подобного рода задачи я решаю через кастомные правила c помощью .test()

  1. Пользователь выбирает необходимую ему дату. Требуется проверить является ли выбранный день рабочим, т.е не выходной, и не приходится на праздники. За это отвечает отдельный микросервис, которым пользуются все формы.

  2. Это не неявное преобразование, если вы работаете с нормальным моделями данных, то у вас даты должны лежать не строками, а объектом Date. С Бека приходит строка, формат строки задаётся спекой. На входные данные конечно можно закрыть глаза(нет), а вот выходные данные придется форматировать

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

  1. Валидация одного значения в зависимости от других других

А как при это можно поступить: все участвующие параметры становятся/признаются невалидными или невалидным признаётся только исходный проверяемый параметр? И требуется ли дополнительная валидация участвующих параметров перед такой многопараметровой проверкой?

Как в доке описывается zod:

Zod is a TypeScript-first schema declaration and validation library. <...> The goal is to eliminate duplicative type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type. It's easy to compose simpler types into complex data structures.

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

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

Если ты зачем-то хочешь тот же zod, но более оптимизированный (как будто в этом основная проблема твоего приложения), то стоит, скорее, смотреть в сторону Valibot. Но и то, это лишнее, жертвуем DX, чтобы сэкономить пару миллисекунд.

  1. Вы кажется не учли секцию где я описывал работу с ts типами( Infer ). В цитате который вы привели говорится о:

  • валидации - как понимаете она сейчас есть

  • генерации ts типов на основании схемы - она тоже есть. В статье это Infer , в библиотеке это InferDesy

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

  1. Benchmark, который вы привели, замеряет основную функцию библиотек - валидацию данных, zod позиционирует себя как TypeScript-first schema validation with static type inference и я использую его как валидатор в runtime, при этом если бы в zod была возможность валидировать данные без преобразования типов - я бы воспользовался ей для замеров. Если хотите могу в benchmark добавить и Valibot, но если правильно помню после беглых замеров он немного быстрее zod

  2. В тексте нет утверждения, что я сделал тот же zod, я не собирался повторять весь его набор функционал, расшифровка названия библиотеки намекает desy - Dead Extraordinary Simple Yup . Я написал библиотеку которая построена на достаточно простых идеях валидации и в силу этого содержит минималистичный код в кодовой базе, при этом имея хорошую производительность

В приведенной цитате из доки, когда говорится о "static type inference", имеется в виду как раз то, что Вы называете, "преобразование типов в runtime", а не z.infer<typeof userSchema>.

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

Но Вы не правы, если считаете, что не это, а что-то другое, является основной фишкой zod.

"Проброс ошибок является операцией которая должна сообщать о непредвиденной работе приложения. " - Не обязательно, те же самые async await к примеру (можно долго спорить). Это делает валидацию немного удобней (когда начнется валидация сложных объектов, строки дают сбой и просто не поймете где ошибка/ошибки). Можно использовать объекты со статусами, переменные передаваемые по ссылке, использование логера ошибок и прочие подходы. Бытует мнение что try catch медленнее работает (скорее всего так, но тут вопрос объема).

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

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

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

Потому кому-то весь этот кал придётся отлаживать. А это может произойти, скажем, лет через пять, когда у этого юпа поменяется схема. Сиди потом ищи как сделать валидацию...

Уважайте коллег.

Sign up to leave a comment.

Articles