Как стать автором
Обновить

Комментарии 33


Чудесный пример безалаберности разработчиков. Кому-то показалась хорошей идея затащить в библиотеку валидации все варианты номеров телефонов со всего мира — довольно бесполезная штука в свете существования NAT от мира телефонии — добавочных номеров.

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

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

Есть конечно разные способы а-ля «удаление гланд через задний проход» (Object.defineProperty внутри Object.defineProperty), но они имеют свои проблемы. Ну и кстати говоря, валидация отдельной функцией, как у вас в статье — это тоже «гланды». Идиоматичная валидация не должна требовать каких-то сторонних вызовов, а тупо отрабатывать на set.

ЗЫ: Другие декораторы — очень полезная штука. Property decorator — увы.

Не понял в чём проблема объявить свои get/set обработчики, которые выполняются всегда на экземпляре?

Они перекроют отдекорированное поле. Вы можете либо оставить поле, но тогда вам get/set придётся заводить на другое имя, либо вы заведете get/set на это же имя, но тогда всё, что там изначально было — помрёт. А быть там может многое, не только просто поле, если, например, декораторов было несколько.

Короче, функциональное программирование на других декораторах вполне норм, и выполнение кода, если не делать сайд-эффектов и прочих резких движений, приобретает вид decorateA(decorateB(decorateC(proto))). C property decorator — хренушки. Только вон как в статье, с неявными соглашениями относительно того, что будет лежать в и извлекаться из метаданных Reflect. Понятно, что ни о какой совместной работе разных либ при таком подходе не может идти и речи.

Ничего не понял. Вам на вход приходит дескриптор, у которого есть get/set — просто дёргаете их для получения/установки значения.

Пример с кодом можно?
class Foo extends Object { 

  @log
  get bar() { return 5 }
  set bar(val: number) { }

  [Symbol.toStringTag]: string

}

function log<
  Host extends object,
  Field extends keyof Host,
  Value extends Host[Field]
>(
  host: Host,
  field: Field,
  descr: TypedPropertyDescriptor<Value>
) {

  return {

    ...descr,

    get(this: Host) {
      const val = descr.get!.call(this)
      console.log('%s.%s => %s', this, field, val)
      return val
    },

    set(this: Host, val: Value) {
      console.log('%s.%s <= %s', this, field, val)
      descr.set!.call(this, val)
    },

  }
}

const foo = new Foo
foo[Symbol.toStringTag] = 'foo'

foo.bar
foo.bar = 7
foo.bar

Playground Link

Скажите, а как так получилось, что я вам несколько постов пишу про недостатки property decorator, а вы якобы в ответ пишете пример с accessor decorator?
Я, собственно, поясню, в чем тут проблема — декораторы удобны в первую очередь из-за того, что они крайне лаконичны. Если лаконичности нет — то можно и «руками» обернуть классы и всё остальное — выйдет длиннее, зато очень очевидно.

Поэтому когда вы предлагаете вместо
public bar = 5;

написать геттер, сеттер, и приватное хранилище значения bar — то теряется солидная часть смысла использования декораторов, теперь с тем же успехом можно написать
  get bar() { return logGet(this._bar) }
  set bar(val: number) { this._bar = logSet(val) }

И выйдет уже даже не длиннее.

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


Ваш второй код можно переписать так:


@log @mem
get bar() { return 5 }
set bar(val) {}

Заодно, бесплатно получаете ленивую инициализацию.


Но ещё практичней, конечно, вообще использовать методы:


@log @mem
bar( val = 5 ) { return val }

Тогда сможете ещё и легко делегировать:


@log @mem
lol( val? : number ) {
    return this.bar( val )
}
Хотя вы меня натолкнули на идею — если делать Object.getOwnPropertyDescriptor(), разбирать ответ, переопределять имеющееся свойство «вглубь» (под какой-нибудь символ, например), и оборачивать его в get/set декоратора — можно, наверное, зафигачить всё так, чтоб оно снаружи выглядело как код без сайд-эффектов.

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

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

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

Вот тут не очень понятно.
Аттрибуты в .NET же это просто кусок метаданных.
Они сами по себе ничего не делают.
Они даже приметивнее чем декораторы в TypeScript.
Там точно так же надо извращатсья с рефлексией как у вас в статье, чтобы осуществлять валидацию через аттрибуты.
Или я вас не правильно понял?
может быть, я что-то сам запамятовал про .NET. мне казалось, что там все равно больше возможностей доступа к объекту, но возможно вы правы. не проверял к сожалению
Идиоматичная валидация не должна требовать каких-то сторонних вызовов, а тупо отрабатывать на set.

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


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


Каноничный пример убогости property validators: дан объект "дата", у которого есть изменяемые свойства "день", "месяц" и "год" — попробуйте сделать валидацию этих свойств на property validators, чтобы 1) никаким изменением свойств нельзя было выставить дату "2020-02-31" и 2) при изменении даты с валидной "2020-01-31" на валидную "2020-02-01" пользователь не получал бы ошибку валидации, если он вдруг начнёт с изменения месяца.

Нужно будет либо добавлять некие «транзакции» (т.е. фактически конструктор/билдер/фабрику), либо объект в какой-то момент окажется в промежуточном невалидном состоянии

Эм. А если у вас валидация отдельно — оно как-то иначе будет, что ли? Точно так же и будет: провели валидацию в какой-то неправильный момент — получили облом (а забыли и не провели — тоже облом).

Всё те же транзакции, просто теперь про них думать нужно пользователю объекта.

ЗЫ: Валидация, кстати, не обязана делать throw, если ей что-то не нравится.
А если у вас валидация отдельно — оно как-то иначе будет, что ли?

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


Всё те же транзакции, просто теперь про них думать нужно пользователю объекта.

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


провели валидацию в какой-то неправильный момент — получили облом

Правильный момент для валидации данных — это либо непосредственно перед отправкой данных (при нажатии кнопки submit), либо сразу по приёму данных. Вызов функции валидации в эти моменты вполне естественнен, его легко контролировать и трудно забыть.


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


Валидация, кстати, не обязана делать throw, если ей что-то не нравится.

Ну да, я поэтому и написал про "функцию, возвращающей список ошибок валидации". С сеттерами трудно такое сделать прозрачно.

Мы позволяем объекту быть в невалидном состоянии

Это делается и с сеттерами — заводите в объекте флажок invalid, и наслаждаетесь. Или вообще коллекцию, в которой храните список Error. Как угодно. Разница тут — именно во внешнем API. И я продолжаю утверждать, что внешний API с отдельной функцией валидации — кривой.

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

Это неплохо, если у вас формочки, которые вы проверяете на submit. Формочки — не у всех, да и в любом случае, нормальные люди прекратили проверять формочки только при submit еще в 2010 году. Ждать, пока конечный пользователь нажмёт на кнопку, и только потом радостно говорить ему «ага! ты всё неправильно ввёл!» — это моветон и лишние потери времени на повторную навигацию по форме. Если вы продолжаете так делать — вы плохой фронтэндер, совершенно не заботящийся об UX.

А с сеттерами пользователю придётся думать «а если я поменяю вот это свойство, не получу ли я ошибку валидации?» и «а если после ошибки я поменял другое свойство, первая ошибка пропала или нет?».

Именно поэтому объект, который предполагается изменять транзакциями — должен ими и изменяться. Скажем, изменять у даты отдельно день, месяц, и год — не имеет смысла.

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

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

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

В 2020 году это как раз таки первостепенный сценарий. И вешать общий validate() на onchange конкретных полей и выполнять полную валидацию всего в ответ на изменения чего-то одного — отличный рецепт к созданию тормозов на ровном месте.
заводите в объекте флажок invalid

Во-первых, запуск пересчёта этого флажка в каждом сеттере — это тоже самое, что вызов validate() на каждое изменение. Потому что если я поменял свойство x.foo на невалидное, этот флажок взведётся, а если я после этого поменяю свойство x.bar на валидное, то в сеттере bar придётся решать: "опа, у нас тут invalid стоит, сбрасываем или где-то в другом месте осталась ошибка?" И я либо сбрасываю флаг (что неверно), либо бегу опрашивать все свойства "как вы там? можно уже флаг сбрасывать?"


Во-вторых, на кой ляд мне в классе дополнительные поля, не имеющие отношения к предметной области? А если в моём классе уже есть бизнес-свойство Invalid? Искать другое имя? Решение с флагом — инвазивное, заставляет меня менять класс. Функция же валидации может запросто быть внешней к классу, она вообще может динамически подтягиваться из какого-нибудь менеджера валидаций со сложными правилами ("если VIP-клиент, то валидируем так, иначе валидируем эдак"). Я легко могу построить пайплайн валидаций, пропуская объект через разные rules, и мне не надо менять в объекте ничего, его можно держать чистым от левых метаданных.


Формочки — не у всех

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


И вешать общий validate() на onchange конкретных полей и выполнять полную валидацию всего в ответ на изменения чего-то одного — отличный рецепт к созданию тормозов на ровном месте.

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


Но проблема в том, что сегодня у вас простые объекты, и вы решили выбрать property validation как наиболее простую и популярную модель, а завтра правила усложнятся и вам придётся всё это выбрасывать и делать нормальные валидаторы.


Поэтому лучше сразу делать правильно и никогда не иметь этих проблем. Функция-валидатор по сложности одинакова или даже проще проперти-валидаторов, так как ей не нужны никакие библиотеки декораторов, логика проверок находится в одном месте и написана на том же языке, что и прочий код, а не на мета-надстройке, которую нужно дополнительно учить, транспилировать и проч. Если хочется DSL, то есть fluent-syntax, который легко совмещается с обычным синтаксисом. Если нужно временно поменять валидацию — просто пишешь ещё одну функцию, не трогая сам объект и существующий код валидаци; в отличие от метаданных, вызов имеющейся функции легко поменять на экспериментальную, легко вернуть обратно, легко сделать юнит-тест, легко сделать A/B-тест.

Во-первых, запуск пересчёта этого флажка в каждом сеттере — это тоже самое, что вызов validate() на каждое изменение.

Зависит от деталей реализации. Всегда можно хранить не один флажок, а пачку.

Во-вторых, на кой ляд мне в классе дополнительные поля, не имеющие отношения к предметной области? А если в моём классе уже есть бизнес-свойство Invalid? Искать другое имя?

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

Главный профит относительно валидации «сбоку» через допфункцию — отсутствие возможности забыть провалидировать, или провалидировать в неправильный момент с точки зрения транзакций.

Функция же валидации может запросто быть внешней к классу, она вообще может динамически подтягиваться из какого-нибудь менеджера валидаций со сложными правилами («если VIP-клиент, то валидируем так, иначе валидируем эдак»).

Это вообще ничего не меняет. Говорю же, мы ведем разговор о внешнем API некоторой модельной библиотеки. Под капотом у неё можно быть всё, что угодно, в том числе и «менеджер валидаций со сложными правилами».

Если у вас зависимые свойства, то у вас нет выхода, кроме как валидировать объект целиком.

Разумеется нет. Возможны оптимизации — кому, как не самому объекту, знать о том, что конкретно от чего у него зависит? Если у вас сторонний validate() — там да, оптимизировать тоже можно, но придётся в этом стороннем validate() как-то получать информацию о зависимостях, или хардкодить её.

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

YANGI.
Ваш тезис — это банальный путь в никуда, когда любители «сразу делать правильно» начинают городить схемы и абстракции на хайлоад и миллионы пользователей, хотя на самом деле пользователей у них 5, и в будущем могло бы быть 50, если б они не разорились, неспешно делая правильнейшую архитектуру вместо фич.

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

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

Это если у вас простая дубовая модель, по которой можно гонять validate() без потерь производительности. Если у вас что-то сложное — опа, нужно устраивать оптимизации, то есть либо хардкодить в этом вашем «одном месте» знания о модели (которая находится в другом месте), либо передавать из модели метаданные — привет, «мета-надстройка».
Всегда можно хранить не один флажок, а пачку.

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


кому, как не самому объекту, знать о том, что конкретно от чего у него зависит?

Если речь о бизнес-объекте — да, это часто так. Но там речь идёт о валидности в контексте бизнес-логики.


Если же речь о DTO — то нет, валидность DTO зависит от контекста. Один и тот же DTO может быть валидным в контексте одной системы и невалидным в контексте другой. Например, если ваш объект принимается из веб-формы, потом сохраняется в БД, а потом экспортируется во внешнюю систему, то финальное ограничение на длину поля — это комбинация ограничений от БД (например, там длина 32 символа) и от внешней системы (например, там ограничение 16 символов). Добавляете экспорт в третью систему — накладывается ещё одно ограничение. Если валидация прибита к классам, вам нужно вручную высчитать максимальную длину поля с учётом всех ограничений, поменять метаданные класса, захардкодить там @maxLength(16) и потерять информацию о том, по какой причине макс. длина поля именно 16 символов, а не 32. Если валидация в функциях и вам надо добавить ещё одну комбинацию ограничений поверх существующих — вы просто добавляете ещё одну функцию в пайплайн, не меняя ничего, и вы всегда точно знаете, какая подсистема какие ограничения накладывает, и можете легко логгировать, какое именно правило стрельнуло.


придётся в этом стороннем validate() как-то получать информацию о зависимостях, или хардкодить её.

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


начинают городить схемы и абстракции

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


Это если у вас простая дубовая модель, по которой можно гонять validate() без потерь производительности. Если у вас что-то сложное — опа, нужно устраивать оптимизации

Если правила валидации простые, то валидация на сеттерах не даёт никакого преимущества перед функцией полной валидации — всё летает.


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


либо хардкодить в этом вашем «одном месте» знания о модели (которая находится в другом месте), либо передавать из модели метаданные — привет, «мета-надстройка».

См. выше — так как правила валидации данных зависят от принимающей системы, то и кодироваться они должны в принимающей системе. В гомогенной среде вроде JavaScript передать эти знания с сервера на клиент легко — просто делайте функции, которые работают как на сервере, так и в браузере, и используйте их на обоих сторонах. В гетерогенной среде (скажем C# и JS) придётся написать два набора функций на двух языках, но это намного проще, чем искать две похожих библиотеки для валидации на метаданных.

Могу дополнить про декораторы свои наблюдения, для тех кто их пишет:
1) Если декоратор допускает использование его без аргументов — не поленитесь написать реализацию, чтобы можно было писать и так @SomeDecorator и так @SomeDecorator()
2) Будьте внимательны при реализации декораторов для методов, которые могут быть асинхронными — это особая магия.

Привет декоратору LogTime
  1. Декораторы и так-то не простые штуки. Не надо их ещё более усложнять разными сигнатурами. Лучше если будет одна понятная сигнатура. Вариант со скобками может быть легко расширен в будущем.
  2. А как их реализуют? Детектируют асинхронную функцию и заворачивают в асинхронную обёртку с эвейтом на оригинальной?
спасибо! насчет асинхронности думал, но пока не проверял. спасибо за коментарий.
Не понимаю как вы получили функцию внутри класса, у меня TS ругается
declare var Decorator: any;
class Lol {
  @Decorator
  function kek() {
    // Это же не 
  }
}
да, это был ошибочный копи/паст пример. исправил. спасибо!
Декораторы нельзя назначить на функции из-за function hoisting. В какой момент должен сработать декоратор, в начале или когда код доходит до определения функции?

К тому же, декоратор на функцию легко делается через обертку
const myFuncion = Decorator(() => { .... })

Только у функции при этом теряется имя, поэтому приходится писать как-то так:


const myFuncion = Decorator(function myFuncion() { .... })
да, спасибо! к сожалению это не так легко и лаконично, как обычные декораторы.

Декораторы это классно. Единственное смущает что как я читал в свое время включение в стандарт задерживается спором по одному из типов декораторов который в стандарт предлагают ввести совсем не в том виде в каком мы его сейчас применяем. Ну и по reflect-metadata сразу наткнулся но одну существенную проблему. Я не могу получить список полей класса для которых определены метаданные. А это означает что многое из функционала становится невозможным к реализации. Да я могу получить список полей объекта. Но если поле отсутствует в объекте я его никак не могу получить из метаданных. Например я создаю декоратор что поле является обязательным. Но никак не могу до него достучаться средствами рефлексии. По это у поводу было даже issue в reflect-metadata которое было закрыто с формулировкой что в проекте стандарта нет и в библиотеке не будет

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Публикации

Истории