📜 ⬆️ ⬇️

Replacing jitter exceptions

I bring to your attention a translation of the article " Replace Throw With Notification " by Martin Fowler. Examples are adapted for .NET.

If we validate the data, we usually should not use exceptions to report validation errors. Here I will describe how to refactor such a code using the “Notification” pattern.


')
Recently I looked at the code that did the basic validation of incoming JSON messages. It looked like this ...

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

This is a general approach to the implementation of validation. A series of checks is started for some data (in the example above, two parameters are validated). If any check fails, an exception is thrown with an error message.

This approach has several drawbacks. First, we should not use exceptions in this way. Exceptions signal that something has gone beyond the boundaries of the expected behavior code. But if we do checks on input parameters, it is because we expect an error message, and if the error is the expected behavior, then we should not use exceptions.
If the error is the expected behavior, then we should not use exceptions.

The second problem with this code is that it crashes with the first error found, but it is usually better to report all errors with the input data, not only with the first one. In this case, the client can choose to show all errors to the user so that he can correct them in one go. This is better than giving the user the impression that he is playing a sea battle with a computer.

Preferably, in the cases above, use the “Notification” pattern. A notification is an object that collects errors. Each validation error adds an error to the notification. The validation object returns a notification that we can integrate to get the information. A simple example of use is as follows.

 private void ValidateNumberOfSeats(Notification note) { if (numberOfSeats < 1) note.addError("     "); //  ,    } 

Then we can simply call the Notification.hasErrors () method to respond to errors. Other Notification methods can provide more error details.

if (numberOfSeats <1) throw new ArgumentException ("The number of places must be a positive number");


  if (numberOfSeats < 1) note.addError("     "); return note; 


When to use this refactoring


I should note that I do not advocate avoiding the use of exceptions in your code. Exceptions are a very useful technique for handling an unexpected situation and separating it from the main flow of logic. This refactoring makes sense when the output signal as an exception is not an exceptional situation, and therefore must be processed by the main logic of the program. The code above is a common example of such a situation.

A good rule for using exceptions is found in the Pragmatic Programmers book:

We believe that exceptions should rarely be used as part of the normal flow of the program: exceptions should be reserved for unexpected situations. Imagine that an unhandled exception completes your program and ask yourself: “Will this code still work if I remove all exception handlers?” If the answer is “no,” then perhaps exceptions were used as part of the normal program flow.

- Dave Thomas and Andy Hunt

An important conclusion to be drawn from this is that the decision whether to apply exceptions for a specific task depends on the context. Reading a file that does not exist may or may not be an exceptional situation. If we are trying to read a file in a well-known path, for example, / etc / hosts in Unix, then we can assume that the file should be here, so throwing an exception makes sense. On the other hand, if we read the file along the path passed by the user via the command line, then we should expect that there is probably no file here and use a different mechanism for interacting with a non-exclusive error in nature.

There are cases where the use of exceptions for validation errors may be appropriate. These are situations where there is data that has already been validated during processing, but we want to run the validation again to secure
yourself from a program error that allows invalid data to slip through.

This article is about replacing exceptions with notifications in the context of validating raw input. You can also find this technique useful in other situations where notifications are a better choice than throwing an exception, but focus on the validation case, since it is most often found.

Starting point


So far we have not mentioned the business logic, since it was important to concentrate on the general form of the code. But for further discussion, we need to get some information about business logic. In this case, some code receives JSON messages with reserved seats in the theater. The code is in the BookingRequest class, derived from JSON using the JSON.NET library.

  JsonConvert.DeserializeObject<BookingRequest>(json); 

The BookingRequest class contains only two elements that we validate here: the date of the performance and how many seats were requested.

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

Validation has already been shown above.

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

Create notification


To use notifications, we need to create a Notification object. Notification can be quite simple, sometimes just a List.

  var notification = new List<string>(); if (NumberOfSeats < 5) notification.add("      5"); //   // … if (notification.Any()) //   

Although the implementation above also allows the use of a pattern, it is preferable to do a little more, creating a simple class instead.

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

Using a special class we make our intentions more obvious - the reader does not need to create a mental map between ideas and its implementation.

We divide method Check


Our first step will be to divide the Check method into two parts, the internal part will deal only with notifications and not throw exceptions, and the external part will keep the current behavior of the Check method, which throws exceptions, if any error is detected.

Using the method "selection method", we put the body of the function Check into the function 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("     "); } 

Then we extend the Validation method with creating Notification and returning it from the function.

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

Now I can check Notification and throw an exception if it contains errors.

 public void heck() { var notification = Validation(); if (notification.HasErrors) throw new ArgumentException(notification.ErrorMessage); } 

We have made the Validation method open, as it is expected that most users in the future will prefer to use this method rather than Check.

By the current moment, we have not changed the behavior of the code at all, all the validation checks that have fallen will continue to throw exceptions, but we have created a base to begin replacing the emission of exceptions with notifications.

Separating the original method allowed us to separate validation from reaction to its results.

Before we continue, a few words should be said about the error messages. When we do refactoring, it is important to avoid changes in the observed behavior. This rule leads us to the question of what behavior is observable. Obviously, throwing an exception is what the external program will observe, but to what extent do they care about the error message? Notification will collect many error messages and merge them into one, for example in this way.

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

But there may be a problem with the higher layers of the program, which rely on getting only the first error detected. In this case, we should implement it as follows.

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

We should look not only at the called function, but also at the existing handlers to determine the correct behavior in a particular situation.

Validation number


The obvious thing to do is replace the first check.

 public Notification Validation() { var notification = new Notification(); if (Date == null) notification.AddError("  "); //... } 

Obvious replacement, but bad, because it breaks the code. If we pass null as an argument for Date, we add an error to the Notification object, the code will continue to run, and when parsed we get a NullReferenceException in the DateTime.Parse method. This is not what we want to get.

Unobvious, but more effective, what needs to be done in this case is to go from the end of the method.

 public Notification Validation() { //... if (NumberOfSeats < 1) notification.AddError("     "); } 

The next check is a check for null, so we must add a condition to avoid a NullReferenceException

 public Notification Validation() { //... if (NumberOfSeats == null) notification.AddError("   "); else if (NumberOfSeats < 1) notification.AddError("     "); } 

As we can see, the following checkout includes another field. And these checks should also be taken into account in the checks of another field. The verification method becomes too complicated. Therefore, we put the NumberOfSeats checks into a separate method.

 public Notification Validation() { //... ValidateNumberOfSeats(notification); } private void ValidateNumberOfSeats(Notification notification) { if (NumberOfSeats == null) notification.AddError("   "); else if (NumberOfSeats < 1) notification.AddError("     "); } 

When we look at the highlighted validation for a number, it doesn’t look very natural. Using if-then-else blocks for validation can easily lead to overly nested code. It is more preferable to use a linear code that breaks if it cannot go further, which we can implement using the protection condition.

 private void ValidateNumberOfSeats(Notification notification) { if (NumberOfSeats == null) { notification.AddError("   "); return; } if (NumberOfSeats < 1) notification.AddError("     "); } 

Our decision to go from the end of the method to leave the code to the workers demonstrates the basic principle of refactoring. Refactoring is a special technique of code restructuring through a series of behavior-preserving transformations. Therefore, when we refactor, we should always try to take the smallest steps that preserve behavior. By doing this, we reduce the likelihood of an error that will cause us to debug.

Date validation


Let's start with the removal of checks for the date in a separate method.

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

Then, as in the case of the number, we begin to replace the exception at the end of the method.

 private void ValidateNumberOfSeats(Notification notification) { //... if (parsedDate < DateTime.Now) notification.AddError("     "); } 

In the next step, there is a slight difficulty in catching the exception, since the released exception includes the original exception. To handle this, we need to change the Notification class to accept the exception.

Add the Exception parameter to the AddError method and set the default value to null.

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

This means we accept the exception, but ignore it. To put it somewhere we need to change the type of error inside the Notification class from string to a more complex object. Create an Error class inside Notification.

 private class Error { public string Message { get; set; } public Exception Exception { get; set; } public Error(string message, Exception exception) { Message = message; Exception = exception; } } 

Now we have a class and it remains for us to change the Notification to use it.

 //... 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)); } } 

With a new notice in place, we can now make changes to the booking request.

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

And the last change is quite simple.

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

Conclusion


Once we have converted the method using the notification mechanism, the next task will be to look at the places where the Check method is invoked and think about the possibility of using Validate instead. To do this, it is necessary to analyze how the validation falls into the current implementation of the application; this goes beyond the scope of refactoring discussed here. But in the medium term, there should be a goal: to exclude the use of exceptions in all circumstances where validation errors are expected.

In many cases, this should lead to getting rid of the Check method completely. In this case, any tests for this method should be updated using the Validation method. We may also want to add tests that validate the correct collection of multiple errors using a notification.

Source: https://habr.com/ru/post/279737/


All Articles