1.场景
最近玩吃鸡玩的很嗨,我们可以看到游戏里面五花八门的装备,应接不暇。玩的同时也不禁感叹开发者的强大,那么假定让我们自己来设计这样一个简单的换装小游戏,我们如何来建立一个易于维护,方便拓展的体系呢?
首先假定我们遇到的需求是,设计一个穿戴武器头盔的玩家,我们可能会这么写:
public class Player {
private void have98K() {
System.out.println("装备98k");
}
private void haveHelm() {
System.out.println("装备头盔");
}
private void haveHandgun() {
System.out.println("装备手枪");
}
public void show() {
have98K();
haveHelm();
haveHandgun();
System.out.println("的玩家");
}
}
然后在客户端调用
public static void main(String[] args) {
Player player = new Player();
player.show();
}
//打印如下
装备98k
装备头盔
装备手枪
的玩家
设计的缺陷很明显,假设我们拓展了新的装备类型,比如步枪,手雷,背包等一系列的东西,我们需要修改Player这个类,拓展的越多,这个类就越臃肿,最后导致每次新添类型都需要修改源码,无法维护。
这样明显不够面向对象,我们来个面向对象优化版的:
public class Player {
public void show() {
System.out.println("的玩家");
}
}
//装备基类
public abstract class Equip {
public abstract void show();
}
public class Equip1 extends Equip {
private void quip1() {
System.out.println("装备98k");
}
@Override
public void show() {
quip1();
}
}
public class Equip2 extends Equip {
private void quip2() {
System.out.println("装备头盔");
}
@Override
public void show() {
quip2();
}
}
public class Equip3 extends Equip {
private void quip3() {
System.out.println("装备手枪");
}
@Override
public void show() {
quip3();
}
}
//最后是客户端的调用
public static void main(String[] args) {
Player player = new Player();
Equip equip1 = new Equip1();
Equip equip2 = new Equip2();
Equip equip3 = new Equip3();
equip1.show();
equip2.show();
equip3.show();
player.show();
}
//打印
装备98k
装备头盔
装备手枪
的玩家
这样我们拓展装备的时候可以随时新建,而不需要修改Player中的代码,起到了解耦的作用。但是,总感觉哪里不对,因为我们知道,一个玩家带着展示的装备,更应该是在他内部的组装完成,我们的代码总觉得少了些 组装 的感觉,而如果我们将组装放到Player内部中,就又回到了起点(新添需要修改),这是矛盾的,我们需要对拓展开放,对修改关闭。那么有人就会这么做,通过继承Player来创造新的玩家类,来达到内部组合的目的,比如,拥有枪头盔的玩家,拥有背包轿车的玩家,但是,装备的多样化,导致了组合个数的指数型增长,这样衍生的玩家类数目也会爆炸性增长,很明显,通过继承是解决不了问题的。
那么怎么优化呢?这里有一个很好的设计模式可以帮我们解决问题:
2.定义
装饰者模式:
动态地将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
来看看类图:
分析一下:
- AbstractComponent : 抽象组件类,这个是装饰者和被装饰者的基类,用来规范准备接收附加责任的对象。
- ConcreteComponent : 具体组件类,也就是被装饰者,定义被拓展的功能。
- Decorator :抽象装饰者类,内部持有一个Component对象的引用用以为其拓展功能。
- ConcreteDecoratorA ,B :具体装饰者类,负责为被装饰者“添加”拓展的功能。
疑问:为什么被装饰者类和装饰者类需要继承自同一基类
我们很疑惑,因为装饰者模式定义上采用组合的方法来取代继承,但是这里还是使用了继承。为什么?这么做的重点在于,装饰者和被装饰者必须是一样的类型,也就是有共同的超类,利用继承,达到的是 类型匹配 的目的,而不是 获得行为 。
3.优化
掌握了装饰者模式的定义,我们用来优化我们自己的例子:
//抽象组件/角色,定义展示功能
public abstract class ShowComponent {
public abstract void show();
}
//被装饰者
public class Player extends ShowComponent {
@Override
public void show() {
System.out.println("的玩家");
}
}
//抽象装饰者,持有抽象组件的引用
public abstract class Equip extends ShowComponent {
ShowComponent base;
Equip(ShowComponent base) {
this.base = base;
}
}
//具体被装饰者
public class Equip1 extends Equip {
public Equip1(ShowComponent base) {
super(base);
}
private void quip1() {
System.out.println("装备98k");
}
@Override
public void show() {
quip1();
base.show();
}
}
public class Equip2 extends Equip {
public Equip2(ShowComponent base) {
super(base);
}
private void quip2() {
System.out.println("装备头盔");
}
@Override
public void show() {
quip2();
base.show();
}
}
public class Equip3 extends Equip {
public Equip3(ShowComponent base) {
super(base);
}
private void quip3() {
System.out.println("装备手枪");
}
@Override
public void show() {
quip3();
base.show();
}
}
//调用
public static void main(String[] args) {
new Equip3(new Equip2(new Equip1(new Player()))).show();
}
//打印
装备手枪
装备头盔
装备98k
的玩家
这样,当出现新的装备类型,我们只需要添加新的具体装饰者即可,并且我们发现每一个装饰者,都不关心自己具体被放到了这个链中的哪一环,我们可以随意调换顺序。
4.android中的装饰者模式
一开始说到装饰者模式你肯定很陌生,但是要是提到
BufferedInputStream bis =
new BufferedInputStream(new FileInputStream(new File("/home/user/abc.txt")));
你肯定很熟悉,是的,java中的io流使用的就是装饰者模式,不过由于io流族谱的类实在过多,基于篇幅的原因,我们这里换一个简单一些,并且为我们所熟知的,Context继承体系,先来看类图:
是不是完全符合了装饰者模式的类图?我们可以看到Context是一个抽象类,而具体实现全部在ContextIml中,Activity等组件的超类ContextWrapper,持有的mBase对象引用即是ContextIml,并且所有行为都调用这个实现类的方法,其他组件作为装饰类,拓展ContextIml的功能。
5.要点
- 1.组合和继承都是为了拓展对象的功能,继承在编译期间决定,而组合提供了更高的灵活性。
- 2.通过使用不同的具体装饰类以及这些装饰类的排列组合,可以设计出很多不同的组合。
- 3.当需要执行特殊行为时,客户端可以根据需求有选择地,按顺序的组合包装所需要修饰的对象。
- 4.当存在过多的装饰者,也会导致程序中出现很多细小的对象,使得程序变得复杂。