之前的文章有提到這位作者(Vladimir Khorikov)對函數式編程的有趣概念,趁目前有空順便翻譯下,原文:Functional C#: Immutability
本文描述的主題是我的Pluralsight課程的一部份–Applying Functional Principles in C#。
我開始這系列的文章是因為我想展示如何在C#中以一個較函數式的方法來編程。
- Functional C#: Immutability
- Functional C#: Primitive obsession
- Functional C#: Non-nullable reference types
- Functional C#: Handling failures and input errors
為什麼要不可變(Immutability)?
在開發商用軟體時最大的問題是程式的複雜性。而程式碼的可讀性應是你在開發專案時的首要目標,沒有它,你無法對你的軟體的正確性做出適當的判斷,或者會大大的減低你判斷的合理性。
可變物件會增加或減少程式可讀性?讓我們來看個範例:
// Create search criteria
var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10);
// Search customers
IReadOnlyCollection<Customer> customers = Search(queryObject);
// Adjust criteria if nothing found
if (customers.Count == 0)
AdjustSearchCriteria(queryObject, name);
// Is queryObject changed here?
Search(queryObject);
在我們第二次搜尋客戶時,查詢物件是否已改變?也許是,也許不是。這取決於第一次時我們是否有找到,以及AdjustSearchCriteria
函式是否更改條件。要知道到底發生了什麼,我們需要查看AdjustSearchCriteria
的實作程式,無法僅從它的函式定義來瞭解。
現在將它和以下程式比較:
// Create search criteria
var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10);
// Search customers
IReadOnlyCollection<Customer> customers = Search(queryObject);
if (customers.Count == 0)
{
// Adjust criteria if nothing found
QueryObject<Customer> newQueryObject = AdjustSearchCriteria(queryObject, name);
Search(newQueryObject);
}
現在很清楚,AdjustSearchCriteria
函式會以新的條件建立一個新的查詢物件,再執行搜尋。
所以,可變資料結果有什麼問題?
* 不易從程式判斷,如果你無法確定資料是否被變更。
* 不易循序閱讀程式,如果你無法從函式本身判斷,而需要知道函式如何被撰寫。
* 如果是在多執行緒程式中,對程式循序閱讀及除錯會更困難
如何建立不可變型別?
如果你有一個相對簡單的類別,你應該總是考慮讓它成為不可變的。這個經驗規則與Value Objects的概念相關:值物件很簡單,且易於讓它成為不可變的。
那麼我們要如何建立不可變型別?讓我們看個範例,假設我們有一個名為ProductPile
類別,代表一堆我們要販賣的產品:
public class ProductPile
{
public string ProductName { get; set; }
public int Amount { get; set; }
public decimal Price { get; set; }
}
要讓它不可變,我們要讓寫的屬性是唯讀的,並新增一個建構式:
public class ProductPile
{
public string ProductName { get; private set; }
public int Amount { get; private set; }
public decimal Price { get; private set; }
public ProductPile(string productName, int amount, decimal price)
{
Contracts.Require(!string.IsNullOrWhiteSpace(productName));
Contracts.Require(amount >= 0);
Contracts.Require(price > 0);
ProductName = productName;
Amount = amount;
Price = price;
}
}
假設我們要在賣出一個產品後要減少一個產品數量,我們從現有的物件中建立一個新物件,而不是直接減少當前的產品數量。
public class ProductPile
{
public string ProductName { get; private set; }
public int Amount { get; private set; }
public decimal Price { get; private set; }
public ProductPile(string productName, int amount, decimal price)
{
Contracts.Require(!string.IsNullOrWhiteSpace(productName));
Contracts.Require(amount >= 0);
Contracts.Require(price > 0);
ProductName = productName;
Amount = amount;
Price = price;
}
public ProductPile SubtractOne()
{
return new ProductPile(ProductName, Amount – 1, Price);
}
}
那麼我們在這裡得到什麼?
- 不可變類別,我們只需要在建構式中驗證一次其代碼合約。
- 我們絕對確定物件總是在一個正確的狀態。
- 物件是自動為執行緒安全的。
- 程式的可讀性增加了,因為我們不需要進入函式定義去確認它沒有變更任何東西。
它有限制嗎?
當然,每件事情都有它的代價。越小越簡單的類別可得到越多的好處,但對大型類別來說不是。
第一,這會增加效能上的考量。如果你的物件很大,只為了改變一個屬性而複製了整個物件,對程式的效能會有影響。
這裡有一個好例子:Immutable collections。他們的作者考慮了潛在的效能問題,並增加了允許改變集合的Builder類別。若(資料)準備完成後,你可以轉換成一個不可變集合以結束它。
var builder = ImmutableList.CreateBuilder<string>();
builder.Add(“1”); // Adds item to the existing object
ImmutableList<string> list = builder.ToImmutable();
ImmutableList<string> list2 = list.Add(“2”); // Creates a new object with 2 items
另一個問題是,一些類別本身是可變的,若你試著讓它們變為不可變,帶來的問題可能比預解決的問題還多。
但不要讓這些問題阻止你建立不可變資料型別。要考慮到每個設計決策的優缺點,再評估是否使用。
結論
在大多情況下,你總是能從不可變性中得到好處,特別是你讓你的類別總是保持小而美時。
Written with StackEdit.
沒有留言:
張貼留言