本博文碍于作者的学识与见解,难免会有疏漏错误之处,请谅解。

转载请注明出处: https://www.morcat.cn 谢谢~

前言

这篇文章主要想讲述一下设计模式中的装饰器模式。其实我是不太敢写关于设计模式的博文的,一是觉得用好设计模式本身就是需要大量经验作为基础,目前的我还欠些火候,二是觉得设计模式作为一种经验之谈,本来就很难描述清楚其精髓,网上查阅的博文大都只是扔了几个类图,放上代码就完事了,但作为读者真的是一脸懵逼...不过呢尽管这样,我也想尝试一下看看自己能不能解释清楚这个东西,也检查下自己究竟掌握了多少。这个系列的博文,我会尽量以一个读者的角度由浅入深的去了解装饰器模式的由来和使用。

为什么要用装饰器模式?

从某款《传奇》网游的需求开始

我们今天的主角是一个叫Skrrr的程序员,任职于某著名游戏公司"贪玩红月",最近公司在研发一款传奇类网游,自然请了众多大牌明星来代言哦。公司的业务是当用户被广告吸(e)引(xin)到之后,马上就能一键注册,瞬间创建自己的游戏人物。职业有战士,刺客,射手等等等,当然每个职业的初始属性是不同的。现在需要Skrrr同学,实现创建游戏角色的功能。

当然,这个简单的需求自然难不倒skrrr同学,它的设计如下:

image.png

这真的很简单,每个职业继承于一个游戏角色的类就行了,以后如果要新增职业那就新建类就好啦,也方便扩展。于是,Skrrr同学看着自己满意的杰作开开心心的回家睡觉了。可是第二天,公司发现单单让用户创建几个职业根本赚不到钱呀,为了让用户体验到氪金的乐趣,让充钱的土豪在出生的时候就赢在起跑线上。于是推出了3种出生套餐,充了99元的用户,出生可以获得一把大剑,充了999的用户出生可以获得金丝软甲,充了9999的用户出生甚至可以获得上古神器——倚天剑......当然这些装备都是有属性加成的,不同的装备加的属性不一样。

这下Skrrr同学可头大了,没办法只能硬着头皮加班加点完成了这些功能,这一次它的设计如下:

image.png

经过了一个晚上的战斗,Skrrr同学根据相应的职业创造出了各式各样的类,虽然很累,但是至少也是完成了,于是Skrrr同学看着自己似乎不是很满意的杰作疲惫的回家睡觉了。结果第二天,Skrrr同学终于发现事情并没有那么简单,噩梦才刚刚开始...原来公司的野心比自己想的还大,只要用户钱到位什么姿势它都会...公司推出了各式各样的出生套餐,什么匕首啊,屠龙刀啊等一堆装备。更夸张的是,当客户充满88888元后解锁多装备成就,左手屠龙刀右手倚天剑,身穿软猬甲,脚踩风火轮不是梦!当这些需求扔到Skrrr同学面前时,他终于崩溃了,增加一件装备就等于给每个职业加一个类,更何况时多件装备的组合,而且未来万一再新增职业,这工作量可是成几何倍的增长啊。

Skrrr同学这下真没辙了,哪怕加班到猝死,这种变态量的需求也应付不过来呀,那还有什么办法呢?

开闭原则

可以发现在上面的设计中,角色和装备这两个东西之间是完全耦合的,当一个装备进行了新增,那么相应的所有角色都要新增一个对应的类。同样的如果一个角色属性发生了修改,那么所有使用装备的该角色都要修改。这就导致了类非常多,同时很难扩展。这种方法其实违反了六大设计设计原则中的“开闭原则”。

开闭原则: 一个软件实体(如类,模块和函数)应该对扩展开放,对修改关闭。

其实在软件开发的过程中,改需求是非常频繁的一件事情。开闭原则 则是希望使用扩展的方式来进行改变,而不是修改现有代码的方式进行改变。这样的好处在于大大增加了系统的稳定性以及灵活性,降低代码劣化速度。

在Skrrr同学的设计里,如果任何一件装备的属性或者角色的属性改变了,那么所有有关它们的类全部都要修改,会导致后期的维护非常繁琐。而在设计之初如果能考虑到这一方面,使用合适的架构来设计类,就能使得角色与装备之间解耦,做到灵活配置。

当然对于整个软件架构完完全全做到开闭原则是几乎是不可能的,因为想要遵循开闭原则通常要引入一些新的抽象层次,增加代码的复杂度。而我们所要做的应该是在软件规划的时候就提前找到最容易变化的地方,针对性的应用开闭原则。

以上写了这么多其实就是想说明Skrrr同学遇到的这种困难完全是因为它违反了"开闭原则"而导致的。那么如何解决这个问题,或者说如何让代码遵循“开闭原则”,就可以用到我们今天所要讲的装饰器模式

装饰器模式的使用

装饰器模式的定义:动态地将责任附加到对象上。想要扩展功能,装饰者提供有别于继承的另一种选择

其实装饰器模式挺好理解的,顾名思义,就是通过对一个类或者说对象进行装饰,而使得不修改这个类就能达到动态扩展的目的。这种模式会创建一个装饰类,用来包装原有的类,同时该装饰类也同样会保持原有类的类型。它的类图如下:

image.png

从类图中可以发现一个很特别的点,装饰器类继承了我们被装饰类的同时又持有了被装饰的类。我个人觉得这就是装饰器模式的精髓,利用了组合+继承的方式实现动态扩展。通过继承的方法继承了被装饰类的类型,但是并没有继承被装饰类的行为,而是通过装饰者和组件组合的方式,重新定义了类的行为。

利用装饰器实现《传奇》网游的新需求

Skrrr同学认认真真的了解了装饰器模式后,终于顿悟了。在一个晚上的努力后,这一次它的新设计如下:

image.png

使用了装饰器模式之后,装备和角色完全解耦,新增一件装备就直接扩展新的类即可,再也不用处理一堆类了。不仅如此,面对同时要求携带多个装备的需求也相应解决。

代码实现

  • 游戏角色的基类AbstractCharacter(被装饰的类):
@Data
public abstract class AbstractCharacter {
    /**
     * 生命值
     */
    private Integer hp;

    /**
     * 攻击力
     */
    private Integer atk;

    /**
     * 防御力
     */
    private Integer def;

    /**
     * 速度
     */
    private Integer spd;

    /**
     * 职业名称
     */
    private String name;

    /**
     * 人物描述
     */
    public String describe() {

        return name;
    }
}
  • 游戏装备类AbstractEquipmentDecorator(装饰类):

在这里可以看到在装备类中我们要求继承的基类中所有的方法都设置为了Abstract必须要重写。还记得上文所提到的么,装饰类继承被装饰类是为了继承被装饰类的类型,而不是被装饰类的行为。所有的行为都是通过组合的方式去重新定义行为

public abstract class AbstractEquipmentDecorator extends AbstractCharacter {

    /**
     * 该方法必须重写
     */
    @Override
    public abstract Integer getHp();

    /**
     * 该方法必须重写
     */
    @Override
    public abstract Integer getAtk();

    /**
     * 该方法必须重写
     */
    @Override
    public abstract Integer getDef();

    /**
     * 该方法必须重写
     */
    @Override
    public abstract Integer getSpd();

    /**
     * 该方法必须重写
     */
    @Override
    public abstract String describe();
}
  • 骑士(游戏角色实现类):

定义了最简的初始属性

public class Knight extends AbstractCharacter {
    public Knight() {
        this.setHp(80);
        this.setAtk(45);
        this.setDef(70);
        this.setSpd(30);
        this.setName("骑士");
    }
}
  • 大剑(游戏装备实现类)

通过构造方法传参,我们允许让大剑装饰类允许引用一个角色类,这个角色类不一定就是战士/刺客这种角色实现类哦,还有可能也是一件装备(甚至可以用无数个包装类包装一个组件)

public class Sword extends AbstractEquipmentDecorator {

    /**
     * 通过组合的方式持有了游戏角色基类
     */
    private AbstractCharacter character;

    public Sword(AbstractCharacter character) {
        this.character = character;
    }

    @Override
    public Integer getHp() {
        return character.getHp();
    }

    /**
     * 在原有角色基础上攻击力+30
     */
    @Override
    public Integer getAtk() {
        return character.getAtk() + 30;
    }

    /**
     * 在原有角色基础上防御力+5
     */
    @Override
    public Integer getDef() {
        return character.getDef() + 5;
    }

    @Override
    public Integer getSpd() {
        return character.getSpd();
    }


    /**
     * 将原有角色 携带的所有都打印出来
     */
    @Override
    public String describe() {
        return character.describe() + " 装备了一把大剑";
    }
}

测试一下

  • 测试类:
public class Main {

    public static void main(String[] args) {

        //创建一个持剑带铠甲的刺客
        AbstractCharacter assassinWithSwordAndArmor = new Assassin();
        assassinWithSwordAndArmor = new Sword(assassinWithSwordAndArmor);
        assassinWithSwordAndArmor = new Armor(assassinWithSwordAndArmor);

        System.out.println(assassinWithSwordAndArmor.describe());
        System.out.println(" 攻击力:" + assassinWithSwordAndArmor.getAtk());
        System.out.println(" 防御力:" + assassinWithSwordAndArmor.getDef());
        System.out.println(" 生命值:" + assassinWithSwordAndArmor.getHp());
        System.out.println(" 速度:" + assassinWithSwordAndArmor.getSpd());
        System.out.println("===================================");

        //创建一个双剑骑士
        AbstractCharacter doubleSwordKnight = new Knight();
        doubleSwordKnight = new Sword(doubleSwordKnight);
        doubleSwordKnight = new Sword(doubleSwordKnight);

        System.out.println(doubleSwordKnight.describe());
        System.out.println(" 攻击力:" + doubleSwordKnight.getAtk());
        System.out.println(" 防御力:" + doubleSwordKnight.getDef());
        System.out.println(" 生命值:" + doubleSwordKnight.getHp());
        System.out.println(" 速度:" + doubleSwordKnight.getSpd());
        System.out.println("==================================");
    }
}
  • 打印:

刺客 装备了一把大剑 装备了一具铠甲
 攻击力:110
 防御力:85
 生命值:70
 速度:50
===================================
骑士 装备了一把大剑 装备了一把大剑
 攻击力:105
 防御力:80
 生命值:80
 速度:30
==================================

至此Skrrr同学完全实现了动态拓展游戏角色和装备。无论是装备还是角色再如何修改与变动,都只要专注于修改它们本身即可。这种方式看起来似乎万无一失,但是不是真的完美呢?

装饰器模式仍存在的问题

以上大致介绍了装饰器模式的使用方式。我们可以了解到装饰器模式最大的好处在于使得装饰对象与被装饰对象解耦,使得被修饰的类可以动态拓展。并且他完全符合开闭原则,解决了以前使用继承而导致的类爆炸问题。

当然所有设计模式都不是完美的,它只适用于特定的情况。装饰器模式自然也有些弊端:

  1. 这种多层修饰会导致类的层次比较复杂会衍生出许多包装类,因此如果只是简单的几个类的扩展的话,完全没必要使用装饰器模式。
  2. 可以动态装饰,但是不能动态的撤销装饰。在上述例子中,虽然每个角色可以动态携带各种装备,但是一旦人物创建完成后,未来想要把某件装备换下就做不到了(想要解决这个问题,可以采用策略模式,未来应该会有介绍)。

因此不止是装饰器模式,任何设计模式在使用前需要仔细考量需求,判断设计模式是否真的符合当前情况。在合适的地方使用合适的设计模式的确可以大大优化系统架构,但是如果滥用设计模式的话反而会导致系统更加的复杂难懂。当然这种判断力自然是需要经验的累积的,只有踩过了足够的坑才知道什么才是最适合的。

JDK中的装饰器模式

JDK中其实也运用了许许多多精巧的设计模式。装饰器模式自然也有,最经典的就是Java的IO流。IO流中分为了节点流与处理流,节点流如:FileInputStream,ByteArrayInputStream。处理流如:BufferedInputStream,DataInputStream。其中节点流其实就是被装饰的对象,而处理流则是基于节点流的装饰对象。其类图如下:

image.png

可以看到所有的处理流类都继承于FilterInputStream的抽象包装类,类似于例子中的装备类。而所有节点流都直接继承InputStream的基类,类似于例子中的角色类。当然正是应用了装饰器模式,所以导致Java.io包的各种流特别多,造成了一定的复杂度,当然为了好的扩展性,这种设计模式是利大于弊的。

小结

今天所介绍的是设计模式之——装饰器模式。洗完后其实感觉这篇文章思路并不是特别清晰,同时也碍于自己的水平设计模式这一块的分析并不是特别到位,以后说不定还会有所修改吧。那简单介绍下我个人认为装饰器模式的几个要点:

  1. 装饰器模式通过继承+组合的方式在动态拓展类的行为的同时,仍保留了原有类的类型。
  2. 可以用无数个装饰类包装一个组件。
  3. 装饰器模式符合“开闭原则”,即对拓展开放,对修改关闭。
  4. 不要滥用装饰器模式,使用装饰器模式会产生许多小的包装类,需要合理选择。

参考资料

《HeadFirst设计模式(中文版)》

Q.E.D.


吃的苦中苦,卷成王中王🏆