使用组合的设计模式 | 美颜相机中的装饰者模式

3,353 阅读7分钟

这是设计模式系列的第二篇,系列文章目录如下:

  1. 一句话总结殊途同归的设计模式:工厂模式=?策略模式=?模版方法模式

  2. 使用组合的设计模式 —— 美颜相机中的装饰者模式

  3. 使用组合的设计模式 —— 找对象要用的远程代理模式

  4. 用设计模式去掉没必要的状态变量 —— 状态模式

几乎所有的设计模式都是通过增加一层抽象来解决问题。

上一篇中提到的三个设计模式通过相同的手段来达到相同的目的:它们通过接口和抽象方法来新增抽象层以应对变化。

这一系列的后续几篇中会提到的四个设计模式通过相同的手段来达到不同的目的:它们通过新增一个类并持有原有类的方式实现对其扩展或限制。

这一篇先来看看装饰者模式。

装饰者模式就好像美颜相机,通过添加不同的装饰品,它可以让你变成另一个你。(虽然可能面目全非,但本质上还是你)

只复用类型

假设有四种饰品:耳环、钻石、黄金、羽毛。不同装饰品有不同价格,通常我们会这样做抽象:

//抽象饰品
public abstract class Accessory {
    public abstract String name();//饰品名称
    public abstract int cost();//饰品价格
}

//耳环
public class Ring extends Accessory {
    @Override
    public String name() { return "Ring"; }
    @Override
    public int cost() { return 20; }
}

//钻石
public class Diamond extends Accessory {
    @Override
    public String name() { return "Diamond"; }
    @Override
    public int cost() { return 1000; }
}

//黄金
public class Gold extends Accessory {
    @Override
    public String name() { return "Gold"; }
    @Override
    public int cost() { return 300; }
}

//羽毛
public class Feather extends Accessory {
    @Override
    public String name() { return "Feather"; }
    @Override
    public int cost() { return 90; }
}

现推出两款新饰品:黄金耳环,羽毛黄金耳环。同样的思路,使用继承可以解决问题:

public class GoldRing extends Accessory {
    @Override
    public String name() { return "GoldRing"; }
    @Override
    public int cost() { return 320; }
}

public class FeatherGoldRing extends Accessory {
    @Override
    public String name() {  "FeatherGoldRing"; }
    @Override
    public int cost() { return 1110; }
}
  • 如果继续推出更多的新品,比如羽毛耳环,钻石耳环,羽毛钻石耳环。。。每个新产品都用一个新的类表示,这样就会遇到子类膨胀的问题。
  • 除此之外,继承还有一个更致命的缺点:对单个类型的饰品没有统一的控制力。如果黄金涨价了,我们需要分别修改GoldRingFeatherGoldRing的价格,如果和黄金相关的饰品有好几十个,那简直是一场噩梦。
  • 在计算GoldRing价格的时候,我们并没有复用现有代码,即没有复用GoldRing已经定义的cost()行为,而只是通过继承复用了类型(GoldRing是一个Accessory)。只复用类型而没有复用行为的后果是:当Gold涨价时,GoldRing无感知。

有没有一种比继承更好的方案在现有饰品基础上扩展新的饰品?

既复用类型又复用行为

采用组合的方式就可以实现既复用类型又复用行为:

public class Gold extends Accessory {
    private Accessory accessory;
    public Gold(Accessory accessory) { this.accessory = accessory; }
    
    @Override
    public String name() {
        return "Gold " + accessory.name();
    }
    @Override
    public int cost() {
        return 300 + accessory.cost();
    }
}

public class Feather extends Accessory {
    private Accessory accessory;
    public Feather(Accessory accessory) { this.accessory = accessory; }

    @Override
    public String name() {
        return "Feather " + accessory.name();
    }
    @Override
    public int cost() {
        return 90 + accessory.cost();
    }
}
  • 上述四种饰品其实分为两类,耳环属于基本饰品,而羽毛、黄金、钻石属于附加饰品,附加饰品可以装饰基本饰品。
  • 附加饰品和基础饰品拥有相同的超类型Accessory,但附加饰品还通过组合的方式持有一个超类型实例,这样就可以通过注入超类型的方式将其和任意基础饰品组合到一起形成新的饰品。

用组合的方式实现羽毛黄金耳环:

Accessory ring = new Gold(new Feather(new Ring()));
  • 为了说明装饰与被装饰的关系,使用了带有俄罗斯套娃既视感的代码(虽然这样的代码可读性较差)。
  • Ring作为基础饰品被Feather装饰成羽毛耳环,羽毛耳环接着被Gold装饰成换羽毛黄金耳环。
  • 过程中并没有为羽毛黄金耳环新增一个叫FeatherGoldRing的子类,而是复用了现有的FeatherGold的行为。这样就解决了子类泛滥和控制力的问题。如果黄金涨价,只需要修改Gold.cost(),所有被Gold装饰的饰品价格都会随之而涨。
  • 这个方案还有一个更有用的好处:在运行时动态新增类型。通过继承新增的类型都是在编译时定死的,而通过组合的方式只要新增一行俄罗斯套娃式的代码,程序运行起来后就新增了一个类型,比如要新增“双倍黄金羽毛耳环”这个类型,只需要如下的代码:
Accessory ring = new Gold(new Gold(new Feather(new Ring())));

抽象的装饰者?

新的需求来了:基础饰品镶嵌附加饰品收取 10% 的一次性加工费。我们可以为所有附加饰品增加一层抽象:

public abstract class Decorator extends Accessory{
    private Accessory accessory;
    public Decorator(Accessory accessory) { this.accessory = accessory; }

    @Override
    public int cost() {
        return  1.1 * accessory.cost();
    }
}
  • Decorator通过组合持有超类型Accessory且规定了在构造时必须注入超类型,它还定义了镶嵌加工费的收费标准。
  • 现在就可以像这样重新定义附加饰品:
public class Gold extends Decorator {
    public Gold(Accessory accessory){ super(accessory); }

    @Override
    public String name() {
        return "Gold " + accessory.name();
    }
    @Override
    public int cost() {
        return 300 + super.cost();
    }
}
  • 其实对于装饰者模式来说,为装饰者定义一个抽象的父类不是必须的,只要满足继承超类型,以及持有超类型引用这两点就是装饰者模式。除非需要统一操作所有装饰者,比如在美颜相机这个场景中,需要通过遍历找出所有附加饰品。

题外话

使用装饰者模式后,GoldFeather中有一些样板代码,如果使用Kotlin可以将代码简化如下:

class Feather(val accessory: Accessory) : Accessory by accessory {
    override fun name(): String = "Feather" + accessory.name()
    override fun cost(): Int = 90 + accessory.cost()
}

class Gold(val accessory: Accessory) : Accessory by accessory {
    override fun name(): String = "Gold" + accessory.name()
    override fun cost(): Int = 300 + accessory.cost()
}

通过by关键词把类委托给一个具有超类型的成员变量accessory,如果不重写,类中的name()cost()的默认实现都将转发给accessory

总结

  • 装饰者模式是一种复用原有类并对其进行扩展的方式,它是继承的替代方法。
  • 装饰者模式比继承灵活,影响范围更小,因为它是给一个对象新增功能,而不是给对应类新增功能。
  • 装饰者模式通过继承原有类型实现复用类型。这一点很重要,因为所有使用原有类型的地方不需要修改代码就可以替换成装饰者。
  • 装饰者模式通过组合持有原有类实例实现复用行为。
  • 装饰者模式通过在调用原有类方法的前后插入新的逻辑实现功能扩展。
  • 装饰者模式符合开闭原则,即在新增功能的时候没有修改原有代码。
  • 装饰者模式特别适用于子类型之间可以有随机组合的场景,比如美颜相机的各种道具组合之后形成新的道具。

运用组合的设计模式不止装饰者一个,该系列的后续文章会继续分析“组合”在设计模式中的运用。

推荐阅读