前言
学习设计模式已经有一段时间了,前段时间一直在忙一个安卓的app,没时间更新。今天有点空,本着开源的精神,把策略模式的一些东西分享一下。
注意:博主只是个搞安卓的大三学生狗,以下内容纯属自学的,若有不正确的地方欢迎指出。
正文
转载请注明出处: http://blog.csdn.net/h28496/article/details/46403815
发 表 时 间: 2015年6月7日
举例说明为什么要有策略模式
假设一个场景:我们有一些排序算法,需要观察每个排序算法的排序过程。然后把它封装为客户端。
1. 一般情形下应该怎么做?
我们可以写一个客户端类 Client_NotUseStrategy.java,如下:(注意run()方法里面的switch-case)
/**
* 没有使用策略模式的客户端
* @author 郑海鹏
*/
public class Client_NotUseStrategy {
public static final int INSERT_SORT = 0;
public static final int BUBBLE_SORT = 1;
int sortType;
int[] a;
public Client_NotUseStrategy(int sortType, int[] a) {
this.sortType = sortType;
this.a = a;
}
public void run() {
switch (sortType) {
case INSERT_SORT:
insertSort();
break;
case BUBBLE_SORT:
bubbleSort();
break;
default:
break;
}
}
/**
* 插入排序
*/
private void insertSort() {
for (int i = 1; i < a.length; i++) {
int temp = a[i];
int j = i - 1;
for (; j >= 0; j--) {
if (a[j] > temp) {
a[j + 1] = a[j];
} else {
break;
}
}
a[j + 1] = temp;
Tools.print(a);
}
}
/**
* 冒泡排序
*/
private void bubbleSort() {
for (int i = 0; i < a.length - 1; i++) {
for (int j = 0; j < a.length - i - 1; j++) {
if (a[j] > a[j + 1]) {
Tools.swap(a, j, j + 1);
}
}
}
}
}
执行结果:
2. 这种解决方案的缺点
试想,我们现在要再添加一个快速排序的排序方式。需要怎么做呢?
① 需要更改客户端的run()方法,增加一个case。
② 还有可能需要在客户端中增加一个quickSort()方法。
即,当需要扩展功能时,必须修改客户端代码。
除此之外,如果具体的排序算法需要被修改,那么还要去修改客户端代码。例如我要修改一下插入排序,那么还需要修改Client_NotUseStrategy类。
即,当需要修改原功能时,也必须修改客户端代码。
只要客户端代码需要被修改,这就不是一个好的代码结构。因为好的代码结构怎么能不满足“对修改封闭,对扩展开放”的开闭原则呢?
那要怎么修改才能在这种多分支结构下也满足开闭原则呢?这就引出策略模式了。
初步了解策略模式
什么是策略模式呢?
1. 策略模式的定义
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
2. 什么时候需要使用策略模式?
有大量if-else if – … - else或者switch – case … , 并且这些不同的操作属于同一类型的操作,只是具体的处理方式不同时,就可以使用策略模式。
3. 策略模式的组成
1.UML图
2.角色介绍
① IStrategy:策略接口;
② ConcreteStrategy1, ConcreteStrategy2: 具体的策略实现,封装了相关的算法和行为;
③ Context:用来操作策略的上下文环境。
使用策略模式解决问题
现在,我们利用策略模式来解决刚才遇到的排序问题。注意观察Client_NotUseStrategy类和Client_UseStrategy类的区别。
1. 代码结构
2. IStrategy接口
/**
* 策略接口
* @author 郑海鹏
*/
public interface IStrategy {
void sort(int[] a);
}
3. Strategy_BubbleSort具体实现策略之冒泡排序
/**
* 实现策略接口的一个冒泡排序
*
* @author 郑海鹏
*/
public class Strategy_BubbleSort implements IStrategy {
@Override
public void sort(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = 0; j < a.length - i - 1; j++) {
if (a[j] > a[j + 1]) {
Tools.swap(a, j, j + 1);
}
}
}
}
}
4.Strategy_InsertSort 具体实现策略之插入排序
/**
* 实现策略接口的一个插入排序
* @author 郑海鹏
*/
public class Strategy_InsertSort implements IStrategy {
@Override
public void sort(int[] a) {
for (int i = 1; i < a.length; i++) {
int temp = a[i];
int j = i - 1;
for (; j >= 0; j--) {
if (a[j] > temp) {
a[j + 1] = a[j];
} else {
break;
}
}
a[j + 1] = temp;
Tools.print(a);
}
}
}
5. Context 上下文环境
/**
* 上下文类,用来放置策略
* @author 郑海鹏
*/
public class Context {
IStrategy strategy;
public Context(IStrategy strategy){
this.strategy = strategy;
}
public void sort(int[] a){
this.strategy.sort(a);
}
}
6. 客户端代码
/**
* 使用策略模式的客户端
* @author 郑海鹏
*/
public class Client_UseStrategy {
IStrategy strategy;
int[] a;
public Client_UseStrategy(IStrategy strategy, int[] a){
this.strategy = strategy;
this.a = a;
}
public void run(){
Context context = new Context(strategy);
context.sort(a);
}
}
7. 执行效果
结果和不使用策略模式的结果是一样。
8. 总结
在上面的代码中,如果我们还要再添加一个排序算法,只需添加一个实现了Istrategy接口的类,如快排Strategy_QuickSort即可。需要去修改客户端Client_UseStrategy吗?不需要。实现了具体策略与客户端的解耦。
反观没有使用策略模式的客户端Client_NotUseStrategy,如果需要添加新的算法,必须修改客户端类。
关于策略模式中Context的思考
1. 在策略模式中,Context角色有存在的必要吗?
我在学习策略模式时,这个问题困扰我很长时间。如上面的例子,Context完全可以不要。下面是去掉Context之后的代码:
/**
* 没有用Context的客户端
*
* @author 郑海鹏
*/
public class Client_UseStrategy_WithoutContext {
IStrategy strategy;
int[] a;
public Client_UseStrategy_WithoutContext(IStrategy strategy, int[] a) {
this.strategy = strategy;
this.a = a;
}
public void run() {
// 原来的
// Context context = new Context(strategy);
// context.sort(a);
// 不要Context
this.strategy.sort(a);
}
}
可以看到,这样也是可以执行出同样的效果。并且优缺点也和有Context时一样。 那Context是不是真的没用呢?肯定不是的。
2. Context角色的作用 – 消除冗余代码,提高代码复用性
假设,我们需要计算不同的排序用的时间,可以怎么做呢?下面有两种方法,思考一下哪种更好。
① 可以在Client客户端代码的run()方法中添加计算时间的代码,如下:
/**
* 使用策略模式的客户端
*
* @author 郑海鹏
*/
public class CopyOfClient_UseStrategy_WithoutContext {
IStrategy strategy;
int[] a;
public CopyOfClient_UseStrategy_WithoutContext(IStrategy strategy, int[] a) {
this.strategy = strategy;
this.a = a;
}
public void run() {
// 原来的
// long startTime = System.currentTimeMillis();
// Context context = new Context(strategy);
// context.sort(a);
// long endTime = System.currentTimeMillis();
// long usedTime = endTime - startTime;
// System.out.println("该排序用掉的时间是:" + usedTime);
// 不要Context
long startTime = System.currentTimeMillis();
this.strategy.sort(a);
long endTime = System.currentTimeMillis();
long usedTime = endTime - startTime;
System.out.println("该排序用掉的时间是:" + usedTime);
}
}
② 在Context中添加计算时间的代码,如下:
/**
* 新Context类
* @author 郑海鹏
* @since 2015年6月5日
*/
public class CopyOfContext {
IStrategy strategy;
public CopyOfContext(IStrategy strategy){
this.strategy = strategy;
}
public void sort(int[] a){
long startTime = System.currentTimeMillis();
this.strategy.sort(a);
long endTime = System.currentTimeMillis();
long usedTime = endTime - startTime;
System.out.println("该排序用掉的时间是:" + usedTime);
}
}
哪种方式更好呢?尤其是需要添加的代码较多时。
很明显,我们不应该添加客户端不用关心的代码在客户端中。于是在Context中添加代码就合理多了。
从代码复用的角度去看,当客户端存在多个时,在每个客户端中都添加一堆相同的计时代码,显然太冗余了。而使用Context则可消除这些冗余。
假设有100个客户端类,有一天老板让改一下这些代码,这些相同的代码就得改100次。但如果放在Context中,只需修改一次,代码复用的优势就体现出来了。
3. Context角色的作用 – 满足开闭原则
上面的例子中,IStrategy的sort(int[] a)方法只有一个参数。突然有一天,需求改了,参数变成了两个(int[] a, float[] b)。这时应该怎么修改代码呢?
① 如果没有Context:我们需要去在每个客户端代码中更改代码this.strategy(a)为this.strategy(a, b)。这没有满足开闭原则!
② 如果有Context:我们只需在Context类中修改sort(int[] a)方法即可,客户端代码不受任何影响。
策略模式在Android开发中的使用
下面举一个在Android开发中用到策略模式的例子。
在Android开发中经常会用到Handler。Handler一般放在Activity中,子线程通过handler发送消息到Activity,然后通过switch(msg.what)的方式来处理不同的消息。这会造成如下问题:
① 需要定义很多常量,用于 case xxx。子线程在发送时需要调用这些常量,这增加了类之间的耦合性。
② 当处理的消息类型很多时,就会出现大量的case分支,Activity类的代码行数嗖嗖嗖就上去了,十分臃肿。一个Activity类的任务可是很重的,仅仅一个handler就占了几百行代码是个很恐怖的事。(在此吐槽一下很多东西都需要Activity来处理的这种设计,不过据说在刚过去的I/O大会上,Google把 Data Binding引入到了Android中,有空学习一下~)
③ 没有满足开闭原则。
下面只贴了activity的代码,其它部分的代码在文末的附件中。模拟的是在两个子线程中从服务器下载图片和文字。然后通过handler发送消息给Activity,并显示在界面上。在handler处理msg时用到策略模式。
1. 没有使用策略模式的Activity
/**
* @author 郑海鹏
* @since 2015年6月5日
*/
public class NotUseStrategy extends Activity {
/**
* 从服务器下载图片完毕,用于handler的消息
*/
public static final int MSG_DOWNLOAD_IMAGE_FINSHED = 100;
/**
* 从服务器获得好友列表结束
*/
public static final int MSG_GET_CONTAINS_FINSH = 101;
Handler handler;
LinearLayout bgLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bgLayout = (LinearLayout) findViewById(R.id.bgLayout);
initHandler();
new Thread_DownloadImage1(handler, this).start();
new Thread_GetContains1(handler).start();
}
/**
* 初始化Handler
*/
private void initHandler() {
handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
// 通过msg.what的不同处理各种操作
switch (msg.what) {
// 当图片下载完毕后,加入到视图中
case MSG_DOWNLOAD_IMAGE_FINSHED:
// 新建一个ImageView
ImageView imageView = new ImageView(NotUseStrategy.this);
// 获得从子线程中传入的Bitmap
Bitmap bitmap = (Bitmap) msg.obj;
// 将imageView的图像设置为传入的图像
imageView.setImageBitmap(bitmap);
// 添加ImageView到视图中
bgLayout.addView(imageView);
break;
// 当联系人下载完毕后,加入到视图中
case MSG_GET_CONTAINS_FINSH:
TextView textView = new TextView(NotUseStrategy.this);
String s = (String) msg.obj;
textView.setText(s);
bgLayout.addView(textView);
break;
default:
break;
}
return false;
}
});
}
}
2. 使用策略模式后的Activity
public class UseStrategy extends Activity {
// 使用策略模式不用再预先定义一堆消息。降低了耦合性。
Handler handler;
LinearLayout bgLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bgLayout = (LinearLayout) findViewById(R.id.bgLayout);
initHandler();
new Thread_DownloadImage2(handler, this, bgLayout).start();
new Thread_GetContains2(handler, this, bgLayout).start();
}
/**
* 初始化handler
*/
private void initHandler() {
handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
// 使用策略模式,不用写一堆switch-case
// 获得传入的策略
IStrategy strategy = (IStrategy) msg.obj;
// 把策略放到Context中执行
MyContext context = new MyContext(strategy);
context.run();
return false;
}
});
}
}
3. 运行截图
4. 评价
使用策略模式时,不管以后多添加了多少种情况,initHandler()方法中的那三行代码都能处理过来。整个Actiivty类中的代码几乎不需要修改(要修改也不会是在这个方法中修改)。不需要预定义一些msg,耦合性也很低。