👀 Валидация в GoLang: где, когда и как?

👀 Валидация в GoLang: где, когда и как?

Forzend

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

Многие используют go-playground/validator, кто-то пишет проверки руками.

Кто-то делает метод Validate и валидирует всё в слое представления, кто-то где-то ещё...

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

Но начнём с Domain Model... А именно Value Object и primitive obsession. Кажется, я прикрепил достаточно ссылок, чтобы не пересказывать, о чем это.

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

Следующее, о чем хотелось бы сказать, подход Always Valid Domain Model. Как понятно из названия, этот подход говорит о том, что у нас не должно быть возможности создать невалидный доменный объект, в том числе и Value Object, который является составной частью Domain Model (Хотя у нас может агрегат проверять всё состояние, это приводит к большому дублированию кода, а так же не очень надежно, позже вы поймёте, почему).

Я правда в восторге с этого подхода. Почему? Потому что имея на руках в любом месте моего кода Value Object я уверен на 100% в том, что объект валиден. Мне не нужно держать в голове, что нужно вызвать метод, аля Validate или прогнать объект через go-playground/validator (ну или вызывать Validate несколько раз). Я вас уверяю, рано или поздно кто-нибудь забудет вызвать Validate и в системе обнаружится баг.

Как достичь этого в GoLang? До безобразия просто: все поля модели приватны и меняются с помощью методов, которые и производят все проверки. Фабрика (NewValueObject) так же проверяет всё перед созданием объекта.

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


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

Бытует мнение, что инварианты — проверки со стороны мнения, которые должны проверяться Domain Model, а валидация, это системные проверки, которые делаются на другом уровне.

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

Я предпочитаю все проверки держать в Value Object.


Я так же наблюдал подход, когда все проверки делались в слое представления. Я от этого подхода так же отказался. Почему? Есть у меня "лайфхак", которому я всегда следую, когда принимаю решение о том, какая логика должна попасть в слой представления. Я задаю себе очень простой вопрос: "Если мы меняем этот интерфейс (пусть сейчас у нас REST) на какой-то другой (например gRPC), не придётся ли нам писать всё то же самое?". Я понимаю, что на практике интерфейс сервиса меняется так же часто, как и база данных (привет ORM и интерфейсам репозитория), однако этот подход помогает мне разделять ответственность слоёв.

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

И так, как это всё выглядит в коде, если собрать это всё вместе.

Есть слой представления (handlers), который достаёт примитивные типы из запроса, создаёт Value Object (если фабрика вернула ошибку, мы её обрабатываем и отдаём пользователю красивую (или не очень) ошибку).

Дальше, слой сервиса принял Value Object (именно для того, чтобы иметь возможность передать отдельный VO, а не весь агрегат, VO сами держат себя валидными. Это полезно, например, при поиске по ID). Имея на руках VO мы уже не думаем о проверках и просто пишем логику приложения.

Проблема, с которой я столкнулся на данном этапе... Уродство. Создание Value Object внутри слоя представления (или внутри слоя сервисов, тогда я попробовал оба варианта, в том числе и принятие примитивов в сервис и создание VO на его уровне). Решение этой проблемы настолько же примитивно, как и всё, о чем мы здесь говорим. Я создал структуру парамметров для каждого метода сервиса. Структура состоит из VO, имеет фабрику, принимающую примитивные типы. Таким образом хендлер вызывает фабрику и полученную от него структуру передаёт в сервис.

Вот простенький набросок того, как это выглядит в (псевдо)коде.

Report Page