Разделение ответственности

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

В классических подходах валидация часто «расползается» по приложению: часть правил оказывается в компонентах интерфейса, часть — в API-контроллерах, часть — в утилитах. Это приводит к дублированию логики и усложнению поддержки.

Vest предлагает иной подход: валидация оформляется как независимая декларативная система, представляющая собой набор тестов (suites), которые выполняются над входными данными.

Ключевой объект — suite, внутри которого описываются правила проверки:

import { create, test, enforce } from 'vest';

const loginSuite = create((data = {}) => {
  test('email', 'Некорректный email', () => {
    enforce(data.email).isEmail();
  });

  test('password', 'Пароль слишком короткий', () => {
    enforce(data.password).longerThan(5);
  });
});

В этой модели отсутствует зависимость от UI: suite ничего не знает о том, где будут показаны ошибки и как именно пользователь взаимодействует с формой.

Изоляция доменной логики

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

Это позволяет:

  • переиспользовать одну и ту же suite в разных интерфейсах;
  • применять валидацию на клиенте и сервере без изменений;
  • тестировать правила отдельно от UI-слоя;
  • избегать дублирования логики между фронтендом и бэкендом.

Пример использования одной suite в разных контекстах:

const result = loginSuite({ email: 'test@mail.com', password: '123456' });

if (result.hasErrors()) {
  console.log(result.getErrors());
}

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

Тестовый подход как основа структуры

Vest использует терминологию тестов для описания валидации. Каждый test — это атомарная единица проверки, что усиливает декомпозицию логики.

test('username', 'Имя пользователя обязательно', () => {
  enforce(data.username).isNotEmpty();
});

Каждый тест отвечает только за одну конкретную проверку. Это ограничение играет ключевую роль в архитектуре:

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

Такой подход формирует структуру, в которой validation suite становится аналогом набора unit-тестов, выполняемых не над кодом, а над входными данными.

Разделение слоёв: данные, правила и результат

В Vest можно выделить три независимых слоя:

1. Данные (Input Layer) Это входной объект, содержащий значения, подлежащие проверке. Он не модифицируется внутри suite.

2. Правила (Validation Layer) Набор деклараций через test, enforce, группировки и условия выполнения.

3. Результат (Result Layer) Объект, содержащий ошибки, статус валидности и вспомогательную информацию.

const result = loginSuite(data);

result.hasErrors();
result.getErrors('email');
result.getAllErrors();

Разделение этих уровней обеспечивает отсутствие побочных эффектов: suite не изменяет данные и не зависит от состояния приложения.

Независимость от UI-слоя

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

Это позволяет интегрировать одну и ту же валидационную логику в различные UI-архитектуры:

  • React
  • Vue
  • Angular
  • серверные API
  • CLI-инструменты

Пример использования в UI-логике:

const result = loginSuite(formState);

setErrors(result.getErrors());

Валидация при этом остаётся полностью независимой и может существовать вне жизненного цикла компонента.

Группировка и контекст как средство разделения ответственности

Vest вводит механизм групп (group), который позволяет логически разделять наборы проверок внутри одной suite. Это особенно важно для сложных форм и сценариев с условной логикой.

import { group } from 'vest';

group('auth', () => {
  test('email', 'invalid email', () => {
    enforce(data.email).isEmail();
  });

  test('password', 'invalid password', () => {
    enforce(data.password).longerThan(8);
  });
});

Группы позволяют:

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

Таким образом, одна suite может содержать несколько независимых поддоменов логики.

Условное выполнение и декомпозиция логики

Разделение ответственности усиливается за счёт механизмов условного выполнения: skip, only, динамические проверки.

test('companyName', 'required', () => {
  if (!data.isCompany) return;

  enforce(data.companyName).isNotEmpty();
});

Или через встроенные механизмы управления:

import { skip } from 'vest';

if (!data.email) {
  skip('email');
}

Такие инструменты позволяют разделять:

  • обязательную и условную логику;
  • независимые сценарии проверки;
  • контекстные правила.

В результате validation suite не превращается в монолитный блок условий, а сохраняет структурированность.

Асинхронные проверки как отдельная зона ответственности

Отдельное направление разделения ответственности связано с асинхронными проверками, такими как запросы к серверу.

test('email', 'email already taken', async () => {
  const exists = await api.checkEmail(data.email);

  enforce(exists).isFalsy();
});

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

Это разделяет:

  • локальные проверки формата данных;
  • удалённые проверки бизнес-ограничений;
  • инфраструктурные запросы.

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

Переиспользование как следствие изоляции

Чёткое разделение ответственности приводит к тому, что validation suite становится переиспользуемым модулем. Он может быть:

  • импортирован в разные приложения;
  • использован в разных слоях архитектуры;
  • расширен без модификации существующих правил.
export const emailRules = (email) => {
  test('email', () => {
    enforce(email).isEmail();
  });
};

Эти правила затем интегрируются в разные suites, не нарушая изоляцию логики.

Композиция как механизм структурирования ответственности

Vest поддерживает композиционный подход, при котором suites могут комбинироваться.

const userSuite = create((data) => {
  loginSuite(data);
  profileSuite(data);
});

Композиция позволяет:

  • разделять ответственность по функциональным модулям;
  • собирать сложные проверки из простых блоков;
  • поддерживать единообразие логики в разных частях системы.

Каждый модуль остаётся автономным, но может быть включён в более крупную структуру.

Отсутствие скрытых зависимостей

Важный аспект разделения ответственности — отсутствие скрытых зависимостей между проверками. Каждая validation suite получает данные явно и не использует глобальное состояние.

const suite = create((data) => {
  test('field', () => {
    enforce(data.field).isNotEmpty();
  });
});

Это исключает:

  • неявные связи между модулями;
  • зависимость от состояния UI;
  • побочные эффекты;
  • труднопредсказуемое поведение.

Связь с принципами чистой архитектуры

Подход Vest коррелирует с принципами разделения слоёв в чистой архитектуре:

  • бизнес-правила отделены от интерфейса;
  • инфраструктура не влияет на доменную логику;
  • валидация существует как независимый слой;
  • зависимости направлены внутрь системы правил.

Validation suite в этой модели становится аналогом доменного сервиса, не привязанного к конкретной реализации приложения.

Управление сложностью через декомпозицию

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

  • тесты вместо монолитных функций;
  • группы вместо хаотичных условий;
  • композиция вместо дублирования;
  • независимые suites вместо связанных модулей.

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