Chapter 4 - Item 34 : Loosen Coupling by Using Function Parameters

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

利用方法委派當作輸入參數,能夠將實作細節與底層組件解耦;實務上可以利用介面交給客戶端實作,但這也增加了客戶端呼叫的複雜度(實作介面需要額外程式碼)。本篇提出利用方法委派當作輸入參數能夠同時達到解耦與減少客戶端複雜度的解決方案。

範例一:

考慮 .NET List<T> 提供的方法。

public int RemoveAll( predicate<T> match );

此方法可以利用介面實作。

public interface IPredicate<T>
{
    bool match( T soughtObject );
}

private class List<T>
{
    public void removeAll( IPredicate<T> match )
    {
        // eilided
    }

    // elided
}
public class MyPredicate : IPredicate<int>
{
    public bool match( int soughtObject ) =>
        soughtObject < 100;
}

此設計方式讓用戶端必須實作 IPredicate<T> 介面,增加了程式碼的數量;相較於 IComparable<T> 與 IEquatable<T> ,IPredicate<T> 並沒有明確表名自身代表意義(IComparable<T>:表示自身可被比較相對大小,IEquatable<T>:表示自身可比較是否相等)。再者,IPredicate<T> 只被特定的方法使用(removeAll),重用性並不高;因此,此方法很適合使用方法委派輸入的方式實作。

範例二:

Item 31 提到的 zip 方法如下:

public static IEnumerable<string> zip( IEnumerable<string> first,
    IEnumerable<string> second )
{
    using ( var firstSequence = first.GetEnumerator( ) )
    {
        using ( var secondSequence = second.GetEnumerator( ) )
        {
            while ( firstSequence.MoveNext( ) &&
                secondSequence.MoveNext( ) )
                yield return $"{firstSequence.Current}, {secondSequence.Current}";
        }
    }
}

我們可以利用方法委派輸入的方式,同時定義為泛型;得到更為一般化的結果。

public static IEnumerable<TResult> zip2<T1, T2, TResult>( IEnumerable<T1> first,
    IEnumerable<T2> second,
    Func<T1, T2, TResult> zipper )
{
    using ( var firstSequence = first.GetEnumerator( ) )
    {
        using ( var secondSequence = second.GetEnumerator( ) )
        {
            while ( firstSequence.MoveNext( ) &&
                secondSequence.MoveNext( ) )
                yield return zipper( firstSequence.Current,
                    secondSequence.Current );
        }
    }
}

zipper 為呼叫端自定義的方法委派,使用上非常彈性。

範例三:

public static int sum( IEnumerable<int> nums )
{
    var total = 0;

    foreach ( var num in nums )
        total += num;

    return total;
}

這個方法限制了計算 total 的演算法,利用方法委派可以讓呼叫端自定義 total 計算方式。

public static T sum<T>( IEnumerable<T> sequence,
    T total,
    Func<T, T, T> accumulator )
{
    foreach ( T item in sequence )
        total = accumulator( total, item );

    return total;
}

輸入的型別與回傳型別不一定相同,利用泛型也能輕鬆辦到。

public static TResult aggregate<T, TResult>( IEnumerable<T> sequence,
    TResult total,
    Func<T, TResult, TResult> accumulator )
{
    foreach ( T item in sequence )
        total = accumulator( item, total );

    return total;
}

使用情境:

public class Employee
{
    public int Salary { get; set; }
}

var Employees = new List<Employee>( );

var totalSalary = aggregate( Employees, 0M, ( person, sum ) =>
        sum + person.Salary );
Note:雖然方法委派很方便,但需注意物件的生命周期;當某物件雖然沒有被任何其他物件參考到,但其物件中的方法被委派參考到時,等同於仍有被物件參考。同時,方法委派的輸入方式意味著執行端需額外判斷方法委派是否為空值。
結論:
1. 介面與基類的設計方式仍是首先考量的方案;介面提供了明確的合約(可讀性高),基類則能提供可被共用的方法實作,減少客戶端的程式碼。

2. 在彈性設計為首要的目標時,方法委派則是較好的解決方案。