機能的なC#:プリミティブな強迫観念

これは、機能C#に関する記事のミニサイクルの2番目の記事です。





原始的な強迫観念とは何ですか?



要するに、これは、主にアプリケーションドメインのモデリングにプリミティブ型(string、intなど)が使用される場合です。 たとえば、一般的なアプリケーションでCustomerクラスがどのように見えるかを次に示します。



public class Customer { public string Name { get; private set; } public string Email { get; private set; } public Customer(string name, string email) { Name = name; Email = email; } }
      
      





ここでの問題は、いくつかのビジネスルールを実施する必要がある場合、クラスコード全体に検証ロジックを複製する必要があることです。



 public class Customer { public string Name { get; private set; } public string Email { get; private set; } public Customer(string name, string email) { // Validate name if (string.IsNullOrWhiteSpace(name) || name.Length > 50) throw new ArgumentException(“Name is invalid”); // Validate e-mail if (string.IsNullOrWhiteSpace(email) || email.Length > 100) throw new ArgumentException(“E-mail is invalid”); if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”)) throw new ArgumentException(“E-mail is invalid”); Name = name; Email = email; } public void ChangeName(string name) { // Validate name if (string.IsNullOrWhiteSpace(name) || name.Length > 50) throw new ArgumentException(“Name is invalid”); Name = name; } public void ChangeEmail(string email) { // Validate e-mail if (string.IsNullOrWhiteSpace(email) || email.Length > 100) throw new ArgumentException(“E-mail is invalid”); if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”)) throw new ArgumentException(“E-mail is invalid”); Email = email; } }
      
      





さらに、まったく同じコードがアプリケーション層に分類される傾向があります。



 [HttpPost] public ActionResult CreateCustomer(CustomerInfo customerInfo) { if (!ModelState.IsValid) return View(customerInfo); Customer customer = new Customer(customerInfo.Name, customerInfo.Email); // Rest of the method } public class CustomerInfo { [Required(ErrorMessage = “Name is required”)] [StringLength(50, ErrorMessage = “Name is too long”)] public string Name { get; set; } [Required(ErrorMessage = “E-mail is required”)] [RegularExpression(@”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”, ErrorMessage = “Invalid e-mail address”)] [StringLength(100, ErrorMessage = “E-mail is too long”)] public string Email { get; set; } }
      
      





明らかに、このアプローチはDRY原則に違反しています。 この原則は、ドメイン情報の各部分が私たちのアプリケーションのコードに単一の信頼できるソースを持つべきであることを教えています 。 上記の例では、このようなソースが3つあります。



プリミティブへの執着を取り除く方法は?



プリミティブに対する執着を取り除くには、検証ロジックを集約する2つの新しいタイプを追加する必要があります。 このようにして、重複を取り除くことができます。



 public class Email { private readonly string _value; private Email(string value) { _value = value; } public static Result<Email> Create(string email) { if (string.IsNullOrWhiteSpace(email)) return Result.Fail<Email>(“E-mail can't be empty”); if (email.Length > 100) return Result.Fail<Email>(“E-mail is too long”); if (!Regex.IsMatch(email, @”^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$”)) return Result.Fail<Email>(“E-mail is invalid”); return Result.Ok(new Email(email)); } public static implicit operator string(Email email) { return email._value; } public override bool Equals(object obj) { Email email = obj as Email; if (ReferenceEquals(email, null)) return false; return _value == email._value; } public override int GetHashCode() { return _value.GetHashCode(); } } public class CustomerName { public static Result<CustomerName> Create(string name) { if (string.IsNullOrWhiteSpace(name)) return Result.Fail<CustomerName>(“Name can't be empty”); if (name.Length > 50) return Result.Fail<CustomerName>(“Name is too long”); return Result.Ok(new CustomerName(name)); } //     ,  Email }
      
      





このアプローチの利点は、検証のロジックが変更された場合、この変更を一度だけ反映すれば十分であることです。



Emailクラスのコンストラクタは閉じられているため、インスタンスを作成する唯一の方法は、必要なすべての検証を実行する静的なCreateメソッドを使用することです。 このアプローチにより、Emailクラスのすべてのインスタンスが有効な状態にあることを確認できます。



コントローラがこれらのクラスを使用する方法は次のとおりです。



 [HttpPost] public ActionResult CreateCustomer(CustomerInfo customerInfo) { Result<Email> emailResult = Email.Create(customerInfo.Email); Result<CustomerName> nameResult = CustomerName.Create(customerInfo.Name); if (emailResult.Failure) ModelState.AddModelError(“Email”, emailResult.Error); if (nameResult.Failure) ModelState.AddModelError(“Name”, nameResult.Error); if (!ModelState.IsValid) return View(customerInfo); Customer customer = new Customer(nameResult.Value, emailResult.Value); // Rest of the method }
      
      





Result <Email>およびResult <CustomerName>インスタンスは、Createメソッドが失敗する可能性があることを明示的に示しており、失敗した場合は、Errorプロパティを読み取ることで理由を見つけることができます。



リファクタリング後のCustomerクラスの外観は次のとおりです。



 public class Customer { public CustomerName Name { get; private set; } public Email Email { get; private set; } public Customer(CustomerName name, Email email) { if (name == null) throw new ArgumentNullException(“name”); if (email == null) throw new ArgumentNullException(“email”); Name = name; Email = email; } public void ChangeName(CustomerName name) { if (name == null) throw new ArgumentNullException(“name”); Name = name; } public void ChangeEmail(Email email) { if (email == null) throw new ArgumentNullException(“email”); Email = email; } }
      
      





ほとんどすべてのチェックがEmailとCustomerNameに移動しました。 残りの検証は、nullチェックのみです。 次の記事でそれを取り除く方法を見ていきます。



それで、プリミティブへの執着を取り除くことの利点は何ですか?





小さな発言。 一部の開発者は、単一の操作中にプリミティブ型を複数回「ラップ」および「拡張」する傾向があります。



 public void Process(string oldEmail, string newEmail) { Result<Email> oldEmailResult = Email.Create(oldEmail); Result<Email> newEmailResult = Email.Create(newEmail); if (oldEmailResult.Failure || newEmailResult.Failure) return; string oldEmailValue = oldEmailResult.Value; Customer customer = GetCustomerByEmail(oldEmailValue); customer.Email = newEmailResult.Value; }
      
      





アプリケーション全体でカスタムタイプを使用し、ドメインの境界を超えた場合にのみプリミティブに拡張することをお勧めします。たとえば、データベースに保存したり、HTMLでレンダリングしたりします。 ドメインクラスでは、常にカスタムタイプを使用してください;この場合、コードはよりシンプルで読みやすくなります。



 public void Process(Email oldEmail, Email newEmail) { Customer customer = GetCustomerByEmail(oldEmail); customer.Email = newEmail; }
      
      





制限事項



残念ながら、C#でラッパータイプを作成することは、たとえばF#ほど簡単ではありません。 パターンマッチングとレコードタイプが言語レベルで実装されている場合、これはC#7で変更される可能性があります。 それまでは、このアプローチの不器用さに対処する必要があります。



このため、一部のプリミティブ型はラッピングする価値がありません。 たとえば、金額が負になることはできないという単一の不変式を持つ「金額」タイプは、通常の小数として表すことができます。 これにより、検証ロジックが多少重複することになりますが、それにもかかわらず、このアプローチは長期的にもよりシンプルなソリューションになります。



いつものように、常識を使用して、それぞれの場合の決定の長所と短所を比較検討してください。



おわりに



不変および非プリミティブ型により、より機能的なスタイルでC#アプリケーションを設計することに近づきます。 次の記事では、「10億ドルの間違いを緩和する」方法を緩和する方法について説明します。



ソースコード





シリーズの他の記事





記事の英語版: Functional C#:プリミティブオブセッション



All Articles