Developers Club geek daily blog

1 year, 4 months ago
Hello, Habr.

Sometimes articles which want to be translated just for a name come across. It is even more interesting when such article can be useful to different languages specialists, but contains examples on Java. Very soon we hope to share with you our latest idea concerning the edition of the big book about Java for now we suggest to study Martin Fowler's publication of December, 2014 which still was not translated into Russian. Transfer is made with small reductions.

If you execute validation of these or those data, then usually you should not use exceptions in signal quality that validation is not taken place. Here I will describe refactoring of a similar code with use of a pattern "Notification".

Recently the code executing the simplest validation of JSON messages caught sight to me. It looked approximately so:

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");
 }


Quite so usually also validation is executed. You apply several options of check to data (some fields of the whole class are given above). If at least one stage of check is not passed, then the exception with an error message is thrown out.

I had some problems with such approach. First, I do not like to use exceptions in similar cases. The exception is a signal of some extraordinary event in the considered code. But if you subject to check a certain external input, then assume that the entered messages may contain errors — that is, errors are expected, and it is wrong to apply in that case exceptions.

The second problem with a similar code is that if it falls after the first found error, then it is more reasonable to report about all errors which arose in the entered data and not just about the first. In that case the client will be able to display to the user at once all errors that that corrected them for one operation, but not to force the user to play with the computer cat and mouse.

In such cases I prefer to organize the reporting on validation errors by means of a pattern "Notification". The notification is the object collecting errors at each failed act of validation the next error is added to the notification. The method of validation returns the notification which you then can sort for clarification of additional information. A simple example — the following code for execution of checks.

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

Then it is possible to make a simple challenge like aNotification.hasErrors () for response to errors if those are. Other methods in the notification can get to the bottom of more detailed information on errors.

How to manage almost without any exception, having replaced them with notifications

When to apply such refactoring

Here I will note that I do not urge to get rid of exceptions in all your base of a code. Exceptions — very convenient method of processing of emergency situations and their withdrawal from the main logical flow. The offered refactoring is pertinent only when the result reported by means of an exception in practice exclusive is not, so, has to be processed by the main logic of the program. The validation considered here — just such case.

We find the convenient "iron rule" which should be used at implementation of exceptions in Pragmatic Programmers:

We believe that exceptions should be used only incidentally within the normal course of the program; it is necessary to resort to them at exclusive events. Assume that not intercepted exception will complete your program, and ask a question: whether "will continue this code to function if to clean from it exception handlers"? If the answer negative, then probably exceptions are applied in non-exclusive situations.


— Dave Thomas and Andy Hunt

From there is an important consequence: the solution on whether to use exceptions for a specific objective, depends on its context. So, Dave and Andy continue, reading from not found file can be or not to be in different contexts an exclusive situation. If you try to consider the file from well-known location, for example / etc/hosts in the Unix system, it is quite logical to assume that the file has to appear there, and otherwise it is reasonable to issue an exception. On the other hand, if you try to consider the file located on the way entered by the user into the command line, then have to assume that the file will not appear there and to use other mechanism — such which signals about the non-exclusive nature of an error.

There is a case in which it would be reasonable to use exceptions at a validation error. The situation is meant: there are data which already had to pass validation on earlier processing stage, but you want to make such check again to be reinsured from software failures because of which could slip some inadmissible data.

This article tells about replacement of exceptions by notifications in the context of validation of the unprocessed input. Such equipment is useful to you and when the notification — more reasonable option, than an exception, but here we concentrate on option with validation as the most widespread.

Beginning

So far I did not mention data domain as described only the most general code format. But further at development of this example it will be required to outline data domain more precisely. It will be a question of the code accepting in the JSON format of the message on armoring of places in theater. The code represents the class of request for armoring filled on the basis of information of JSON by means of gson library.

gson.fromJson(jsonString, BookingRequest.class)

Gson accepts a class, looks for any fields satisfying to a key in the JSON document and then fills such fields.

This request for armoring contains only two elements which we will validirovat: date of action and the number of the armored places

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");
   }

Creation of the notification

To use the notification, for it it is necessary to create special object. The notification can be very simple, occasionally it consists only of the list of lines.

The notification accumulates errors

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

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

Though the simple idiom of the list provides lightweight implementation of a pattern, I prefer to these not to be limited and I write a simple class.

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

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


Applying concrete class, I express my intention more accurately — the reader of a code should not correlate mentally the idiom and its complete value.

We decompose a test method on part

First of all I will separate a test method into two parts. The internal part will work as a result only with notifications and not to issue any exceptions. The external part will save actual behavior of a test method — that is, will issue an exception at a validation failure.

For this purpose I first of all use selection of a method unusually: I will take out all body of a test method in a validation method.

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");
  }


Then I will modify a validation method so that it created and returned the notification.

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;
  }


Now I can check the notification and issue an exception if it contains errors.

class BookingRequest …

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

I made a validation method public as I expect that the majority causing in the long term will prefer to use this method, but not test.

Breaking an initial method into two parts, I delimit validation check from a solution on how to react to an error.

At this stage I still did not touch behavior of a code at all. The notification will not contain any errors, and any failed checks of validation will and throw out further exceptions, ignoring any new machinery which I will add here. But I prepare the soil to replace issue of exceptions with work with notifications.

Before starting it, it is necessary to tell something about error messages. When refactoring there is a rule: to avoid changes in observed behavior. In such situations as this this rule immediately raises a question for us: what behavior is observable? Obviously, issue of a correct exception in a certain measure will be noticeable for the external program — but in what degree the error message is actual for it? As a result the notification accumulates a set of errors and will be able to generalize them in the uniform message, approximately so:

class Notification …

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

But there will be a problem if at higher level execution of the program is started on receipt of the message only on the first found error, and then you will need something it seems:

class Notification …

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


It is necessary to pay attention not only to defiant function, but also on all exception handlers to define the adequate answer to this situation.

Though here I could not provoke any problems at all, I will surely compile and I will test this code before to make new changes.

Validation of number

The most obvious step in this case — to replace the first validation

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;
  }

Obvious step, but bad as it will break a code. If to transfer functions zero date, then the code will add an error to the notification, but then will at once try to sort it and will issue an exception of null pointer — and we are interested not in this exception.

Therefore in this case it is better to make less rectilinear, but more effective thing — a step backwards.

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;
  }

The previous check is a check on zero therefore we need conditional construction which would allow not to create an exception of null pointer.

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;
  }

I see that the following check affects other field. Not only that at the previous stage of refactoring I should enter conditional construction — now it seems to me that the validation method excessively becomes complicated, and it could be spread out. So, we select the parts which are responsible for validation of numbers.

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");
  }

I look at the selected validation of number, and its structure is not pleasant to me. I do not like to use if-then-else blocks as the code with excessive quantity of attachments so easily can turn out at validation. I prefer the line code which stops working at once as soon as execution of the program becomes impossible – such point can be defined by means of edge condition. So I implement replacement of the enclosed conditional constructions with edge conditions.

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");
  }


When refactoring it is always necessary to try to take the minimum steps to save the available behavior

My solution to take a step back plays a key role when refactoring. The essence of refactoring consists in change of a code format, but so that the executed conversions did not change his behavior. Therefore refactoring always needs to be done with small steps. So we are insured against emergence of errors which can overtake us in a debugger.

Validation of date

At validation of date I begin with selection of a method again:

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");
  }

When I used the automated selection of a method in my IDE, the resulting code did not include argument of the notification. Therefore I tried to add it manually.

Now let's go back at validation of date:

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");
  }

At the second stage there is a complication with processing of an error as in the thrown-out exception there is a conditional exception (cause exception). That to process it, it will be required to change the notification — so that it could accept such exceptions. As I just halfway: I refuse ejection of exceptions and I pass to work with notifications — my code red. So, I am rolled away back to leave the validateDate method in the above-stated type, and I prepare the notification for acceptance of a conditional exception.

Starting change of the notification, I add the addError method accepting a condition then I change an initial method so that it could call a new method.

class Notification …

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

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

Thus, we accept a conditional exception, but we ignore it. That somewhere to place it, I need to turn record about an error from a simple line into a little more difficult object.

class Notification …


  private static class Error {
    String message;
    Exception cause;

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


I do not love not private fields in Java, however as here we deal with a private internal class, everything suits me. If I was going to open access to this class of an error out of the notification somewhere, then would encapsulate this field.

So, I have a class. Now it is necessary to modify the notification to use it, but not a line.

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(", "));
  }


Having the new notification, I can make changes and to request for armoring

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");

As I already in the selected method, it is easy to cancel the remained validation by means of command of return.

Last modification absolutely simple

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");
  }


Up a stack

Now, when we have a new method, the following task — to look who calls an initial test method and to modify these elements that further they used a new validation method. Here it is necessary to consider in wider context as such structure fits into the general logic of the application therefore this problem is beyond the refactoring considered here. But our medium-term task – to get rid of use of the exceptions which are without grounds applied in all cases of possible not passing of validation.

In many situations it will allow to get rid in general of a test method — then all related tests will need to be rewritten so that they worked with a validation method. Besides, correction of tests can be necessary for check of whether errors in the notification correctly accumulate.

Frameworks

A number of frameworks provides a possibility of validation using a notification pattern. In Java it is Java Bean Validation and validation of Spring. These frameworks serve as the peculiar interfaces initiating validation and using the notification for collecting of errors ( Set<ConstraintViolation> for validation and Errors in case of Spring).

Look narrowly at your language and a platform and check whether there is a validation mechanism using notifications there. Parts of this mechanism will be reflected in the refactoring course, but in general it has to turn out very probably.

This article is a translation of the original post at habrahabr.ru/post/270331/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus