策略模式解析-JAVA類庫中TreeSet源碼爲例

轉載自:http://www.cnblogs.com/wukenaihe/archive/2013/04/03/2997279.html

策略模式-JAVA類庫TreeSet爲例

1 策略模式概述

1.1 策略模式定義

         策略模式定義了一系列的算法,並將每一個算法封裝起來,而且使它們還可以相互替換。策略模式讓算法獨立於使用它的客戶而獨立變化。(原文:The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.)

         策略模式體現了面向對象設計兩個最基本的設計原則:

   1、封裝變化的概念

   2、編程中使用接口而不是用接口的實現

1.2 策略模式組成

抽象策略角色: 策略類,通常由一個接口或者抽象類實現。

具體策略角色:包裝了相關的算法和行爲。

環境角色:持有一個策略類的引用,最終給客戶端調用。

 

Context(應用場景):

1、需要使用Concrete Strategy提供的算法。

2、 內部維護一個Strategy的實例。

3、 負責動態設置運行時Strategy具體的實現算法。

4、負責跟Strategy之間的交互和數據傳遞。

Strategy(抽象策略類):

1、 定義了一個公共接口,各種不同的算法以不同的方式實現這個接口,Context使用這個接口調用不同的算法,一般使用接口或抽象類實現。

Concrete Strategy(具體策略類):

1、 實現了Strategy定義的接口,提供具體的算法實現。

1.3 應用場景

  1. 多個類只區別在表現行爲不同,可以使用Strategy模式,在運行時動態選擇具體要執行的行爲。
  2. 需要在不同情況下使用不同的策略(算法),或者策略還可能在未來用其它方式來實現。
  3. 對客戶隱藏具體策略(算法)的實現細節,彼此完全獨立。

2 JAVA類庫中TreeSet的策略模式

TreeSet編程

複製代碼
 1 public class TreeSetTest2 {
 2     public static void main(String[] args) {
 3         Set<Person> set=new TreeSet<Person>(new Mycomparator());
 4         //(1)
 5         Person p1=new Person(1,"ll");
 6         Person p2=new Person(2, "xx");
 7         Person p3=new Person(3, "ss");
 8         
 9         set.add(p1);
10         set.add(p2);
11         set.add(p3);
12                 
13         for(Iterator<Person> itr=set.iterator();itr.hasNext();){
14             System.out.println(itr.next().getName());
15         }
16     }
17 }
18 
19 class Person{
20     private int ID;
21     private String name;
22     
23     public Person(int id,String name){
24         this.ID=id;
25         this.name=name;
26     }
27     public int getID() {
28         return ID;
29     }
30     public String getName() {
31         return name;
32     }
33 }
34 
35 class Mycomparator implements Comparator<Person>{
36     public int compare(Person o1, Person o2) {
37         return o1.getID()-o2.getID();
38     }
39     
40 }
複製代碼

 

結果:

 

  上面代碼實現的功能非常簡單,將Person這個類放入到TreeSet這個集合中,並實現對放入對象的一個排序。TreeSet是java類庫裏面的一個標準類,對於要add進去的類,在編碼時並知道,那麼是如何實現排序的呢?在TreeSet生成時,我們傳入了一個Comparator的類,既是抽象策略類。

  具體的排序算法,在java類庫也是封裝好的,我們並不需要關心。在這個例子中,MyComparator類即是具體的實現類,雖然呢傳入對象千差萬別。我們將通過,源碼進行分析。

2.1 環境角色-TreeSet

在查看源碼時,我們習慣性的第一步肯定是看TreeSet這個類(從(1)->(2)),裏面到底是如何實現對添加進去的類實現排序的。

TreeSet代碼(部分)

複製代碼
 1 public class TreeSet<E> extends AbstractSet<E>
 2     implements NavigableSet<E>, Cloneable, java.io.Serializable
 3 {
 4     TreeSet(NavigableMap<E,Object> m) {
 5         this.m = m;
 6 }
 7 
 8     public TreeSet(Comparator<? super E> comparator) {
 9         this(new TreeMap<>(comparator));//(2)
10 }
11 }
複製代碼

   上面的代碼中給出的構造方法是衆多構造函數中的一個,也是我們例子中使用到的構造函數。可以很明顯看出,TreeSet在底層是用TreeMap實現的(Map中的key並不會重複)。這樣,我們就必須要轉到TreeMap的代碼去看了(從(2)->(3))。

TreeMap代碼(部分)

複製代碼
 1 public class TreeMap<K,V>
 2     extends AbstractMap<K,V>
 3     implements NavigableMap<K,V>, Cloneable, java.io.Serializable
 4 {
 5     private final Comparator<? super K> comparator;
 6 
 7 private transient Entry<K,V> root = null;
 8 
 9     public TreeMap(Comparator<? super K> comparator) {
10         this.comparator = comparator;//(3)
11     }
12 }
複製代碼

   從上述代碼中,我可以發現我們傳入的Mycompator對象最後,賦給了TreeMap對象中的comparator。TreeSet的對象已經生成,接下來我們看看使用add()方法是代碼是如何進行的。

TreeSet的add()方法:

1     public boolean add(E e) {
2         return m.put(e, PRESENT)==null;
3 }

   很顯然,TreeSet的add(),使用的是TreeMap的put()方法,將傳入的對象作爲key,一個空的Object對象作爲value。事實上,所有的實現均在TreeMap中,所以繼續看TreeMap中的put()方法。

TreeMap的put()方法:

複製代碼
 1 public V put(K key, V value) {
 2         Entry<K,V> t = root;
 3         //空樹時,第一個節點給根節點
 4         if (t == null) {
 5             compare(key, key); // type (and possibly null) check
 6 
 7             root = new Entry<>(key, value, null);
 8             size = 1;
 9             modCount++;
10             return null;
11         }
12         int cmp;
13         Entry<K,V> parent;
14         // split comparator and comparable paths
15         Comparator<? super K> cpr = comparator;
16         //我們現在的實現方式cpr就不爲null,具體實現類時MyComparator
17         if (cpr != null) {
18             do {
19                 parent = t;
20 //這一步非常關鍵,對於排序算法來說,我只要知道兩個對象比//較後誰大水小就可以了。對於該算法而言,cpr是一個接口類//(具體實現我不管,反正肯定有compare這個函數,肯定會返//回一個int類型)。這樣,我獲取到這個值之後就能排序了,//不管要排序的是Person類還是Animal類。
21                 cmp = cpr.compare(key, t.key);
22                 if (cmp < 0)
23                     t = t.left;
24                 else if (cmp > 0)
25                     t = t.right;
26                 else
27                     return t.setValue(value);
28             } while (t != null);
29         }
30 //如果comparator爲空,就要用comparable這個藉口了,也是策略模式
31         else {
32             if (key == null)
33                 throw new NullPointerException();
34             Comparable<? super K> k = (Comparable<? super K>) key;
35             do {
36                 parent = t;
37                 cmp = k.compareTo(t.key);
38                 if (cmp < 0)
39                     t = t.left;
40                 else if (cmp > 0)
41                     t = t.right;
42                 else
43                     return t.setValue(value);
44             } while (t != null);
45         }
46         Entry<K,V> e = new Entry<>(key, value, parent);
47         if (cmp < 0)
48             parent.left = e;
49         else
50             parent.right = e;
51         fixAfterInsertion(e);//看來還是棵平衡二叉排序樹
52         size++;
53         modCount++;
54         return null;
55 }
複製代碼

2.2 具體實現類-Mycomparator

1 class Mycomparator implements Comparator<Person>{
2     public int compare(Person o1, Person o2) {
3         return o1.getID()-o2.getID();
4     }
5     
6 }

  在這個類中,我們實現的是對Person這個類的對象的比較。當然這裏你也可以用別的類,阿貓阿狗都沒問題只要實現了Comparator這個接口就可以了,這不正體現了策略模式靈活、可擴展的特點。

2.3 抽象類(接口)- Comparator

1 public interface Comparator<T> {
2     int compare(T o1, T o2);
3     boolean equals(Object obj);
4 }

   這個接口只定義了兩個函數,compare函數的行爲很簡單如果o1比o2大,就返回一個大於0的整數,反之則小於0。

2.4 客戶端類-TreeSetTest2

這個類即是客戶端類,同時也實現了具體的算法。在基本的策略模式中,選擇所用具體實現的職責由客戶端對象承擔,並轉給策略模式的Context對象。(這本身沒有解除客戶端需要選擇判斷的壓力,而策略模式與簡單工廠模式結合後,選擇具體實現的職責也可以由Context來承擔,這就最大化的減輕了客戶端的壓力。)

其實,在某個方面來說這個也不能稱之爲一個缺點看具體的應用,在這個例子中你就不能把這個負擔減輕。Comparator本來就千差萬別,不可能交給工廠類。如果是排序的話,可能有很多種具體實現你可以交給工廠類給你來實現。

3 策略模式的優缺點

把策略模式的優缺點放在這個位置不尷不尬。不過,我覺得先看優缺點,思考過後再看後面的對比例子更容易理解。

3.1 策略模式優點

1、 提供了一種替代繼承的方法,而且既保持了繼承的優點(代碼重用)還比繼承更靈活(算法獨立,可以任意擴展)。

2、 避免程序中使用多重條件轉移語句,使系統更靈活,並易於擴展。

3、 遵守大部分GRASP原則和常用設計原則,高內聚、低偶合。

3.2 策略模式的缺點

1、 因爲每個具體策略類都會產生一個新類,所以會增加系統需要維護的類的數量。

2、 在基本的策略模式中,選擇所用具體實現的職責由客戶端對象承擔,並轉給策略模式的Context對象。(這本身沒有解除客戶端需要選擇判斷的壓力,而策略模式與簡單工廠模式結合後,選擇具體實現的職責也可以由Context來承擔,這就最大化的減輕了客戶端的壓力。)

4 對比-非策略模式時

假設一個場景,如果有一個籠子,裏面可以放狗Dog或者Bird,且只能放兩種中的一種。取出來,要根據他們的編號順序取出。

4.1 2B程序員做法

複製代碼
 1 public class ArrayAnimal {
 2     Dog[] dog=new Dog[10];
 3     Bird[] bird=new Bird[10];
 4     int count=0;
 5     
 6     public void add(Dog dog){
 7         //如果空間不夠了,分配更大空間
 8         //根據插入排序(通過ID),將dog插入適合的位置
 9     }
10     public void add(Bird bird){
11         //如果空間不夠了,分配更大空間
12         //根據插入排序(通過ID),將dog插入適合的位置
13     }
14 }
15 
16 class Dog{
17     public int ID;
18 }
19 class Bird{
20     public int ID;
21 }
複製代碼

   這是2B程序員(且泛型也不會用)的做法,如果這個籠子只放狗和鳥,不會有任何問題。設想,如果我們說這個籠子現在雞鴨都可以放了呢?這個是時候,你就不得不去修改ArrayAnimal這個類了,增加新的add方法。這個時候就完全沒有靈活性、擴展性可言了(優點2),基本上不能想象ArrayAnimal這個類放在jar包裏,給別人使用。

4.2 進階版非策略模式

複製代碼
 1 public class ArrayAnimal {
 2     Animal[] animal=new Animal[10];
 3     int count=0;
 4     
 5     public void add(Animal obj){
 6         //如果空間不夠了,分配更大空間
 7         //根據插入排序(通過ID),將dog插入適合的位置
 8     }
 9 }
10 class Animal{
11     public int ID;
12     int compare(Animal obj){
13         return ID-obj.ID;
14     }
15 }
16 
17 class Dog extends Animal{
18     public String name;
19 }
20 class Bird{
21     public String name;
22 }
複製代碼

   這個程序,就比前一個高明多了,基本上所有繼承Animal的類都能放到這個籠子裏面,而且這籠子也不需要修改。但是,擴展依然有限,因爲放進籠子裏面的都只能是動物。而且,這個程序裏面即便是繼承Animal類的子類,比較方式可能跟父類不一樣(甚至沒有比較方法),這個時候你就只能用覆蓋的方式來完成了。在程序設計時,還是建議分開會變化和不會變化的部分,把變化的部分獨立出來成爲接口。

5 簡單工廠模式和策略模式區別

  這兩種模式的作用就是擁抱變化,減少耦合。在變化來臨時爭取做最小的改動來適應變化。這就要求我們把些“善變”的功能從客戶端分離出來,形成一個個的功能類,然後根據多態特性,使得功能類變化的同時,客戶端代碼不發生變化。

5.1 簡單工廠模式

  簡單工廠模式:有一個父類需要做一個運算(其中包含了不同種類的幾種運算),將父類涉及此運算的方法都設成虛方法,然後父類派生一些子類,使得每一種不同的運算都對應一個子類。另外有一個工廠類,這個類一般只有一個方法(工廠的生成方法),這個方法的返回值是一個超類,在方法的內部,根據傳入參數的不同,分別構造各個不同的子類的對象,並返回。客戶端並不認識子類,客戶端只認識超類和工廠類。每次客戶端需要一中運算時,就把相應的參數傳給工廠類,讓工廠類構造出相應的子類,然後在客戶端用父類接收(這裏有一個多態的運用)。客戶端很順理成章地用父類的計算方法(其實這是一個虛方法,並且已經被子類特化過了,其實是調用子類的方法)計算出來結果。如果要增加功能時,你只要再從父類中派生相應功能的子類,然後修改下工廠類就OK了,對於客戶端是透明的。

5.2 策略模式

  策略模式:策略模式更直接了一點,沒有用工廠類,而是直接把工廠類的生成方法的代碼寫到了客戶端。客戶端自己構造出了具有不同功能的子類(而且是用父類接收的,多態),省掉了工廠類。策略模式定義了算法家族,分別封裝起來,讓他們之間可以互相替換,此模式讓算法的變化,不會影響到使用算法的客戶。這裏的算法家族和簡單工廠模式裏的父類是同一個概念。當不同的行爲堆砌在一個類中時,就很難避免使用條件語句來選擇合適的行爲,將這些行爲封裝在一個個獨立的策略子類中,可以在客戶端中消除條件語句。

  簡單工廠模式+策略模式:爲了將工廠方法的代碼從客戶端移出來,我們把這些代碼搬到了父類的構造函數中,讓父類在構造的時候,根據參數,自己實現工廠類的作用。這樣做的好處就是,在客戶端不用再認識工廠類了,客戶端只要知道父類一個就OK,進一步隔離了變化,降低了耦合。

  在基本的策略模式中,選擇所用具體實現的職責由客戶端對象成端,並轉給客戶端。這本身並沒有減除客戶端需要選擇判斷的壓力,而策略模式與簡單工廠模式結合後,選擇具體實現的職責也可以由父類承擔,這就最大化地減輕了客戶端的職責。

 

PS:有軟件架構這門課上課要求講,所以索性也把PPT也給分享了得了,網絡的精神就應該是分享。http://pan.baidu.com/share/link?shareid=372728&uk=3792525916。也剛剛學設計模式,難免有紕漏,希望批評指正,畢竟沒有做過大型項目。

6 參考資料

[1].   《Head First設計模式》

[2].   http://www.cnblogs.com/syxchina/archive/2011/10/11/2207017.html

[3].   http://fendou.org/post/2011/03/23/factory-strategy/

[4].   http://baike.baidu.com/view/2141079.htm


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章