策略模式-JAVA类库TreeSet为例
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。(原文: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、编程中使用接口而不是用接口的实现
抽象策略角色: 策略类,通常由一个接口或者抽象类实现。
具体策略角色:包装了相关的算法和行为。
环境角色:持有一个策略类的引用,最终给客户端调用。
Context(应用场景):
1、需要使用Concrete Strategy提供的算法。
2、 内部维护一个Strategy的实例。
3、 负责动态设置运行时Strategy具体的实现算法。
4、负责跟Strategy之间的交互和数据传递。
Strategy(抽象策略类):
1、 定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,Context使用这个接口调用不同的算法,一般使用接口或抽象类实现。
Concrete Strategy(具体策略类):
1、 实现了Strategy定义的接口,提供具体的算法实现。
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类即是具体的实现类,虽然呢传入对象千差万别。我们将通过,源码进行分析。
在查看源码时,我们习惯性的第一步肯定是看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 }
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这个接口就可以了,这不正体现了策略模式灵活、可扩展的特点。
1 public interface Comparator<T> { 2 int compare(T o1, T o2); 3 boolean equals(Object obj); 4 }
这个接口只定义了两个函数,compare函数的行为很简单如果o1比o2大,就返回一个大于0的整数,反之则小于0。
这个类即是客户端类,同时也实现了具体的算法。在基本的策略模式中,选择所用具体实现的职责由客户端对象承担,并转给策略模式的Context对象。(这本身没有解除客户端需要选择判断的压力,而策略模式与简单工厂模式结合后,选择具体实现的职责也可以由Context来承担,这就最大化的减轻了客户端的压力。)
其实,在某个方面来说这个也不能称之为一个缺点看具体的应用,在这个例子中你就不能把这个负担减轻。Comparator本来就千差万别,不可能交给工厂类。如果是排序的话,可能有很多种具体实现你可以交给工厂类给你来实现。
把策略模式的优缺点放在这个位置不尴不尬。不过,我觉得先看优缺点,思考过后再看后面的对比例子更容易理解。
1、 提供了一种替代继承的方法,而且既保持了继承的优点(代码重用)还比继承更灵活(算法独立,可以任意扩展)。
2、 避免程序中使用多重条件转移语句,使系统更灵活,并易于扩展。
3、 遵守大部分GRASP原则和常用设计原则,高内聚、低偶合。
1、 因为每个具体策略类都会产生一个新类,所以会增加系统需要维护的类的数量。
2、 在基本的策略模式中,选择所用具体实现的职责由客户端对象承担,并转给策略模式的Context对象。(这本身没有解除客户端需要选择判断的压力,而策略模式与简单工厂模式结合后,选择具体实现的职责也可以由Context来承担,这就最大化的减轻了客户端的压力。)
假设一个场景,如果有一个笼子,里面可以放狗Dog或者Bird,且只能放两种中的一种。取出来,要根据他们的编号顺序取出。
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包里,给别人使用。
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类的子类,比较方式可能跟父类不一样(甚至没有比较方法),这个时候你就只能用覆盖的方式来完成了。在程序设计时,还是建议分开会变化和不会变化的部分,把变化的部分独立出来成为接口。
这两种模式的作用就是拥抱变化,减少耦合。在变化来临时争取做最小的改动来适应变化。这就要求我们把些“善变”的功能从客户端分离出来,形成一个个的功能类,然后根据多态特性,使得功能类变化的同时,客户端代码不发生变化。
简单工厂模式:有一个父类需要做一个运算(其中包含了不同种类的几种运算),将父类涉及此运算的方法都设成虚方法,然后父类派生一些子类,使得每一种不同的运算都对应一个子类。另外有一个工厂类,这个类一般只有一个方法(工厂的生成方法),这个方法的返回值是一个超类,在方法的内部,根据传入参数的不同,分别构造各个不同的子类的对象,并返回。客户端并不认识子类,客户端只认识超类和工厂类。每次客户端需要一中运算时,就把相应的参数传给工厂类,让工厂类构造出相应的子类,然后在客户端用父类接收(这里有一个多态的运用)。客户端很顺理成章地用父类的计算方法(其实这是一个虚方法,并且已经被子类特化过了,其实是调用子类的方法)计算出来结果。如果要增加功能时,你只要再从父类中派生相应功能的子类,然后修改下工厂类就OK了,对于客户端是透明的。
策略模式:策略模式更直接了一点,没有用工厂类,而是直接把工厂类的生成方法的代码写到了客户端。客户端自己构造出了具有不同功能的子类(而且是用父类接收的,多态),省掉了工厂类。策略模式定义了算法家族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。这里的算法家族和简单工厂模式里的父类是同一个概念。当不同的行为堆砌在一个类中时,就很难避免使用条件语句来选择合适的行为,将这些行为封装在一个个独立的策略子类中,可以在客户端中消除条件语句。
简单工厂模式+策略模式:为了将工厂方法的代码从客户端移出来,我们把这些代码搬到了父类的构造函数中,让父类在构造的时候,根据参数,自己实现工厂类的作用。这样做的好处就是,在客户端不用再认识工厂类了,客户端只要知道父类一个就OK,进一步隔离了变化,降低了耦合。
在基本的策略模式中,选择所用具体实现的职责由客户端对象成端,并转给客户端。这本身并没有减除客户端需要选择判断的压力,而策略模式与简单工厂模式结合后,选择具体实现的职责也可以由父类承担,这就最大化地减轻了客户端的职责。
PS:有软件架构这门课上课要求讲,所以索性也把PPT也给分享了得了,网络的精神就应该是分享。http://pan.baidu.com/share/link?shareid=372728&uk=3792525916。也刚刚学设计模式,难免有纰漏,希望批评指正,毕竟没有做过大型项目。
[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