轉載自: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 應用場景
- 多個類只區別在表現行爲不同,可以使用Strategy模式,在運行時動態選擇具體要執行的行爲。
- 需要在不同情況下使用不同的策略(算法),或者策略還可能在未來用其它方式來實現。
- 對客戶隱藏具體策略(算法)的實現細節,彼此完全獨立。
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