之前的文章有提到這位作者(Vladimir Khorikov)對函數式編程的有趣概念,趁目前有空順便翻譯下,原文:Primitive obsession
- Primitive Obsession的譯名來自Teddy的搞笑談軟工的談談壞味道(5):Data Clumps & Primitive Obsession,或者我覺得「原型狂熱」也可以 :)
本文描述的主題是我的Pluralsight課程的一部份–Applying Functional Principles in C#。
這是我的blog上Functional C#系列的第二篇文章。
- Functional C#: Immutability
- Functional C#: Primitive obsession
- Functional C#: Non-nullable reference types
- Functional C#: Handling failures and input errors
什麼是基本型別偏執?
基本型別偏執表示使用基本型別來建立領域模型。舉例來說,在典型的C#應用程式中,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原則–我們只需單一來源的真理。這表示在你程式中的每一種領域知識應只有同一個驗證的來源,上面的範例中,至少有三個。
如何去除基本型別偏執?
為了去除基本型別偏執,我們需要引入兩個新的類別,它們可以聚合整個應用程式中的所有驗證邏輯:
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));
}
// The rest is the same as in Email
}
這種方法的優點是,每當驗證邏輯(或附加到這些類別的任何其他邏輯)發生變化時,你只需要在一個地方更改它。你的重複次數越少,得到的錯誤越少,你的客戶就越快樂!
請注意,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和CustomerNam類別,剩下的唯一檢查是Null檢查,它們仍然很煩索,但我們在下一章會知道一個更好的處理的方式。
所以,在移除基本型別偏執後有什麼好處?
- 我們為每一個領域問題建立一個單一的驗證來源,沒有重覆,只有清楚及dry的程式。
- 更強的型別系統,編譯器為我們提供了雙重效果:現在不可能錯誤的將一個email指定給customer name,這會發生編譯錯誤。
- 不需要驗證傳入值,如果我們得到了一個Email或CustomerName的物件,我們可以百分百確認它處於正確的狀態。
我想指出另一個細節,一些人喜歡在單一操作中多次wrap和unwrap基本型別:
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)再對它們unwrap。在你的領域類別中,儘可能的使用它們,你會得到一個更清楚及更可維護的程式:
public void Process(Email oldEmail, Email newEmail)
{
Customer customer = GetCustomerByEmail(oldEmail);
customer.Email = newEmail;
}
另一方面:限制
不幸的是,在C#中建立的自定類別並不像函數式語言F#中那樣的簡潔。 That probably will be changed in C# 7 if we get record types and pattern matching, 但在那之前,我們要處理因這目標帶來的笨重。
因此,我發現一些簡單的基本型別不值得包裝,例如,用單一不可變量表示價格不能為負的金額仍然可以以decimal來表示,這會導致一些驗證邏輯的重覆,但–再次的說–在長期來看,這仍不失為一個較簡單的設計決策。
如之前所述,在每種狀況下評估其優缺點,不要猶豫去改變你的想法,即使會反覆多次。
結論
靠著使用不可變性及非基本型別,我們更接近以函數式思考來設計C#應用程式,下一章,我將會展示如何減輕billion dollar mistake。
原始碼
Written with StackEdit.
沒有留言:
張貼留言