摘要:
什么样的程序设计被称为策略模式?什么时候使用策略模式?为什么用策略模式代替抽象类或接口?这篇文章引用构建(Builder)模式的例子,将绘制钟表的过程改写成符合策略模式程序设计,程序结构如下图:
WatchViewImpl继承View,实现自定义钟表视图
WatchViewActivity显示钟表效果
IClock定义绘制钟表的接口,impl目录的三个文件分别是接口IClock的三种实现,处理钟表的绘制逻辑,客户可以自由选择使用其中的一种策略。
一、关键代码分析
本程序如果不使用设计模式,只需要WatchViewImpl和WatchViewActivity即可实现,绘制钟表的逻辑全部写在自定义视图类内,不足的地方:如果想要修改绘制钟表的逻辑,唯一的方式就是修改WatchViewImpl,修改的同时又想保留原来的绘制逻辑,如果将第一个版本的逻辑实现在当前类封装后保留,再添加新的绘制逻辑,自定义视图类显得比较臃肿,不符合程序设计的单一设计原则
这时候开始考虑设计模式——策略(strategy)模式,看一下WatchViewActivity关键代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
cn.teachcourse.strategy.WatchViewImpl watchView=new cn.teachcourse.strategy.WatchViewImpl(this);
/*第一种策略:默认算法*/
IClock defaultClock=new DefaultClockImpl(watchView);
/*第二种策略:常用算法*/
IClock normalClock=new NormalClockImpl(watchView);
/*第三种策略:设计算法*/
IClock designClock = new DesignClockImpl(watchView);
/*自由选择使用其中的一种策略,比如:normalClock*/
watchView.setIClock(normalClock);
setContentView(watchView);
}
三种算法彼此之间没有影响,如果想要添加第四种算法,不需要修改或者改动很少自定义视图类WatchViewImpl,创建第四个算法类实现接口IClock,处理绘制钟表的逻辑,然后在客户端WatchViewActivity选择实例化的对象即可,符合程序设计中的开闭原则
看一下IClock的关键代码:
/**
* Created by https://www.teachcourse.cn on 2017/7/5.
*/
public interface IClock {
void paint(Canvas canvas);
}
再来看一下默认策略算法DefaultClockImpl的实现逻辑,关键代码:
public DefaultClockImpl(WatchViewImpl watchView) {
this.watchView = watchView;
}
@Override
public void paint(Canvas canvas) {
//移动画布中心点到当前View中心
canvas.translate(getWidth() / 2, getHeight() / 2);
//绘制外圆背景颜色
paintExternalCircle(canvas);
//绘制内圆背景颜色
paintCircle(canvas);
//绘制刻度
paintScale(canvas);
//绘制指针
paintPointer(canvas);
}
/**
* 绘制钟表外部圆形背景
*/
public void paintExternalCircle(Canvas canvas) {
...
}
/**
* 绘制钟表的内部圆形背景
*
* @param canvas
*/
public void paintCircle(Canvas canvas) {
...
}
/**
* 绘制钟表刻度
*
* @param canvas
*/
private void paintScale(Canvas canvas) {
...
}
/**
* 绘制钟表上的时针、分针、秒针
*
* @param canvas
*/
private void paintPointer(Canvas canvas) {
...
}
...
}
对比DesignClockImpl设计算法实现的逻辑,关键代码:
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
postInvalidate();
}
};
public DesignClockImpl(WatchViewImpl watchView) {
this.watchView = watchView;
mPaint=new Paint();
mPath = new Path();
}
@Override
public void paint(Canvas canvas) {
...
}
...
}
罗列关键代码的目的,想让读者可以对策略模式的设计结构一个整体的认识DefaultClockImpl类和DesignClockImpl类分别是两种不同处理绘制钟表的逻辑,两种策略的效果图分别如下:
设计策略模式,实现效果图:
小明同学可能喜欢绘制挂在墙上的钟表,类似于DefaultClockImpl效果;小红同学想要绘制带在手上的电子表,比较时尚,类似于DesignClockImpl效果;如果小李还想绘制一款与小红、小明不一样的钟表,那也不是不可能,小李构思一下绘制的逻辑,重新实现IClock接口即可。
二、策略模式分析
在钊林的另一篇介绍UML的文章里说过,学习设计模式,UML图谱可以简单清晰表示类之间的关系,可以整体理解设计模式的思路,不懂UML图谱的同学先了解一下。
绘制上述Demo的UML图谱如下:
UML类图的含义:WatchViewActivity依赖于WatchViewImpl,同时WatchViewImpl依赖于IClock接口;DefaultClockImpl、NormalClockImpl和DesignClockImpl实现接口,为了方便部分同学理解,总结一下UML图的基本语法:
类的UML图分为抽象类和非抽象类,抽象类名使用斜体字形;
接口的UML图,接口名使用斜体字形,同时使用《Interface》符号修饰;
泛化关系的UML图使用实线空心箭头表示;
关联关系的UML图使用实线箭头表示;
依赖关系的UML图使用虚线箭头表示;
实现关系的UML图使用虚线空心箭头表示
策略模式:定义了算法族,分别封装起来,让它们之间可以相互代替,此模式让算法的变化独立于使用算法的客户。
通用的UML类图:
学习设计模式的过程,比较简单的方法,记住每种设计模式通用的UML类图,依样画葫芦,创建同样结构的类关系,达到使用设计模式的目的,比如上文绘制钟表的例子,IClock对应Strategy,WatchViewImpl对应Context等
三、为什么用策略模式代替抽象类或接口?
在绘制钟表的例子中,另外的两种处理方法:
第一种:可以尝试将自定义视图类WatchViewImpl,声明为一个抽象类,声明抽象方法void paint(Canvas canvas),让子类实现绘制逻辑,不同的子类实现不同的效果,这样也是可以实现自定义视图的多样化的。
抽象类的UML类图:
不足地方:
如果存在另一个WatchView,比如:WatchViewCopy,该类想要使用DefaultWatchViewImpl绘制钟表的逻辑代码,通常的做法复制一份到WatchViewCopy的子类中,结果出现比较多的重复代码;类似的,如果想要使用另外两个子类的绘制逻辑,依然继续复制;
这个时候,策略模式的优势就体现出来了,处理绘制钟表代码的逻辑与WatchViewImpl没有联系,其他需要IClock接口的类,可以简单注入对应的实现类,可以实现代码的复用。
上面的说法如果不容易理解,那么再举一个例子,WatchView作为一个自定义视图类,通常可以在布局文件中使用,当用户选择使用DefaultWatchViewImpl,必然布局文件、Activity类关联的是DefaultWatchViewImpl的引用,如果这时候更换NormalWatchViewImpl,需要改动的地方势必比策略模式要多。
当然,还有别的原因,这里列举的两个不足,想要说明可以使用抽象类的地方可以使用策略模式代替,反过来则不行。
第二种:如果使用接口,定义接口IClock,定义方法void paint(Canvas canvas),让WatchView继承View的同时实现接口方法,处理绘制钟表的逻辑全部写在了自定义视图类内,这个时候的接口相比抽象类,扩展性能更不好,然后就可以因此引生出策略模式啦。
接口的UML类图:
四、总结
全文从引入绘制钟表的例子作为入口,让读者对策略模式的设计过程一个整体的认识;第二部分引入策略模式的定义、模板,让读者可以快速掌握该模式的使用;第三部分,从例子的角度出发,说明策略模式比抽象类或接口的优势,目的想要阐明:多用组合,少用继承的观点,对对象的行为可能需要复用的情况,选择使用策略模式代替抽象类,抽象类和接口同样有存在的优势,具体看开发的需要。