In this part we will look at how to deal with failures and input errors in a functional style.
Dealing with errors in C #: standard approach
The concept of validation and error handling is well developed, but the code needed for this can be quite awkward in languages ​​like C #. This article was inspired by Railway Oriented Programming, an idea presented by Scott Wlaschin in
his presentation at NDC Oslo.
Consider the code below:
')
[HttpPost] public HttpResponseMessage CreateCustomer(string name, string billingInfo) { Customer customer = new Customer(name); _repository.Save(customer); _paymentGateway.ChargeCommission(billingInfo); _emailSender.SendGreetings(name); return new HttpResponseMessage(HttpStatusCode.OK); }
The method is simple and straightforward. First, we create a customer, then save it, then we charge a commission and, finally, send a letter of welcome. The problem here is that this code processes only the positive scenario - the scenario when everything goes according to plan.
If we start to consider potential failures, input errors and logging, the method will grow strongly:
[HttpPost] public HttpResponseMessage CreateCustomer(string name, string billingInfo) { Result<CustomerName> customerNameResult = CustomerName.Create(name); if (customerNameResult.Failure) { _logger.Log(customerNameResult.Error); return Error(customerNameResult.Error); } Result<BillingInfo> billingInfoResult = BillingInfo.Create(billingInfo); if (billingInfoResult.Failure) { _logger.Log(billingInfoResult.Error); return Error(billingInfoResult.Error); } Customer customer = new Customer(customerNameResult.Value); try { _repository.Save(customer); } catch (SqlException) { _logger.Log(“Unable to connect to database”); return Error(“Unable to connect to database”); } _paymentGateway.ChargeCommission(billingInfoResult.Value); _emailSender.SendGreetings(customerNameResult.Value); return new HttpResponseMessage(HttpStatusCode.OK); }
Moreover, if we need to catch errors in both methods, Save and ChargeCommission, there is a need for a compensation mechanism: we must roll back the changes in case one of the methods fails:
[HttpPost] public HttpResponseMessage CreateCustomer(string name, string billingInfo) { Result<CustomerName> customerNameResult = CustomerName.Create(name); if (customerNameResult.Failure) { _logger.Log(customerNameResult.Error); return Error(customerNameResult.Error); } Result<BillingInfo> billingIntoResult = BillingInfo.Create(billingInfo); if (billingIntoResult.Failure) { _logger.Log(billingIntoResult.Error); return Error(billingIntoResult.Error); } try { _paymentGateway.ChargeCommission(billingIntoResult.Value); } catch (FailureException) { _logger.Log(“Unable to connect to payment gateway”); return Error(“Unable to connect to payment gateway”); } Customer customer = new Customer(customerNameResult.Value); try { _repository.Save(customer); } catch (SqlException) { _paymentGateway.RollbackLastTransaction(); _logger.Log(“Unable to connect to database”); return Error(“Unable to connect to database”); } _emailSender.SendGreetings(customerNameResult.Value); return new HttpResponseMessage(HttpStatusCode.OK); }
Our 5-line method turned into 35 lines, i.e. became 7 times more! This code is quite difficult to read, because 5 lines of code carrying meaning are now “buried” in a heap of sample code.
Functional style error handling
Let's see how to fix this method.
You may have noticed that the same approach is used here as in the article on the
primitive obsession : instead of using strings as a name and billing information, we wrap them in the CustomerName and BillingInfo classes.
The static Create method returns a special Result class in which all information regarding the operation results is encapsulated: an error message in case the operation failed and the result in case it was successful.
Also note that potential errors are caught by try / catch blocks. This is
not the best way to handle exceptions , since here we catch them not at the lowest level. To remedy the situation, we can refactor the ChargeCommission and Save methods so that they return a Result object, just like the Create method does:
[HttpPost] public HttpResponseMessage CreateCustomer(string name, string billingInfo) { Result<CustomerName> customerNameResult = CustomerName.Create(name); if (customerNameResult.Failure) { _logger.Log(customerNameResult.Error); return Error(customerNameResult.Error); } Result<BillingInfo> billingIntoResult = BillingInfo.Create(billingInfo); if (billingIntoResult.Failure) { _logger.Log(billingIntoResult.Error); return Error(billingIntoResult.Error); } Result chargeResult = _paymentGateway.ChargeCommission(billingIntoResult.Value); if (chargeResult.Failure) { _logger.Log(chargeResult.Error); return Error(chargeResult.Error); } Customer customer = new Customer(customerNameResult.Value); Result saveResult = _repository.Save(customer); if (saveResult.Failure) { _paymentGateway.RollbackLastTransaction(); _logger.Log(saveResult.Error); return Error(saveResult.Error); } _emailSender.SendGreetings(customerNameResult.Value); return new HttpResponseMessage(HttpStatusCode.OK); }
The Result class is quite similar to Maybe, discussed in the
previous article : it allows us to think about the code without looking at the implementation details of the nested methods. Here is the class itself (some details omitted for brevity):
public class Result { public bool Success { get; private set; } public string Error { get; private set; } public bool Failure { } protected Result(bool success, string error) { } public static Result Fail(string message) { } public static Result<T> Ok<T>(T value) { } } public class Result<T> : Result { public T Value { get; set; } protected internal Result(T value, bool success, string error) : base(success, error) { } }
Now we can use the functional approach:
[HttpPost] public HttpResponseMessage CreateCustomer(string name, string billingInfo) { Result<BillingInfo> billingInfoResult = BillingInfo.Create(billingInfo); Result<CustomerName> customerNameResult = CustomerName.Create(name); return Result.Combine(billingInfoResult, customerNameResult) .OnSuccess(() => _paymentGateway.ChargeCommission(billingInfoResult.Value)) .OnSuccess(() => new Customer(customerNameResult.Value)) .OnSuccess( customer => _repository.Save(customer) .OnFailure(() => _paymentGateway.RollbackLastTransaction()) ) .OnSuccess(() => _emailSender.SendGreetings(customerNameResult.Value)) .OnBoth(result => Log(result)) .OnBoth(result => CreateResponseMessage(result)); }
If you are familiar with functional languages, you may notice that the OnSuccess method is actually the Bind method. I called it OnSuccess because its purpose is more clear in this particular case.
The OnSuccess method checks the result of the previous method execution and if it is successful, the delegate that is passed. Otherwise, it returns the previous result. Thus, the chain is executed until one of the operations fails. In this case, the operations following the one that ended in failure will be skipped.
The OnFailure method is executed only if the previous operation failed. This is a great place for compensation logic, which we have to activate in case the access to the database failed.
OnBoth is located at the end of the chain. The main usage scenarios for it are the logging of the operation results and the creation of the resulting message.
Thus, we have exactly the same behavior as in the original version, but with a much smaller amount of template code. Read this code is much easier.
What about the CQS principle?
What about the principle of
Command-Query Separation ? The approach described above uses return values ​​(which, in our case, are objects of the Result class) even if the method itself is a command (that is, changes the state of the object). Does this approach contradict CQS?
Not. Moreover, it improves readability even more. The approach described above not only allows you to find out whether a method is a command or a request, it also indicates whether or not this method can fail.
Designing for unsuccessful execution expands the amount of information that we can get from the method signature. Instead of two possible options (void for commands and some value for queries), we now have 4.
The method is a command and cannot fail :
public void Save(Customer customer)
The method is a request and cannot fail .
public Customer GetById(long id)
The method is a command and may fail :
public Result Save(Customer customer)
The method is a request and may fail :
public Result<Customer> GetById(long id)
Now we can see that if the method returns Customer, and not Result <Customer>, this means that failure in such a method will be an
exceptional situation .
Conclusion
Clearly expressing your intentions when writing code is extremely important for improving its readability. Combined with the other three practices — unchangeable types, a departure from primitive obsession and non-zero reference types — this approach is a fairly useful technique that can significantly increase your productivity.
Sources
Source code examples from the articleThe rest of the articles in the series