装饰者模式
装饰者模式定义
装饰模式又名包装(Wrapper)模式。是一种在不改变原有对象的基础之上,将功能附加到对象上。提供了比继承更有弹性的替代方案(扩展原有对象功能)。
装饰者模式能够动态地将责任附加到对象身上的一种设计模式,若要扩展功能,装饰者提供了比继承更有弹性的替代方案,比生成子类更加灵活。通常在继承关系中,为了扩展功能需要新增子类进行扩展,而装饰者模式,可以在不扩展子类的情况下,将对象的功能进行动态的扩展。
下面我们通过一个具体的例子,详细介绍一下装饰者模式:
某教育咨询公司“学帮忙”,为了帮助学生更好的了解每个大学信息,打算做一个大学院校综合实力评分系统。系统可以根据每个大学的信息,计算出大学的综合实力得分:
-
普通本科记10分,独立学院记8分,中外合办记8分,高职专科学校记5分。
-
211和985学校额外分别加20分。
-
每一个硕士点加1分。
-
每一个博士点加2分。
于是他们针对这种需求,进行了软件的设计和开发。
image.png设计一个抽象的大学接口,然后每个大学对其进行实现,这样就可以计算出每个大学的的综合实力得分。
但是,当他们开始对系统进行实现时,百度了一下中国有多少所大学。。。
image.png将近3000所大学,如果按照这个模式进行实现的话,那么将有3000个具体的类实现,很显然,这是不现实的。。。
而且就算我们开始的时候发动人力,对这3000所学校进行了实现,但是如果以后计算规则发生改变,比如一个211院校额外加15分,那么再回过头去修改这3000个学校,将又是很大的一个工程。
那么有没有办法能不生成这么多类呢?聪明的你一定已经想到,可以在子类里面标记大学的具体信息,然后在基类里面计算大学得分。
image.png结合代码我们看一下
public abstract class University {
public abstract String getUnvName();
public abstract int getUnvType();
public abstract boolean is211();
public abstract boolean is985();
public abstract int getMasterNum();
public abstract int getDoctorNum();
public int getPoint(){
int point = 0;
switch (getUnvType()){
case 0: //普通本科
point = 10;
break;
case 1: //独立学院
case 2: //中外合资
point = 8;
break;
case 3: //高职院校
point = 5;
break;
}
if (is211()){
point += 20;
}
if (is985()){
point += 20;
}
point += getMasterNum();
point += getDoctorNum() * 2;
return point;
}
}
我们结合上面的类图和代码,可以看出,我们如果想得到一个大学的分数,只需要让他继承University类,然后调用getPoint方法即可,但是这种方案就没有弊端了吗?
设想一下几个场景:
-
计算规则变了,211院校额外加25分。
-
新增了计算规则,对于有强基计划的学校额外加10分。
如果发生这样的需求变更的话,作为开发者的我们都知道,需求是肯定会变更的。。。当需求变更了,我们必须要修改基类了,不是不可以,但是这样做违背了软件设计的开闭原则(类应该对扩展开放,对修改关闭。)
那么怎么才能避免上面这些问题呢?这就需要用到本文介绍的设计模式了,装饰者模式:
在不改变原有对象的基础之上,将功能附加到对象上。提供了比继承更有弹性的替代方案(扩展原有对象功能)
我们先来看一下装饰者模式的类图。
image.png通过上面的类图我们可以了解装饰者模式的具体结构样式,把这个模式套用到大学评分系统中,可以得到如下的类图。
image.png在该系统中,四种不同的大学类型,是我们扩展大学基类后获得的不同类型院校,分别是普通本科,独立学院,中外合办还有高职专科。UnvFeature类是装饰者的基类,用来标识大学的综合实力,也就是评分的标准。最下面四个是具体的装饰者,分别用于标识大学是否是211,985以及大学的硕士和博士点个数。
大学装饰者模式的类我们都已经实现好了,那么怎么才能计算出大学的得分呢?
image.png比如我们新建一个天津大学,然后分别用211,985和硕士博士的装饰者对其进行装饰,这样一来,我们最后得到的一个包裹了很多层,但是依然是University的对象,调用这个University的getPoint方法,那么就可以逐层计算得分,最后返回一个具体得分。
下面我们看一下具体的代码实现:
**
* 大学基类
*/
public abstract class University {
public abstract String getUnvName();
public abstract int getPoint();
}
/**
* 普通本科
*/
public class NormalUniversity extends University {
String name;
public NormalUniversity(String name) {
this.name = name;
}
@Override
public String getUnvName() {
return name;
}
@Override
public int getPoint() {
return 10;
}
}
/**
* 大学特征装饰者
*/
public abstract class UnvFeature extends University{
}
/**
* 211院校
*/
public class Unv211 extends UnvFeature {
University unv;
public Unv211(University unv) {
this.unv = unv;
}
@Override
public String getUnvName() {
return null;
}
@Override
public int getPoint() {
return unv.getPoint() + 20;
}
}
/**
* 硕士点
*/
public class UnvMaster extends UnvFeature {
University unv;
int masterNum;
public UnvMaster(University unv, int num) {
this.unv = unv;
this.masterNum = num;
}
@Override
public String getUnvName() {
return null;
}
@Override
public int getPoint() {
return unv.getPoint() + masterNum;
}
}
这里只列举几个特征类的实现方式,其他类的实现也与此类似,因篇幅问题不在具体列出。
那么我们计算一个大学的得分点,只需要执行如下代码
University tju = new NormalUniversity("天津大学");
tju = new Unv211(tju);
tju = new Unv985(tju);
tju = new UnvMaster(tju, 39);
tju = new UnvDoctor(tju, 29);
int point = tju.getPoint();
到此,我们的大学评分系统设计完成。回过头看一看我们上面的两个问题是否得到解决:
问题1.如果211大学评分标准发生改变,那么我们只需要修改211大学这个装饰者的类即可,不需要修改基类,也不需要变动计算方法。
问题2.如果新增计算标准,那么我们只需要新增一个装饰者即可。比如新增双一流大学可额外加10分,我们可添加如下新的装饰者:
/**
* 双一流院校
*/
public class UnvDual extends UnvFeature {
University unv;
public UnvDual(University unv) {
this.unv = unv;
}
@Override
public String getUnvName() {
return null;
}
@Override
public int getPoint() {
return unv.getPoint() + 10;
}
}
对此,我们的问题得到圆满解决。
装饰者模式的在实际工作中的运用
在Android开发过程中,我们最常用的四大组件之一就是Activity了,那你知道Activity中所包含的设计模式吗?要搞清楚这一点,我们先来看看Activity相关的类图。
image.pngContext是个抽象类,它的唯一实现是ContextImpl。ContextWrapper通过包装ContextImpl来实现扩展,保证了ContextImpl类最原始的上下文特性。然后通过ContextWrapper的多态实现,来生成系统组件Activity,Service,Application。ContextWrapper是Android系统中一个很重要的类,从名称上不难看出就是使用了装饰者模式。通过ContextWrapper构造方法发现传入了一个Context对象,并赋给mBase变量,ContextWrapper就是包装了mBase。
public class ContextWrapper extends Context {
Context mBase;
public ContextWrapper(Context base) {
mBase = base;
}
/**
* Set the base context for this ContextWrapper. All calls will then be
* delegated to the base context. Throws
* IllegalStateException if a base context has already been set.
*
* @param base The new base context for this wrapper.
*/
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
//......
而Application,Activity和Service都相当于具体的装饰者,他们分别具有自己的特性,同时又对Context进行了包装,因此他们都可以提供各种获取系统环境的方法, 比如getResource, getPackageManager, getContentProvider等。
如果Android系统后期考虑添加新的系统组件,可以通过继承ContextWrapper这个类,并且实现它自己应有的功能,这样不需要修改已经很完善的Activity等类,就可以通过添加新的装饰者实现新的需求。
装饰者模式的优点和缺点
1.继承的有力补充,比继承灵活,不改变原有对象的情况下给一个对象扩展功能。(继承在扩展功能是静态的,必须在编译时就确定好,而使用装饰者可以在运行时决定,装饰者也建立在继承的基础之上的)
2.通过使用不同装饰类以及这些类的排列组合,可以实现不同的效果。
3.符合开闭原则
同样,装饰者模式也是有一定的缺点的
1.会出现更多的代码,更多的类,增加程序的复杂性。
2.动态装饰时,多层装饰时会更复杂。(使用继承来拓展功能会增加类的数量,使用装饰者模式不会像继承那样增加那么多类的数量但是会增加对象的数量,当对象的数量增加到一定的级别时,无疑会大大增加我们代码调试的难度)
比如我们看一下JAVA中的装饰者模式
image.png
当我们第一次接触InputSteam相关内容时,是不是很难理解每一个具体装饰者的作用?只有当我们对每一个具体InputSteam都进行尝试之后,才知道什么时候该用哪个去实现我们所需要的功能。
所以在程序设计中,要根据具体使用场景,选择最适合的设计模式,发扬其优点,尽量避免其缺点的扩大。