[Design Pattern]Decorator Pattern with IComparer - 先比這個,再比那個
前言
先前寫了一篇文章,是針對 Decorator Pattern 的讀書筆記:[Design Pattern]Decorator Pattern - 讀書筆記,不過 Design Pattern 就是如此,我自以為懂了,卻在最近 survey LINQ 的 ThenBy() 時,發現自己真的還無法靈活運用。
這邊就先以 IComparer<T> 為例,解釋一下怎麼透過 Decorator Pattern 來滿足我們的需求: 針對兩個 entity ,先比這個,再比那個。
一般的 IComparer<T> 使用方式
首先說明一下 IComparer<T> 的意義,請見下圖:
IComparer<T> 的目的,就是為了說明:兩個型別為 T 的物件,怎麼比較大小。
舉個例子,假設有個 class 為 Person ,結構如下:
public class Person
{
public int Id { get; set; }
public int Salary { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
假設需求是希望比較兩個 Person 大小時,是比較 Salary 的值來決定大小,那麼只要新增一個 PersonSalaryComparer 的 class ,實作 IComparer<Person> 即可,如下所示:
public class PersonSalaryComparer : IComparer<Person>
{
int IComparer<Person>.Compare(Person x, Person y)
{
return x.Salary.CompareTo(y.Salary);
}
}
怎麼使用 PersonSalaryComparer 呢?這邊透過測試案例來說明:
[TestMethod]
public void Test_PersonSalaryComparer()
{
var person1 = new Person { Id = 1, Salary = 200, Name = "Cat", Age = 15 };
var person2 = new Person { Id = 1, Salary = 100, Name = "Dad", Age = 15 };
IComparer<Person> salaryComparer = new PersonSalaryComparer();
var result = salaryComparer.Compare(person1, person2);
// person1的Salary比person2的Salary大,因此回傳值>0
Assert.IsTrue(result > 0);
}
常用 IComparer<T> 的場合,基本上就是排序,因為排序需要「比較大小」。例如:List<T>.Sort(IComparer<T>) 或是 OrderBy(Func<TSource, TKey>, IComparer<TKey>) 。
需求:先比這個,再比那個
假設我們的需求,希望兩個 Person 比較大小時,可以先比較 Age, 比不出來再比 Name, 仍比不出來的話再比 Salary,這時候通常會怎麼作呢?可能會寫出一個 PersonComplexComparer<T> 來實作這樣的邏輯,如下所示:
public class PersonComplexComparer : IComparer<Person>
{
//先比較 Age, 比不出來再比 Name, 仍比不出來的話再比 Salary
int IComparer<Person>.Compare(Person x, Person y)
{
if (x.Age.CompareTo(y.Age) != 0)
{
return x.Age.CompareTo(y.Age);
}
else if (x.Name.CompareTo(y.Name) != 0)
{
return x.Name.CompareTo(y.Name);
}
else if (x.Salary.CompareTo(y.Salary) != 0)
{
return x.Salary.CompareTo(y.Salary);
}
else
{
return 0;
}
}
}
來看一下測試案例:
[TestMethod]
public void Test_ComplexComparer()
{
var persons = new List<Person>
{
new Person {Id = 1, Salary = 200, Name = "Cat", Age = 15},
new Person {Id = 2, Salary = 200, Name = "Bob", Age = 14},
new Person {Id = 3, Salary = 100, Name = "Alice", Age = 15},
new Person {Id = 4, Salary = 50, Name = "Alice", Age = 15},
};
IComparer<Person> comparer = new PersonComplexComparer();
//1跟2比, 因為Age, 1比較大
var result1 = comparer.Compare(persons[0], persons[1]);
Assert.IsTrue(result1 > 0);
//1跟3比, 因為Name, 1比較大
var result2 = comparer.Compare(persons[0], persons[2]);
Assert.IsTrue(result2 > 0);
//3跟4比, 因為Salary, 3比較大
var result3 = comparer.Compare(persons[2], persons[3]);
Assert.IsTrue(result3 > 0);
}
雖然上面的 PersonComplexComparer<T> 可以滿足我們先比 Age, 再比 Name, 再比 Salary 的需求,然而這樣的設計卻是相當沒有彈性。
假設今天的需求,要怎麼組合比較的方式是希望由呼叫端來決定怎麼比,用什麼順序比,而不是寫死的判斷式,那該怎麼設計才有彈性呢?
Decorator Pattern with IComparer<T>
先來看一下 Decorator Pattern 的 class diagram:
這時候,將上圖的 IComponent 換成 IComparer<Person> , 將 Component 的部分取代成 PersonSalaryComparer 、 PersonAgeComparer 以及 PerseonNameComparer。如下所示:
public class PersonSalaryComparer : IComparer<Person>
{
int IComparer<Person>.Compare(Person x, Person y)
{
return x.Salary.CompareTo(y.Salary);
}
}
public class PersonNameComparer : IComparer<Person>
{
int IComparer<Person>.Compare(Person x, Person y)
{
return x.Name.CompareTo(y.Name);
}
}
public class PersonAgeComparer : IComparer<Person>
{
int IComparer<Person>.Compare(Person x, Person y)
{
return x.Age.CompareTo(y.Age);
}
}
Decorator 的部分,則新定義一個 class 稱為 ComboComparer<T> , 一樣實作 IComparer<T> 。這時定義 ComboComparer<T> 的 constructor ,需傳入兩個 IComparer<T>,如下所示:
將傳入的兩個 IComparer<T> 暫存起來,因為實作 Compare() 方法時,我們的目的是希望比較兩個 T 物件時,先比較第一個 comparer ,比不出來時才比較第二個 comparer 。
達成這樣目的的方法內容如下:
public int Compare(TSource x, TSource y)
{
var untilNowComparerResult = this._untilNowComparer.Compare(x, y);
if (untilNowComparerResult != 0)
{
return untilNowComparerResult;
}
return this._thisTimeComparer.Compare(x, y);
}
如需求所說:比較 x 與 y 時,先比較第一個 comparer ,如果第一個 comparer 比不出來時,則交給下一個 comparer 來比。
該怎麼使用 ComboComparer 呢?假設先比較 Age, 再比較 Name ,測試案例如下:
[TestMethod]
public void Test_ComboComparer_with_twoComparer()
{
var persons = new List<Person>
{
new Person {Id = 1, Salary = 200, Name = "Cat", Age = 15},
new Person {Id = 2, Salary = 200, Name = "Bob", Age = 14},
new Person {Id = 3, Salary = 100, Name = "Alice", Age = 14},
new Person {Id = 4, Salary = 50, Name = "Alice", Age = 14},
};
//先比Age, 再比Name
var comboComparer = new ComboComparer<Person>(new PersonAgeComparer(), new PersonNameComparer());
//2跟3比,Bob比Alice大
var result1 = comboComparer.Compare(persons[1], persons[2]);
Assert.IsTrue(result1 > 0);
//3跟4比,Age與Name相同,所以大小相等
var result2 = comboComparer.Compare(persons[2], persons[3]);
Assert.IsTrue(result2 == 0);
}
可以看到,只需要在 ComboComparer 的 construtor ,傳入要比較的兩個 comparer 即可。
等等…我們的需求是要用 3 個 comparer 來比較 Person, 甚至是 N 個 comparer ,難不成要針對不同的需求來開立多個 constructor ? 或是用陣列來存放不同的 comparer ?
其實根本不需要, Decorator 其中一個最大好處,就是可以無止盡的「裝飾」下去。
假設需求是先比 Age, 再比 Name, 再比 Salary ,這個時候只需要透過 ComboComparer 的 constructor , 就能組合出我們想要的 comparer 順序。來看一下測試案例,一切就會很清楚:
[TestMethod]
public void Test_ComboComparer_with_ThreeComparer()
{
var persons = new List<Person>
{
new Person {Id = 1, Salary = 200, Name = "Cat", Age = 15},
new Person {Id = 2, Salary = 200, Name = "Bob", Age = 14},
new Person {Id = 3, Salary = 100, Name = "Alice", Age = 15},
new Person {Id = 4, Salary = 50, Name = "Alice", Age = 15},
};
//先比Age, 再比Name, 再比Salary
var combo1 = new ComboComparer<Person>(new PersonAgeComparer(), new PersonNameComparer());
var finalComparer = new ComboComparer<Person>(combo1, new PersonSalaryComparer());
//1跟2比, 因為Age, 1比較大
var result1 = finalComparer.Compare(persons[0], persons[1]);
Assert.IsTrue(result1 > 0);
//1跟3比, 因為Name, 1比較大
var result2 = finalComparer.Compare(persons[0], persons[2]);
Assert.IsTrue(result2 > 0);
//3跟4比, 因為Salary, 3比較大
var result3 = finalComparer.Compare(persons[2], persons[3]);
Assert.IsTrue(result3 > 0);
}
可以看到,跟剛剛先比 Age 再比 Name 的差異,只是多了一行。把組合過的 combo1 ,再放進去 finalComparer 的第一個參數中。這樣測試出來的結果,就跟上面的 PersonComplexComparer 比較一模一樣。
為什麼組合兩次,就可以比較三個 comparer 呢?或許用下面的方式表達可以更清楚:
再來看一次 ComboComparer 中那個簡單的 Compare() 方法內容:
當測試程式中呼叫 finalComparer 的 Compare() 時,其實這時的 _untilNowComparer 型別是 ComboComparer 。因此當測試程式呼叫 finalComparer 的 Compare() 時,會先呼叫 untilNowComparer 的 Compare() ,而因為 untilNowComparer 是 ComboComparer ,所以就會再呼叫 combo1 的 Compare() ,在 combo1 中的 untilNowComparer 則是 PersonAgeCompare, thisTimeComparer 則是 PersonNameComparer。
因此,就會先用 PersonAgeCompare 比較 Age, 如果比不出來大小,則會接著比較 PersonNameComparer 。如果比完 Age 與 Name 仍比不出來大小,則回到原本的 finalComparer 中,這時 untilNowComparerResult 為 0。因此,會再比較 thisTimeComparer ,也就是 PersonSalaryComparer 。所以,就可以達到比較兩個 Person 物件,先比 Age, 再比 Name, 再比 Salary。
結論
透過 Decorator Pattern 的特性,在這例子中我們可以透過多層的 constructor 來任意組合 IComparer 跟順序,而在呼叫 Compare() 方法時,透過簡單的方式就可以達到像剝洋蔥式的呼叫。其實,這樣的方式,也跟前面一篇文章很像:[ASP.NET]重構之路系列v9 –使用介面+迴圈取代不穩定的判斷式 ,只是把 IValidator.Validate() 改成了 IComparer.Compare() ,然後把組成 Validator 的過程,改為 Decorator Pattern 而已。
希望這篇文章,可以讓大家體會到 Decorator 在這樣的情境下,也是相當適用的唷!
而這一篇文章,正可以當作後面介紹 OrderBy() 與 ThenBy() 如何保留前面的 comparer 與這一次的 comparer ,再一起進行比較的基礎知識。
希望後面介紹 OrderBy() 與 ThenBy() 的文章,不會讓大家失望。
Reference
blog 與課程更新內容,請前往新站位置:http://tdd.best/
