Tuesday, September 1, 2015

Functional C #: Exception Handling

We continue the series of articles about the functional C #. Today part of the final, and we will consider it in exception handling and error. We suggest to read the article:


Functional C #: Immutable objects
Functional C #: Obsession primitives
Functional C #: Non-zero reference types
Functional C #: Exception Handling

Error handling: the basic approach
The concept of checks and exception treatments well known to many of you, but the code is required for this can be really annoying. At least in a programming language like C #. This article is inspired by the concept Railway Oriented Programming, presented by Scott Wlaschin in his presentation. We encourage you to view the complete statement, as it will give you invaluable knowledge of how uncomfortable can be C # and how to get around.

Look at the example 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);
}
It seems that in this code everything is clear and crystal clear. We create an instance of the class Customer, store it in the repository, and then send him greetings via e-mail. But it is true exactly as long as we all passes without errors. We are in this code assumes that all functions are performed exactly. However, to utter Unfortunately, this is practically impossible.

When you start to catch errors, the code becomes like something like this:
[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);
}
It gets worse, if an error occurs, we will need to cancel the last operation. Then, our code becomes even more:
[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);
}
Finally! Now, certainly all. But there is one problem ... Our method previously consisted of 5 rows, but now as many as 35! Sevenfold increase! This is just one simple method. Moreover, now it is difficult to navigate in written. These are the most significant 5 lines of code buried under a layer of treatments exceptions.

Exception handling and input errors in a functional style
Can we do without such an extension of the source code? Fortunately, yes. Let's go through our method, and see what we can do.

You may have noticed that we use the technique described in one of our previous articles. Instead of using primitives, we use the classes. For example, CustomerName and BillingInfo. This allows you to put all the validations in one place and adhere to the principle of DRY.

The static Create method returns an object of class Result, which encapsulates all the necessary information concerning the results of the operation - an error message in case of failure, and an object instance in the case of success.

Also note that the functions that could cause mistakes, wrapped in the design of try / catch. Such an approach violates one of the best practices described in this article. The bottom line is that if you know how to deal with the exception, the process should be as low as possible. Let's rewrite the code:

[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);
}
As you can see, we wrap all possible locations of errors in the Result. The work of this class is very similar to the work of monads Maybe, which we discussed in a previous article. Using Result, we can analyze the code without looking into the details of implementation. Here is the class itself (some details omitted for brevity):
ublic 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 apply the same principle that is used in functional languages. That's where the real magic happens:
[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 programming languages, you may have noticed that the method OnSuccess - is actually a method Bind.

How works OnSuccess? The method checks the previous result and, if successful, performs the current operation. Otherwise it simply returns the last successful result. Thus, the chain continues until an error is encountered. If she meets all the other methods are simply skipped.

OnFailure method works exactly the opposite, as you might have guessed: the current method executes only if the previous operation resulted in an error.

OnBoth located at the end of the chain. It is used to display different kinds of messages and logs.

So, we wrote our desired method of exception handling and error, but with much less code. Moreover, note that it is now much easier to understand that generally makes the method.

What about the principle of CQS?
CQS - Command-Query Separation - the principle of imperative programming. It states that each method must be either a command that performs an action or a query that returns data. Do we have a conflict with this principle?

No. Moreover, our approach increases the readability of the code in the same manner as that in principle CQS. But now, the potential range of information we receive from our methods, expanded twice. Instead of 2 (a null value, and any object returned by the query) have their 4:

The method of team that can not result in an error:
public void Save (Customer customer)

Method request that can not cause an error:
public Customer GetById (long id)

Method team, which may result in an error:
public Result Save (Customer customer)

Method query, which can result in an error:
public Result GetById (long id)

When it is said that the method can not lead to error, we do not mean that the error can not be all. There is always some chance that falls exclusion (especially where it was not expected). Under the method, which can not lead to error, refers to functions that should always work without exceptions. That is, any error occurred therein is unexpected.

Conclusion
It is important to look at your code and immediately understand that it is carried out. The code must be read. But at the same time you have to pay great attention to exception handling. However, the classical approach is quite cumbersome and inconvenient. That is why it makes sense to use the technique described in this article.

In combination with the other three techniques - the same objects, getting rid of the obsession neobnulyaemymi primitives and reference types - this approach is a powerful programming model that can significantly improve performance.

By Unknown with No comments

0 коммент.:

Post a Comment