機能的なC#:バグの処理

このパートでは、クラッシュとタイピングエラーを機能的なスタイルで処理する方法を見ていきます。





C#でのエラー処理:標準的なアプローチ



検証とエラー処理の概念は十分に確立されていますが、これに必要なコードはC#のような言語では非常に扱いにくい場合があります。 この記事は、NDC OsloでのプレゼンテーションでScott Wlaschinが提示したアイディアであるRailway Oriented Programmingに触発されました。



以下のコードを検討してください。



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





この方法はシンプルで簡単です。 最初にカスタムを作成し、それを保存してから手数料を請求し、最後に挨拶状を送ります。 ここでの問題は、このコードが肯定的なスクリプトのみを処理することです。すべてが計画どおりに進んだ場合のスクリプトです。



潜在的な障害、入力エラー、ロギングを考慮し始めると、メソッドは大きくなります:



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





さらに、SaveメソッドとChargeCommissionの両方のメソッドでエラーをキャッチする必要がある場合、補正メカニズムが必要です。いずれかのメソッドが失敗した場合、変更をロールバックする必要があります。



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





5行のメソッドは35行になりました。 7倍になりました! そのようなコードは非常に読みにくい セマンティックロードを運ぶ5行のコードが、テンプレートコードのヒープに「埋め込まれ」ました。



機能的なスタイルのエラー処理



この方法を修正する方法を見てみましょう。



プリミティブな強迫観念に関する記事と同じアプローチがここで使用されていることに気づいたかもしれません。名前と請求情報として文字列を使用する代わりに、CustomerNameクラスとBillingInfoクラスでそれらをラップします。



静的なCreateメソッドは、操作の結果に関するすべての情報がカプセル化された特別なResultクラスを返します。操作が失敗した場合はエラーメッセージ、成功した場合は結果です。



また、潜在的なエラーはtry / catchブロックでキャッチされることに注意してください。 これは、例外を処理する最良の方法ではありません。 ここでは、それらを最下位レベルではありません。 状況を改善するために、Createメソッドと同様に、ChargeCommissionメソッドとSaveメソッドをリファクタリングして、Resultクラスのオブジェクトを返すようにすることができます。



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





Resultクラスは、 前の記事で説明したMaybeと非常によく似ています 。これにより、ネストされたメソッドの実装の詳細を見なくてもコードを検討できます。 クラス自体は次のようになります(簡潔にするために一部の詳細は省略されています)。



 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) { /* … */ } }
      
      





これで、機能的なアプローチを使用できます。



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





関数型言語に精通している場合は、OnSuccessメソッドが実際にはBindメソッドであることに気付くかもしれません。 この特定のケースでの目的がより明確であるため、私はOnSuccessという名前を付けました。



OnSuccessメソッドは前のメソッドの結果を確認し、成功した場合はデリゲートが渡されます。 それ以外の場合は、前の結果を返します。 したがって、チェーンは、操作のいずれかがフレークするまで実行されます。 この場合、失敗して終了した操作に続く操作はスキップされます。



OnFailureメソッドは、前の操作が失敗した場合にのみ実行されます。 これは、データベースへのアクセスに失敗した場合に実行する必要がある補償ロジックの最適な場所です。



OnBothはチェーンの最後に配置されます。 主な使用例は、操作の結果を記録し、結果のメッセージを作成することです。



したがって、元のバージョンとまったく同じ動作をしますが、定型コードははるかに少なくなります。 そのようなコードを読むのはずっと簡単です。



CQSの原則はどうですか?



コマンドとクエリの分離の原則はどうですか? 上記のアプローチでは、メソッド自体がコマンド(つまり、オブジェクトの状態を変更する)であっても、戻り値(この場合はResultクラスのオブジェクト)を使用します。 このアプローチはCQSに反していますか?



いや さらに、読みやすさがさらに向上します。 上記のアプローチでは、メソッドがコマンドであるか要求であるかを確認できるだけでなく、このメソッドが失敗する可能性があるかどうかも示します。



失敗した実行に基づいた設計により、メソッドシグネチャから取得できる情報の量が増えます。 2つの可能なオプション(コマンド用のvoidとクエリ用の値)の代わりに、4があります。



メソッドはコマンドであり、失敗することはできません



 public void Save(Customer customer)
      
      





メソッドはリクエストであり、失敗することはありません



 public Customer GetById(long id)
      
      





メソッドはコマンドであり、失敗する場合があります。



 public Result Save(Customer customer)
      
      





メソッドはリクエストであり、失敗する場合があります。



 public Result<Customer> GetById(long id)
      
      





メソッドがResult <Customer>ではなくCustomerを返す場合、このようなメソッドの失敗は例外的な状況になることを意味することがわかります。



おわりに



コードを書くときの意図を明確に表現することは、コードを読みやすくするために重要です。 他の3つのプラクティス(プリミティブ型や非ゼロ参照型への執着から離れる不変の型)と組み合わせることで、このアプローチは生産性を大幅に向上させることができる非常に便利な手法です。



ソースコード



記事の例のソースコード



シリーズの他の記事






All Articles