2.02.2017

Functional C#: Immutability(譯)

之前的文章有提到這位作者(Vladimir Khorikov)對函數式編程的有趣概念,趁目前有空順便翻譯下,原文:Functional C#: Immutability

本文描述的主題是我的Pluralsight課程的一部份–Applying Functional Principles in C#

我開始這系列的文章是因為我想展示如何在C#中以一個較函數式的方法來編程。

為什麼要不可變(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.

沒有留言:

張貼留言