[Design Pattern]Decorator Pattern with IComparer - 先比這個,再比那個

  • 11388
  • 0
  • 2013-08-14

[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> 的意義,請見下圖:

image

image

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:

UML_thumb[1]

這時候,將上圖的 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>,如下所示:

image

將傳入的兩個 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 呢?或許用下面的方式表達可以更清楚:

image

再來看一次 ComboComparer 中那個簡單的 Compare() 方法內容:

image

當測試程式中呼叫 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

  1. IComparer<T> 介面
  2. Reimplementing LINQ to Objects: Part 26a – IOrderedEnumerable

blog 與課程更新內容,請前往新站位置:http://tdd.best/