1.18.2017

Railway Oriented Programming(軌道式編程?)

All the picture coming from http://fsharpforfunandprofit.com or http://www.slideshare.net

最近看到這個很有趣的設計概念,後來發現原來之前有介紹過的Functional Programming就是此概念的一種實作

作者scottw其實是以F#介紹如何設計並完成一支程式,分為三篇文章:

  1. How to design and code a complete program
  2. Railway Oriented Programming,並提供影音投影片介紹
  3. Organizing modules in a project

作者以一個簡單的use case當做範例,基本上是使用者更新資訊的一個處理過程:
UserUpdateInfo

轉個方向來看:
ProcessingFlow

再延伸成:
RailwayProcessing

基本上綠色成功路徑作者稱之為:Happy path,紅色錯誤路徑當然是Unhappy path了。

Happy path的程式可表示為:
enter image description here

一般加上簡單的錯誤處理或Log後可能會變為:
FlowWithErrorHandling

整個程式可能由單頁變成雙頁,對可讀性和維護性都會有些許的困擾,還好目前在C#至少有基於此概念的兩套函式庫可參考,介紹如下。

Vladimir Khorikov在Functional C#: Handling failures, input errors的文章

NuGet

Happy path:

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

Error Handling:

blablabla…

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

ROP way

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

噹噹!看到這裡你的內心應該會感動很多下吧…

stormy-ua的Railway

NuGet

ROP way

    // combine validation functions
    var combinedValidation = Railway
        // log inpiut request
        .Apply<Request> (r => LogRequest(r))
        // do email and name validation in parallel and combine errors
        .OnSuccess(
            (r1, r2) => r1,
            (e1, e2) => new AggregateException(e1, e2),
            r => ValidateName(r),
            r => ValidateEmail(r)
        )
        // extract request name
        .OnSuccess (request => request.Name)
        // log extracted name
        .OnSuccess (name => Console.WriteLine ("Name: {0}", name))
        // append dash to name
        .OnSuccess (name => name + "-")
        // log name
        .OnSuccess (name => Console.WriteLine ("Name: {0}", name))
        // make nume uppercase
        .OnSuccess (name => name.ToUpper ())
        // log name
        .OnSuccess (name => Console.WriteLine ("Name: {0}", name))
        // log failure if any occured during the pipeline execution
        .OnFailure (e => Console.WriteLine ("Failure: {0} ", e.Message));

    // invoke combined function
    var result = combinedValidation (new Request { Name = "", Email = "" });
    //var result = combinedValidation (new Request { Name = "", Email = "a@b.c" });
    //var result = combinedValidation (new Request { Name = "Kirill", Email = "" });
    //var result = combinedValidation (new Request { Name = "Kirill", Email = "a@b.c" });

    // process result
    switch (result.IsSuccess) {
    case true:
        Console.WriteLine ("Success. {0}", result.Value);
        break;
    case false:
        Console.WriteLine ("Failure: {0}", result.Error);
        break;
    }

基本上我會選用第一種,不過這兩個函式庫的實作方式都值得瞭解。

Written with StackEdit.

1 則留言: