Предлагаю вашему вниманию перевод статьи "Replace Throw With Notification" Мартина Фаулера. Примеры адаптированы под .NET.

Если мы валидируем данные, обычно мы не должны использовать исключения, чтобы известить о валидационных ошибках. Здесь я опишу как отрефакторить такой код с использованием паттерна «Уведомление» («Notification»).



Недавно я смотрел на код, который делал базовую валидацию входящих JSON сообщений. Это выглядело примерно так…

public void Сheck()
{
   if (Date == null) throw new ArgumentNullException("Дата не указана");
   DateTime parsedDate;
   try {
     parsedDate = DateTime.Parse(Date);
   }
   catch (FormatException e) {
     throw new ArgumentException("Дата указана в неизвестном формате", e);
   }
   if (parsedDate < DateTime.Now) throw new ArgumentException("Дата не может быть раньше сегодняшней");
   if (NumberOfSeats == null) throw new ArgumentException("Количество мест не указано");
   if (NumberOfSeats < 1) throw new ArgumentException("Количество мест должно быть положительным числом");
 }

Это общий подход к реализации валидации. Запускается серия проверок для некоторых данных (в примере выше валидируются два параметра). Если какая-либо проверка не проходит, то кидается исключение с сообщением об ошибке.

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

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

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

private void ValidateNumberOfSeats(Notification note)
{
  if (numberOfSeats < 1) note.addError("Количество мест должно быть положительным числом");
  // другие проверки, как проверка выше
}

Затем мы можем просто вызвать метод Notification.hasErrors(), чтобы отреагировать на ошибки. Другие методы Notification могут предоставить больше деталей об ошибках.

if (numberOfSeats < 1) throw new ArgumentException(«Количество мест должно быть положительным числом»);


  if (numberOfSeats < 1) note.addError("Количество мест должно быть положительным числом");
  return note;


Когда использовать этот рефакторинг


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

Хорошее правило использования исключений можно встретить в книге «Pragmatic Programmers»:

Мы верим, что исключения редко должны использоваться, как часть нормального потока программы: исключения должны быть зарезервированы для неожиданный ситуаций. Представьте, что необработанное исключение завершит вашу программу и спросите себя: «Будет ли этот код всё ещё работать, если я уберу все обработчики исключений?» Если ответ «нет», то, возможно, исключения использовались в составе нормального потока программы.

— Дэйв Томас и Энди Хант

Важный вывод, который нужно сделать из этого — решение применять ли исключения для конкретной задачи зависит от контекста. Чтение файла, который не существует может являться исключительной ситуацией, а может и не являться. Если мы пытаемся прочитать файл по хорошо известному пути, например, /etc/hosts в Unix, то мы можем предположить, что файл должен быть здесь, поэтому выбрасывание исключения имеет смысл. С другой стороны, если мы читаем файл по пути, переданному пользователем, через командную строку, то мы должны ожидать, что, вероятно, файла здесь нет и использовать другой механизм взаимодействия с неисключительной по своей природе ошибкой.

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

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

Стартовая точка


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

  JsonConvert.DeserializeObject<BookingRequest>(json);

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

  class BookingRequest
  {
    public int? NumberOfSeats { get; set; }
    public string Date { get; set; }
  }

Валидация уже была показана выше.

public void Сheck()
{
   if (Date == null) throw new ArgumentNullException("Дата не указана");
   DateTime parsedDate;
   try {
     parsedDate = DateTime.Parse(Date);
   }
   catch (FormatException e) {
     throw new ArgumentException("Дата указана в неизвестном формате", e);
   }
   if (parsedDate < DateTime.Now) throw new ArgumentException("Дата не может быть раньше сегодняшней");
   if (NumberOfSeats == null) throw new ArgumentException("Количество мест не указано");
   if (NumberOfSeats < 1) throw new ArgumentException("Количество мест должно быть положительным числом");
 }

Создание нотификации


Чтобы использовать нотификации мы должны создать объект Notification. Нотификация может быть достаточно простой, временами просто List.

  var notification = new List<string>();
  if (NumberOfSeats < 5) notification.add("Количество мест должно быть не менее 5");
  // ещё проверки

  // затем…
  if (notification.Any()) // обработка ошибок

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

public class Notification
{
	private List<String> errors = new List<string>();

	public void AddError(string message)
	{
		errors.Add(message);
	}

	public bool HasErrors
	{
		get { return errors.Any(); }
	}
}

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

Разделяем метод Check


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

Используя способ «Выделение метода», выносим тело функции Check в функцию Validation.

public void Сheck()
{
	Validation();
}

public void Validation()
{
	if (Date == null) throw new ArgumentNullException("Дата не указана");
	DateTime parsedDate;
	try
	{
		parsedDate = DateTime.Parse(Date);
	}
	catch (FormatException e)
	{
		throw new ArgumentException("Дата указана в неизвестном формате", e);
	}
	if (parsedDate < DateTime.Now) throw new ArgumentException("Дата не может быть раньше сегодняшней");
	if (NumberOfSeats == null) throw new ArgumentException("Количество мест не указано");
	if (NumberOfSeats < 1) throw new ArgumentException("Количество мест должно быть положительным числом");
}

Затем расширяем метод Validation с созданием Notification и его возвращением из функции.

public Notification Validation()
{
    	var notification = new Notification();
        //...
        return notification;
}

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

public void Сheck()
{
	var notification = Validation();

	if (notification.HasErrors)
		throw new ArgumentException(notification.ErrorMessage);
}

Мы сделали метод Validation открытым, так как ожидается, что большинство пользователей в будущем будут предпочитать использовать этот метод нежели Check.

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

Разделение изначального метода позволило нам отделить валидацию от реакции на её результаты

Перед тем как мы продолжим, следует сказать несколько слов о сообщениях об ошибке. Когда мы делаем рефакторинг, важно избежать изменений в наблюдаемом поведении. Данное правило ведёт нас к вопросу о том какое поведение является наблюдаемым. Очевидно, что выброс исключения — это то, что внешняя программа будет наблюдать, но в какой степени они заботятся о сообщении об ошибке? Notification будет собирать множество сообщений об ошибках и объединять их в одно, например таким образом.

public string ErrorMessage
{
	get { return string.Join(", ", errors); }
}

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

public string ErrorMessage
{
	get { return errors[0]; }
}

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

Валидация числа


Очевидная вещь, которую нужно сделать это заменить первую проверку.

public Notification Validation()
{
        var notification = new Notification();
        if (Date == null) notification.AddError("Дата не указана");
        //...
}

Очевидная замена, но плохая, так как ломает код. Если мы передадим null в качестве аргумента для Date, то мы добавим ошибку в объект Notification, код продолжит выполняться и при разборе получим NullReferenceException в методе DateTime.Parse. Это не то, что мы хотим получить.

Неочевидное, но более эффективное, что нужно сделать в этом случае, это идти с конца метода.

public Notification Validation()
{
       //...
       if (NumberOfSeats < 1) notification.AddError("Количество мест должно быть положительным числом");
}

Следующая проверка — это проверка на null, поэтому мы должны добавить условие, чтобы избежать NullReferenceException

public Notification Validation()
{
       //...
       if (NumberOfSeats == null) notification.AddError("Количество мест не указано");
       else if (NumberOfSeats < 1) notification.AddError("Количество мест должно быть положительным числом");
}

Как мы видим, следующая проверка включает в себя другое поле. И эти проверки также должны учитываться в проверках другого поля. Метод проверки становится слишком сложным. Поэтому выносим проверки NumberOfSeats в отдельный метод.

public Notification Validation()
{
       //...
       ValidateNumberOfSeats(notification);
}

private void  ValidateNumberOfSeats(Notification notification)
{
       if (NumberOfSeats == null) notification.AddError("Количество мест не указано");
       else if (NumberOfSeats < 1) notification.AddError("Количество мест должно быть положительным числом");
}

Когда мы смотрим на выделенную валидацию для числа, она выглядит не очень естественно. Использование if-then-else блоков для валидации может легко привести к чрезмерно вложенному коду. Более предпочтительно использовать линейный код, который обрывается, если не может идти далее, что мы можем реализовать с использованием защитного условия.

private void ValidateNumberOfSeats(Notification notification)
{
	if (NumberOfSeats == null)
	{
		notification.AddError("Количество мест не указано");
		return;
	}

	if (NumberOfSeats < 1) notification.AddError("Количество мест должно быть положительным числом");
}

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

Валидация даты


Начнем с выноса проверок для даты в отдельный метод.

public Notification Validation()
{
       ValidateDate(notification);
       ValidateNumberOfSeats(notification);
}

Затем, как и в случае с числом, начнём заменять исключения с конца метода.

private void ValidateNumberOfSeats(Notification notification)
{
	//...
	if (parsedDate < DateTime.Now) notification.AddError("Дата не может быть раньше сегодняшней");
}

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

Добавим в метод AddError параметр Exception и укажем ему значение по умолчанию null.

public void AddError(string message, Exception exc = null)
{
	errors.Add(message);
}

Это значит мы принимаем исключение, но игнорируем его. Чтобы поместить его куда-либо мы должны изменить тип ошибки внутри класса Notification с string на более сложный объект. Создадим класс Error внутри Notification.

private class Error
{
	public string Message { get; set; }
	public Exception Exception { get; set; }

	public Error(string message, Exception exception)
	{
		Message = message;
		Exception = exception;
	}
}

Теперь у нас есть класс и нам осталось изменить Notification, чтобы он использовал его.

//...
private List<Error> errors = new List<Error>();

public void AddError(string message)
{
	errors.Add(new Error(message, null));
}

public void AddError(string message, Exception exception = null)
{
	errors.Add(new Error(message, exception));
}

//...
public string ErrorMessage
{
	get { return string.Join(", ", errors.Select(e => e.Message)); }
}

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

private void ValidateDate(Notification notification)
{
	if (Date == null) throw new ArgumentNullException("Дата не указана");
	DateTime parsedDate;
	try
	{
		parsedDate = DateTime.Parse(Date);
	}
	catch (FormatException e)
	{
		notification.AddError("Дата указана в неизвестном формате", e);
		return;
	}
	if (parsedDate < DateTime.Now) notification.AddError("Дата не может быть раньше сегодняшней");
}

И последнее изменение достаточно простое.

private void ValidateDate(Notification notification)
{
	if (Date == null) notification.AddError("Дата не указана");
	DateTime parsedDate;
	try
	{
		parsedDate = DateTime.Parse(Date);
	}
	catch (FormatException e)
	{
		notification.AddError("Дата указана в неизвестном формате", e);
		return;
	}
	if (parsedDate < DateTime.Now) notification.AddError("Дата не может быть раньше сегодняшней");
}

Заключение


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

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

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


  1. danl
    21.03.2016 10:40
    +1

    Бросать Exceptionы при валидации входящих данных в .NET это последнее что бы я делал на проекте, в котором много валидации данных. Еще лет 5 назад для этого атрибуты использовались и единый подход к валидации любых полей. И писать "private void ValidateDate(..." для каждого поля в каждой модели это не многим лучше throw Exception.


    1. AxisPod
      21.03.2016 16:17

      Видимо лет 5 назад тесты вы еще не писали, да и DI не использовали.


      1. lair
        21.03.2016 17:33
        -1

        А как тесты и DI влияют на атрибутивную валидацию?


        1. AxisPod
          21.03.2016 18:06

          Ну видимо так, что нарушают концепцию DI. Не дают возможностей подмены для тестирования и т.д.


          1. lair
            21.03.2016 18:08

            Подмены чего и в каком конкретно сценарии?


  1. lair
    21.03.2016 11:44
    +3

    Уже было, и там уже, что логично, обсудили.


  1. vics001
    21.03.2016 15:32

    Несмотря на то, что обсуждали, отпишусь в новой статье. Я проведу аналогию с Java, что возможно немногим отличается.
    Этот спор возвращает нужны ли checked и non-checked exception. Повсеместно бытует мнение, что checked exception лучше не использовать, и это так, если писать библиотеку, то checked exception заметно ухудшают жизнь пользователю этой библиотеки, так как он зачастую он использует библиотеку и не ждет данных ошибок, так как они предусмотрены другими ошибками.
    Однако, существует случай, когда checked exception помогают, а именно при ожидаемых ошибках, например, неправильно заполненная форма или проверка. В этом случае желательно использовать checked exception и он, конечно же, специфичен для предметной области. Например, NotAvailableSeatsException extends GenericBookingException. Это даст необходимую информацию разработчику UI, что этот exception надо словить и показать, так как он является User case exception, в отличие от NullPointerException, IllegalArgumentException и других стандартных, которые должны быть non-checked и по-хорошему не должны появляется вообще.

    Что касается notification, то этот механизм имеет свои плюсы и минусы по сравнению с checked business exception. Exception уже есть и их не надо придумывать, они интегрированы с IDE, представляют изначально объектно-ориентированную модель и расширяемы, они реализуют естественный pattern fail first and further checks rely on a predictable state. Главный минус exception, что для сложных валидаций, когда надо проверить все поля сразу или структурировать или когда проверок слишком много, код написанный в таком стиле уже будет неестественным.


  1. asvishnyakov
    21.03.2016 16:49
    -1

    Нет, нет и ещё раз. Не для того разработчики .NET включали исключения в CLR и мучались с их поддержкой в куче разных фич вроде async, чтобы мы опять использовали WinAPI-style с возвратом ошибок.


    1. lair
      21.03.2016 17:31
      +1

      А вас не смущает, что те же самые разработчики .net предосмотрели целый интерфейс IValidatableObject с его поддержкой в куче фреймворков?


      1. Einherjar
        21.03.2016 19:05

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


        1. lair
          21.03.2016 19:08

          Валидация в asp.net MVC, скажем, не бросает (не бросала) исключения. И в любом другом месте, где мне надо встроиться, я могу взять валидатор, получить из него ошибки и использовать их.


          1. Einherjar
            21.03.2016 19:34

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


            1. mird
              21.03.2016 21:03

              В wcf нужно кидать FaultException (потому что он преобразуется в описанный стандартом SoapFault).


            1. lair
              21.03.2016 22:03

              фактически в статье велосипед

              Фактически, в статье описан подход, на котором построен и стандартный механизм .net, и некоторые механизмы в Java.

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

              Но зачем?

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

              Почему вы приравниваете исключительные ситуации к "отказаться вообще"? У вас так мало исключительных ситуаций в программах?

              приводит к большому количеству велосипедов.

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

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

              Как мило, вы вот так взяли и навязали майкрософтовский велосипед в виде исключений всем сторонним клиентам оптом. Хотя они знать не хотят про ваше исключение — у них есть soap:fault, и он их устраивает. Поэтому бросать надо не абстрактное исключение со списком ошибок, а FaultException, типизованный вашим списком ошибок, и прописывать этот же список в FaultContract — что, в итоге, и свелось к тому же самому списку ошибок, с которого начинали.

              И это все только потому, что в WCF нет удобного способа писать фильтры и возвращать произвольные типы данных: в MVC (или в WebAPI) можно было бы достигнуть того же эффекта вообще не используя исключения — там есть ModelState, который прекрасно валидируется во время биндинга, и на основе которого можно вернуть клиенту отлуп, который тот сможет корректно распарсить. Причем этот код можно написать один раз, запихнуть в фильтр, и забыть.


              1. Einherjar
                21.03.2016 22:40

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


                1. lair
                  21.03.2016 22:46

                  Во-вторых не каждая система подразумевает наличие сторонних клиентов

                  И поэтому давайте мы будем в одних системах кидать исключения, а в других — нет?

                  В-четвертых даже любое абстрактное исключение при желании можно обернуть в FaultException

                  Знаете, как это мило разбирается на клиенте?

                  Я просто привел пример где бросок исключения для той же валидации вполне уместен и корректен

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

                  Но самое главное, что по факту, если не использовать нормальные исключения (т.е., использовать FaultException), то выясняется, что в WCF паттерн, описанный в статье, прекрасно реализуется без какого-либо велосипеда.


                  1. mird
                    22.03.2016 00:18

                    Но самое главное, что по факту, если не использовать нормальные исключения (т.е., использовать FaultException), то выясняется, что в WCF паттерн, описанный в статье, прекрасно реализуется без какого-либо велосипеда.

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


                    1. lair
                      22.03.2016 00:23
                      +1

                      … причем этот паттерн будет одинаков, используем мы WCF, WebAPI или MVC — отличаться будет только способ конверсии валидационных сообщений в сообщение протокола.


  1. IhnatKlimchuk
    21.03.2016 16:50

    Не так давно столкнулся с тем, что надо было валидировать данные на уровне бизнес логики, а не на уровне представления в ASP.NET Core (3-х уровневая архитектура). После долгих раздумий решил посмотреть как же делают другие. И понравилось решение реализации валидаторов в Identity (https://github.com/aspnet/Identity), UserValidator например c IdentityResult.


  1. AlexLeonov
    21.03.2016 20:54

    Фаулер не сделал еще три простых и очевидных шага. Возможно язык, которым он оперирует, не позволил ему это сделать?

    1. Вводим конструкцию, которая является коллекцией исключений и сама по себе исключением одновременно. Это и есть его объект Notification, осталось только добавить к нему реализацию Throwable

    2. Позволяем нашему низкоуровневому коду (например — валидаторам конкретных полей) делать не throw new Exception, а yield new Exception. Таким образом валидатор либо молчит (всё ОК), либо генерирует серию исключений

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

    Как итог мы имеем реализацию паттерна Notification на стандартных и привычных исключениях, получая все их преимущества.

    Написать что-ли Фаулеру? Мол так и так, мистер, я тут кажется придумал еще один паттерн для вас )))


    1. lair
      21.03.2016 22:04
      -1

      получая все их преимущества.

      … и все недостатки.

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

      А зачем его выбрасывать?


      1. AlexLeonov
        21.03.2016 22:38

        Чтобы поймать. Выбрасывать нужно, чтобы прокинуть сквозь уровни кода. И да — чтобы поймать. На нужном нам уровне. Исключения же?


        1. lair
          21.03.2016 22:41
          -2

          Выбрасывать нужно, чтобы прокинуть сквозь уровни кода.

          А точно нужно прокидывать через все уровни кода? А зачем?

          чтобы поймать. На нужном нам уровне.

          Теперь у вас все уровни выше знают про исключения, кидаемые ниже. Это точно правильно?


          1. AlexLeonov
            21.03.2016 22:49

            А точно нужно прокидывать через все уровни кода? А зачем?

            В одних случаях нужно, в других — нет. Вы архитектор, вам решать.

            И да, почему через ВСЕ? Вы приписываете мне слова, которые я не говорил. Прокинуть через уровни. Всплыть вверх. Но не до конца, иначе будет Uncaught Exception. Следовательно не через все уровни кода.

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

            С чего вы это взяли? Уровни ни о чем не знают, они просто в нужном месте потенциально опасный код окружают try {… } И это — правильно. Я не вижу причин, почему try {… } catch {… } с указанием конкретных ожидаемых исключений может быть "неправильно".

            Метафору "уровни знают про исключения" я не понимаю, извините.


            1. lair
              21.03.2016 22:54
              +1

              В одних случаях нужно, в других — нет. Вы архитектор, вам решать.

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

              Я не вижу причин, почему try {… } catch {… } с указанием конкретных ожидаемых исключений может быть «неправильно».

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


              1. AlexLeonov
                21.03.2016 23:00

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

                try { user = new User; user.fill(data); user.save(); } catch (DbException e) { // чё-то не то в сохранении в БД } catch (ValidateErrors e) { // валидаторы сработали }

                Это интерфейс же. Исключения, бросаемые внутри try — это интерфейс кода, который там внутри. Что плохого в явном указании интерфейса?


                1. lair
                  21.03.2016 23:05

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

                  Да, это плохое. Оно может быть компенсировано хорошим, и хорошее может перевешивать.

                  Что плохого в явном указании интерфейса?

                  Ничего. Но чем многословнее интерфейс, тем дальше мы от принципа сокрытия информации.


                  1. AlexLeonov
                    21.03.2016 23:07
                    +2

                    Я не настолько хорошо учил в институте философию, чтобы поддерживать эту беседу ))


              1. AlexLeonov
                21.03.2016 23:01

                А если я не хочу прокидывать вообще?

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


                1. lair
                  21.03.2016 23:05

                  Да я и пробовал, и пишу. И вот далеко не всегда исключения удобнее.


          1. AlexLeonov
            21.03.2016 22:57

            Теперь у вас все уровни выше знают про исключения, кидаемые ниже. Это точно правильно?

            int value = sum(2,2);
            теперь у вас все уровни знают, что функция sum возвращает int! это точно правильно?


            1. lair
              21.03.2016 23:04

              Я с трудом могу себе представить случай, когда это неправильно.


              1. AlexLeonov
                21.03.2016 23:05

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