Здравствуйте, Хабр.

Иногда попадаются статьи, которые хочется перевести просто за имя. Еще интереснее, когда такая статья может пригодиться специалистам по разным языкам, но содержит примеры на Java. Совсем скоро надеемся поделиться с вами нашей новейшей идеей по поводу издания большой книги о Java, а пока предлагаем ознакомиться с публикацией Мартина Фаулера от декабря 2014, которая до сих пор не была переведена на русский язык. Перевод сделан с небольшими сокращениями.

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

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

public void check() {
   if (date == null) throw new IllegalArgumentException("date is missing");
   LocalDate parsedDate;
   try {
     parsedDate = LocalDate.parse(date);
   }
   catch (DateTimeParseException e) {
     throw new IllegalArgumentException("Invalid format for date", e);
   }
   if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
   if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
   if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
 }


Именно так обычно и выполняется валидация. Вы применяете к данным несколько вариантов проверки (выше приведены лишь несколько полей целого класса). Если хотя бы один этап проверки не пройден, то выбрасывается исключение с сообщением об ошибке.

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

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

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

private void validateNumberOfSeats(Notification note) {
  if (numberOfSeats < 1) note.addError("number of seats must be positive");
  // другие подобные проверки
}

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



Когда применять такой рефакторинг

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

Удобное «железное правило», которым стоит пользоваться при реализации исключений, находим в Pragmatic Programmers:

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


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

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

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

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

Начало

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

gson.fromJson(jsonString, BookingRequest.class)

Gson принимает класс, ищет любые поля, удовлетворяющие ключу в JSON-документе, а затем заполняет такие поля.

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

class BookingRequest…

  private Integer numberOfSeats; 
  private String date;
Валидационные операции — такие, которые уже упоминались выше
class BookingRequest…
  public void check() {
     if (date == null) throw new IllegalArgumentException("date is missing");
     LocalDate parsedDate;
     try {
       parsedDate = LocalDate.parse(date);
     }
     catch (DateTimeParseException e) {
       throw new IllegalArgumentException("Invalid format for date", e);
     }
     if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
     if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
     if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
   }

Создание уведомления

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

Уведомление аккумулирует ошибки

List<String> notification = new ArrayList<>();
if (numberOfSeats < 5) notification.add("number of seats too small");
// сделать еще несколько проверок

// затем…
if ( ! notification.isEmpty()) // обработать условие ошибки

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

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

  public void addError(String message) { errors.add(message); }
  public boolean hasErrors() {
    return ! errors.isEmpty();
  }
  …


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

Раскладываем проверочный метод на части

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

Для этого я первым делом использую выделение метода необычным образом: вынесу все тело проверочного метода в валидационный метод.

class BookingRequest…

  public void check() {
    validation();
  }

  public void validation() {
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
    if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
  }


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

class BookingRequest…

  public Notification validation() {
    Notification note = new Notification();
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
    if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
    return note;
  }


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

class BookingRequest…

  public void check() {
    if (validation().hasErrors()) 
      throw new IllegalArgumentException(validation().errorMessage());
  }

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

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

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

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

class Notification…

  public String errorMessage() {
    return errors.stream()
      .collect(Collectors.joining(", "));
  }

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

class Notification…

  public String errorMessage() { return errors.get(0); }


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

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

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

Самый очевидный шаг в данном случае — заменить первую валидацию

class BookingRequest…

  public Notification validation() {
    Notification note = new Notification();
    if (date == null) note.addError("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
    if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
    return note;
  }

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

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

class BookingRequest…

  public Notification validation() {
    Notification note = new Notification();
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
    if (numberOfSeats < 1) note.addError("number of seats must be positive");
    return note;
  }

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

class BookingRequest…

  public Notification validation() {
    Notification note = new Notification();
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    if (numberOfSeats == null) note.addError("number of seats cannot be null");
    else if (numberOfSeats < 1) note.addError("number of seats must be positive");
    return note;
  }

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

class BookingRequest…

  public Notification validation() {
    Notification note = new Notification();
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
    validateNumberOfSeats(note);
    return note;
  }

  private void validateNumberOfSeats(Notification note) {
    if (numberOfSeats == null) note.addError("number of seats cannot be null");
    else if (numberOfSeats < 1) note.addError("number of seats must be positive");
  }

Смотрю на выделенную валидацию числа, и ее структура мне не нравится. Не люблю использовать при валидации блоки if-then-else, поскольку так легко может получиться код с чрезмерным количеством вложений. Предпочитаю линейный код, который прекращает работать сразу же, как только выполнение программы становится невозможным – такую точку можно определять при помощи граничного условия. Так я реализую замену вложенных условных конструкций граничными условиями.

class BookingRequest…

  private void validateNumberOfSeats(Notification note) {
    if (numberOfSeats == null) {
      note.addError("number of seats cannot be null");
      return;
    }
    if (numberOfSeats < 1) note.addError("number of seats must be positive");
  }


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

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

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

При валидации даты вновь начинаю с выделения метода:

class BookingRequest…

  public Notification validation() {
    Notification note = new Notification();
    validateDate(note);
    validateNumberOfSeats(note);
    return note;
  }

  private void validateDate(Notification note) {
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
  }

Когда я использовал автоматизированное выделение метода в моей IDE, результирующий код не включал аргумента уведомления. Поэтому я попытался добавить его вручную.

Теперь давайте пойдем назад при валидации даты:

class BookingRequest…

  private void validateDate(Notification note) {
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      throw new IllegalArgumentException("Invalid format for date", e);
    }
    if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today");
  }

На втором этапе возникает осложнение с обработкой ошибки, поскольку в выброшенном исключении есть условное исключение (cause exception). Чтобы его обработать, потребуется изменить уведомление — так, чтобы оно могло принимать такие исключения. Поскольку я как раз на полпути: отказываюсь от выбрасывания исключений и перехожу к работе с уведомлениями — мой код красный. Итак, я откатываюсь назад, чтобы оставить метод validateDate в вышеприведенном виде, и готовлю уведомление к приему условного исключения.

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

class Notification…

  public void addError(String message) {
    addError(message, null);
  }

  public void addError(String message, Exception e) {
    errors.add(message);
  }

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

class Notification…


  private static class Error {
    String message;
    Exception cause;

    private Error(String message, Exception cause) {
      this.message = message;
      this.cause = cause;
    }
  }


Я не люблю неприватные поля в Java, однако поскольку здесь мы имеем дело с приватным внутренним классом, меня все устраивает. Если бы я собирался открывать доступ к этому классу ошибки где-нибудь вне уведомления, то инкапсулировал бы эти поля.

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

class Notification…

  private List<Error> errors = new ArrayList<>();

  public void addError(String message, Exception e) {
    errors.add(new Error(message, e));
  }
  public String errorMessage() {
    return errors.stream()
            .map(e -> e.message)
            .collect(Collectors.joining(", "));
  }


Имея новое уведомление, могу внести изменения и в запрос о бронировании

class BookingRequest…

private void validateDate(Notification note) {
    if (date == null) throw new IllegalArgumentException("date is missing");
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      note.addError("Invalid format for date", e);
      return;
    }
    if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today");

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

Последнее изменение совсем простое

class BookingRequest…

private void validateDate(Notification note) {
    if (date == null) {
      note.addError("date is missing");
      return;
    }
    LocalDate parsedDate;
    try {
      parsedDate = LocalDate.parse(date);
    }
    catch (DateTimeParseException e) {
      note.addError("Invalid format for date", e);
      return;
    }
    if (parsedDate.isBefore(LocalDate.now())) note.addError("date cannot be before today");
  }


Вверх по стеку

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

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

Фреймворки

Ряд фреймворков обеспечивает возможность валидации с применением паттерна уведомления. В Java это Java Bean Validation и валидация Spring. Эти фреймворки служат своеобразными интерфейсами, инициирующими валидацию и использующими уведомление для сбора ошибок ( Set<ConstraintViolation> для валидации и Errors в случае Spring).

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

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


  1. lair
    06.11.2015 16:06
    +5

    Генераторы все-таки удобнее (хотя по сути паттерн-то тот же):

    public IEnumerable<ValidationError> Validate() {
        if (Date.IsBefore(DateTime.Now))
            yield return new ValidationError(nameof(Date), "date cannot be before today");
        if (NumberOfSeats < 1)
            yield return new ValidationError(nameod(NumberOfSeats), "number of seats must be positive");
        //raise, rinse, repeat
      }
    


    1. wheercool
      06.11.2015 16:52

      А ничего что IEnumerable обрабатывается лениво?
      Если код вызывающий Validate не будет делать force (явно проходить по коллекции ошибок) то метод не будет выполняться даже если по логике он проходит всю валидацию?


      1. lair
        06.11.2015 16:56

        Единственный способ не вызвать проход по коллекции — это проигнорировать возвращенный результат (аналогично невызову notification.hasErrors() из поста).

        Обычно вызывающая сторона либо делает Any, либо сразу сворачивает в коллекцию, чтобы два раза не ходить.


        1. wheercool
          06.11.2015 17:39

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

          А вообще получается довольно гибкая вещь. Валидаторы можно склеивать тоже лениво, например так:
          Validate1().Union(Validate2())
          А затем, если мы вдруг решим аггрегировать не все ошибки, а только первую, или первые n то код валидаторов останется тот же. Достаточно будет добавить к результату .Take(1)


          1. lair
            06.11.2015 17:42
            +1

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

            В Validate не должно быть побочных эффектов (что семантически правильно).


            1. wheercool
              06.11.2015 17:52
              +1

              Согласен, что для чистых validate бессмысленно его вызывать, если не использовать результат.
              Но возможны validate и с побочными эффектами. Например когда нам нужно проверить не существует ли пользователя с таким логином и нам нужны запросы к БД.
              Тогда как мне кажется ленивость может сыграть с нами злую шутку.


              1. lair
                06.11.2015 17:55
                -2

                Например когда нам нужно проверить не существует ли пользователя с таким логином и нам нужны запросы к БД. Тогда как мне кажется ленивость может сыграть с нами злую шутку.

                И какую же? Мы не сделаем ненужного запроса к БД, если нас интересует, валидна ли форма регистрации, но не интересует список ошибок (хотя это и глупо достаточно)?


      1. artbear
        07.11.2015 09:45

        В статье четко написано, почему выбран класс, а не список или IEnumerable

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

        И это правильно, инкапсуляция рулит


        1. lair
          07.11.2015 13:00

          В отличие от возвращаемого IEnumerable, простой список не дает такого радикального выигрыша в читаемости валидационного метода.

          А еще в .net это просто уже существующая типовая реализация.


  1. AlexLeonov
    06.11.2015 16:11

    А разве нельзя в Java сделать исключение, которое будет коллекцией других исключений? Добавляем в коллекцию, как заполнили — бросаем ее?


    1. burjui
      06.11.2015 17:59

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


      1. AlexLeonov
        06.11.2015 18:06

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


  1. eaa
    06.11.2015 17:03
    +1

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


  1. Mofas
    06.11.2015 21:00
    -4

    +1


  1. Scf
    06.11.2015 21:37
    +3

    Валидатор со списком ошибок крайне полезен в B2B — мало что бесит юзера больше, чем нажатие на кнопку «сохранить» после каждой исправленной ошибки. Особенно если форма на экран не влазит.


  1. ComodoHacker
    07.11.2015 09:03
    +4

    Уважаемые переводчики издательского дома «Питер»!
    «date == null» это не «нулевая дата», это пустая дата. И уж тем более не «проверка на нуль».


  1. igrishaev
    07.11.2015 14:49
    -1

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


    1. lair
      07.11.2015 15:34

      … чтобы не заниматься последующим сопоставлением кодов ошибок с текстами, например? Или не пытаться придумать, как дополнительную информацию (например, имя ошибочного свойства) в коды запихнуть? Или не пытаться каким-то чудом избежать дублирования кода внутри лямбд при одинаковых проверках?


      1. igrishaev
        07.11.2015 16:17
        -3

        Догадки у вас есть, а ответа, похоже, нет.


        1. lair
          07.11.2015 16:26
          +1

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


  1. funca
    07.11.2015 17:18
    -2

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


  1. caballero
    10.11.2015 16:47
    -1

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

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


    Я б использовал более примитивный подход. Статический класс.
    В процессе какие то из функций любой вложенности добавили статическим методом ошибку в коллекцию. Возможно даже два параметра — один для пользака другой для логирования с уточнением места где она возникла и т.п…

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

    .

    .


    1. lair
      10.11.2015 17:16
      -1

      Вы это серьезно? Статическая зависимость для передачи данных между двумя функциями? А параллельное выполнение как же?


      1. caballero
        10.11.2015 17:27

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



        1. lair
          10.11.2015 17:31

          это служебная функция а не бизнес логика.

          На нее поэтому не распространяются принципы программирования?

          И речь идет о взаимодействии с пользователем а не о каком то серверном процессе- при чем тут паралельность. Если это веб — хранить колекцию в сессии.

          … а в сессии параллельных процессов никогда не бывает, конечно же.

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


          1. caballero
            10.11.2015 17:36
            -1

            На нее поэтому не распространяются принципы программирования?

            Индусский код — это не принцип програмирования.

            а в сессии параллельных процессов никогда не бывает, конечно же.

            у нормальных людей не бывает.
            если это веб — какие паралельные процессы на одного юзера в обработке реквеста с браузера?

            А еще, скажем, бывает вложенная валидация.

            Речь о том чтобы вывести пользователю ошибку на введенные им данные а не что бывает в индусском мозгу.




            1. lair
              10.11.2015 17:39

              Индусский код — это не принцип програмирования.

              Я, вроде бы, про индусский код не спрашивал, я конкретный вопрос задал.

              если это веб — какие паралельные процессы на одного юзера в обработке реквеста с браузера?

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

              Речь о том чтобы вывести пользователю ошибку на введенные им данные а не что бывает в индусском мозгу.

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

              Как вы предлагаете это делать в вашем подходе?


              1. caballero
                10.11.2015 17:59
                -1

                Я, вроде бы, про индусский код не спрашивал, я конкретный вопрос задал.

                Разговоры о «принципах» не бывают конкретными.
                У вас принцип максимально все усложнить чтобы обьять необьятное — 100% всевозможных ситуаций.
                Мой принцип — бритва Оккама -не надо плодить сущьности сверх необходимого. Возможно и есть проекты и архитектуры где статика не работает но в 9 случаев из 10 не вижу проблем.


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

                Обычно да. Так работает веб.
                Даже если на странице налеплено аякса все равно идет ответ на действие пользака. Иначе все равно все ошибки налезут в кучу. Даже при оттдельных валидаторах

                Как вы предлагаете это делать в вашем подходе?

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

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



                1. lair
                  10.11.2015 18:01

                  Разговоры о «принципах» не бывают конкретными.

                  Почему же? Сами по себе принципы часто (не всегда) весьма конкретны. В частности, принцип использовать как можно меньше глобального состояния весьма конкретен.

                  Обычно да. Так работает веб.

                  Какой именно «веб» так работает? Если что, «веб» по задумке своей вообще не имеет состояния, в том числе и столь любимых вами сессий.

                  Есть бизнес функция где проверяется адрес, она же и пишет ошибку понимая контекст

                  А откуда она понимает контекст?


                  1. caballero
                    10.11.2015 18:18

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

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

                    Какой именно «веб» так работает? Если что, «веб» по задумке своей вообще не имеет состояния, в том числе и столь любимых вами сессий.

                    веб сервера имеют
                    А откуда она понимает контекст?

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

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


                    1. lair
                      10.11.2015 18:27

                      ну вот это состояние и будет единственным на весь проект.

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

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

                      Ну так паттерн синглтон и считается антипаттерном уже.

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

                      Не, вы не поняли. У меня есть бизнес-объект «Адрес», который в системе используется в десяти разных местах. Во всех этих местах у него есть обязательная валидация, которая одинаковая. Она, естественно, вынесена в отдельный юнит (если это ООП — то метод класса, если ФП — то функцию). (1) откуда этот юнит узнает про контекст (2) теперь этот юнит обязан знать про ваш статический класс (3) что будет, когда у меня юнит-тесты по этой валидации запустятся десятком в параллель?


                      1. caballero
                        10.11.2015 18:59
                        -2

                        А зачем оно нужно, почему нельзя без него обойтись?

                        можно. Но такое решение упрощает проект во многих местах.
                        Ну так паттерн синглтон и считается антипаттерном уже.

                        Вы слишком много внимание уделяете теоретическим конструкциям и терминам вместо практического програмирования. Есть люди, зарабатывюще на жизнь книгами по теории програмированияю Они придумывают разные теоретические конструкции. Сначала паттерны, потом типа сиквела — антипаттерны чтобы написать еще одну книгу потом еще чего нибудь понапридумывают.
                        И есть люди (не будем тыкать пальцами) которые готовы днями спорить является ли Active Record подвидом ORM или нет хотя практической пользы правильная классификация согласно неким теоретическим положнякам, аж никакой.

                        .

                        откуда этот юнит узнает про контекст

                        если он не знает то как поможет приведеное в статье решение?

                        теперь этот юнит обязан знать про ваш статический класс

                        ну куда то ж он по любому складирует ошибку. Почему не может быть статический метод.

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

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

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


                        1. lair
                          11.11.2015 11:01

                          Но такое решение упрощает проект во многих местах.

                          В каких? Чем оно легче простого возврата объекта с ошибками?

                          Вы слишком много внимание уделяете теоретическим конструкциям и терминам вместо практического програмирования.

                          Не вам это решать.

                          если он не знает то как поможет приведеное в статье решение?

                          А ему не надо. Он возвращает свой список ошибок, дальше вызывающая сторона (которая знает контекст или его часть) обрабатывает эти ошибки с учетом известного ей контекста.

                          ну куда то ж он по любому складирует ошибку. Почему не может быть статический метод.

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

                          а зачем запускать впараллель юнит тесты

                          Чтобы быстрее было, очевидно.

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

                          Я эти «искусственные или крайне редко используемые конструкции» вижу каждый день более пяти лет.


    1. burjui
      10.11.2015 18:17

      Статический класс со статическими методами — это, по сути, кучка глобальных переменных со своими методами. Фу. К тому же, вы предлагаете ещё и вставить палку в колесо IDE. Например, в Intellij IDEA нажатие Alt+F7 на идентификаторе или Ctrl+Mouse1 (в объявлении) покажет вам все места, где используется некий объект. Если вы создаёте объект нотификации по мере надобности и передаёте аргументами в методы валидации, вы увидите в коде некий отдельный контекст валидации — например, форму регистрации. Если же вы используете статический класс, то при поиске его использования вы получите все контексты разом, и вам придётся определять, какой вызов к какому контексту относится.


      1. caballero
        10.11.2015 18:23

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


        1. burjui
          10.11.2015 19:21
          +1

          вообще то переменная всего одна (причем инкапсулирована в статическпй клас) — на то она и статическая.
          Вам разве не очевидно, что я говорил об общем случае, который включает в себя и ваш конкретный? Или это придирка к несущественным деталям с целью возразить из принципа? ОК, я разжую за вас: ключевой момент — глобальное состояние.

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

          Или по вашему код надо писать так чтобы он был к IDE привязан?
          Я нигде не говорил про привязку. Разжую ещё раз: если можно писать код так, чтобы IDE облегчала работу с ним, и это не отражается отрицательно на его читабельности — стоит писать именно так.


          1. caballero
            10.11.2015 19:52
            -1

            Вам разве не очевидно, что я говорил об общем случае,

            зачем обсуждать некие общие случаи если речь идет о конкретном прикладном решении.

            Где вы видели IDE, которая понимает не только синтаксис, но и _назначение_ вашего кода?

            я может вообще в блокноте пишу. При чем тут вообще iDE.

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

            это если вы пишете один. А если с этим кодом должен работать кто то еще у которых другая IDE?