从桥接模式到装饰者(套娃)模式的思考

413 阅读7分钟

一、前情提要

今天在B站学尚硅谷韩顺平老师的设计模式时,学到了装饰者模式这一章,给出的例子是咖啡店点咖啡,点咖啡时涉及到两个对象:1-咖啡单品(美式、意式等),2-配料(牛奶、豆浆、巧克力等),需求是模式要能够满足各种灵活的搭配,刚好这一章的前一章刚好讲的是桥接模式,所以我便顺着桥接模式的设计思路进行延展假设,虽然我设想的基于桥接模式的延展方案也能实现需求,但结果发现装饰者模式竟与我设想中的大相径庭,并且学完之后大受启发,特此记录。

二、需求梳理

要求模式能够实现以下点单搭配

1.一杯美式咖啡单品

2.一杯美式咖啡+一份巧克力

3.一杯美式咖啡+两份巧克力+一份豆汁儿

三、笔者的设想

既然笔者是顺着桥接模式进行的思路拓展,那就有必要先简单介绍下桥接模式:

桥接模式:

1.简介

将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变;

Bridge模式基于类的最小设计原则,通过使用封装、聚合及继承等行为让不同的类承担不同的职责,它的主要特点是把抽象与行为实现分离开来,从而可以保持各部分的独立性以及对功能的拓展;

2.上个例子

需求如下:手机具有"样式"和"品牌"两个分类指标,要求通过"一种样式+一种品牌"来指定一款手机

若采用一般的"金字塔结构",则会衍生出很多种搭配,每新增一种品牌或样式时,要做出的新增和修改都是很繁杂的,且违反了设计模式的"开闭原则",即需要对原有代码做拓展或修改;

但若此时采用桥接模式将两者分离,就可避免上述结构面对修改时导致的"类爆炸"问题; 将手机样式与手机品牌作为桥接的两端,将品牌聚合到样式中,这样在新增手机样式或品牌时,只需要增加一个类即可,遵循"开闭原则";

换个角度考虑,朋友们,笔者看到这里的时候,觉得有些东西似曾相识,细想一下发现,这不就是把"手机样式"和"手机品牌"两者之间的一对多关系改造成了多对多嘛?

这也是我们通过桥接模式得出的最核心且应用会更广泛的结论:当发现现有系统中因两个类之间的关系太过死板导致功能拓展时关联改动很大,则需要考虑是否可以把这两个类的关系进行拓展,通过提取公共父类(或接口)的形式,将一对一改成一对多,一对多改成多对多

回到需求

经过对桥接模式的理解,我们发现第1点和第2点比较容易满足,参照手机样式与品牌的例子,将配料(Seasoning)聚合到咖啡(Coffee)中即可实现,示例1将Seasoning设置为Null,示例2将巧克力传入LongBlackCoffee的构造器;

解决思路

但第3点则稍微需要做出些改动,因为一杯咖啡可以有多种配料,这好办啊,将聚合对象进行横向拓展,把配料对象改为配料集合不就行了?安排!

部分代码

1.咖啡父类(Coffee)

/**
 * @author 郭超
 * Date:2020-11-26 13:46
 * Description: 咖啡
 */
public class Coffee {

    public String name;

    private float price = 0.0f;

    private List<Seasoning> seasonings = new ArrayList<>();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getPrice() {
        return price;
    }

    public void setPrice(float price) {
        this.price = price;
    }

    /**
     * 添加调料
     *
     * @param seasonings 调料集合
     */
    public void addSeasonings(List<Seasoning> seasonings) {
        this.seasonings.addAll(seasonings);
    }

    /**
     * 求当前这杯咖啡的价格
     *
     * @return 调料+咖啡单品的价格
     */
    public double cost() {
        Double seasoningPrice = this.seasonings.stream().collect(Collectors.summingDouble(Seasoning::getPrice));
        return seasoningPrice + price;
    }
}

2.美式咖啡(LongBlackCoffee)

public class LongBlackCoffee extends Coffee {
    public LongBlackCoffee() {
        setName("美式咖啡");
        setPrice(5.0f);
    }
}

3.配料父类(Seasoning)

public class Seasoning {

    public String name;

    private float price = 0.0f;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public float getPrice() {
        return price;
    }

    public void setPrice(float price) {
        this.price = price;
    }
}

4.巧克力(Chocolate)

public class Chocolate extends Seasoning {
    public Chocolate() {
        setName("巧克力");
        setPrice(3.0f);
    }
}

5.牛奶(Milk)

public class Milk extends Seasoning {
    public Milk() {
        setName("牛奶");
        setPrice(2.0f);
    }
}

6.实现第三个需求

/**
 * @author 郭超
 * Date:2020-11-26 14:49
 * Description: 整一杯加两份巧克力+一份牛奶的美式咖啡
 */
public class CoffeeBar {

    public static void main(String[] args) {
        EspressoCoffee espressoCoffee = new EspressoCoffee();
        List<Seasoning> seasonings = new ArrayList<>();
        // 准备两份巧克力
        seasonings.add(new Chocolate());
        seasonings.add(new Chocolate());
        // 准备一份牛奶
        seasonings.add(new Milk());
        // 加进咖啡里
        espressoCoffee.addSeasonings(seasonings);
    }
}
其实如果只是单纯的实现需求的话,如此设计已经足够使用了,遵循开闭原则,添加新的咖啡或配料都不影响原有代码,但详细了解了装饰者模式后,我发现"模式拓展"还可以从另一个维度进行

四、未曾设想的道路——装饰者模式

上文咱们已经进行过分析,得出的结论是:第三点需求的关注点在于,如何在咖啡(Coffee)和配料(Seasoning)在多对多关系(提取了公共父类)基础上,再实现一层"一对多"的关系

笔者采用的是横向拓展,将聚合对象从单一对象改为了List集合,而"装饰者模式"则通过嵌套迭代的方式纵向拓展(套娃),接下来我们就看看装饰者模式是如何套娃的

1.简介

想要真正理解装饰者模式,只需要参透装饰者提出的一个最为关键的哲学思想,在本文使用的例子中,该哲学思想的体现为 "加入了咖啡的调料也是咖啡";

好的,本篇博客对装饰者的介绍到这里就结束了,感谢大家的阅读,如果您觉得这篇博客有帮助到您的话记得点赞评论哦

.

.

.

.

.

.

.

.

.

开个玩笑而已啦

2.先通过UML图来看一下装饰者模式的结构

该结构与笔者的设想最大的不同有两点:

1.原来代表咖啡(Coffee)和配料(Dectrator)的两个类,共同继承了抽象类类Drink

2.聚合(组合)的方向发生了变化

第一点的不同正对应了 "加入了咖啡的调料也是咖啡",而第二点的不同,是因为不采用List的话,在一对多的关系中,一端ID并入多端(一种咖啡对应多种配料),很神奇有木有?这是数据库中的一对多关系的设计原则,虽然这样理解略显牵强,但思想是这么个思想;

接下来我们仔细梳理一下这样设计的目的,将目光聚焦到"被装饰者(Drink)"与"装饰者(Decorator)"上。

1.被装饰者Drink

public abstract class Drink {

    public String description;

    private float price = 0.0f;

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public float getPrice() {
        return price;
    }

    public void setPrice(float price) {
        this.price = price;
    }

    public abstract float cost();
}

被装饰者Drink只有一个作用:存储咖啡(单品/加料咖啡)的信息

2.装饰者Decorator

装饰者与被装饰者存在两重关系,"继承"与"聚合",我们先看下核心代码:

public class Decorator extends Drink {

    private Drink drink;

    public Decorator(Drink drink) {
        this.drink = drink;
    }
    
    @Override
    public float cost() {
        return super.getPrice() + drink.cost();
    }

    @Override
    public String getDescription() {
        return super.getDescription() + drink.getDescription();
    }
}

装饰者Decorator存在两重作用:

1.Decorator代表"加入配料后的咖啡",聚合的drink代表"加入配料前的咖啡",两者的信息差通过重写的cost()和getDescription()方法写入了Drink

2.Decorator继承Drink,是为了实现迭代

3.使用实例

只需要创建具体的咖啡类型后,再调用装饰者Decrator子类的构造器,即可实现对咖啡单品的"调用"

public class CoffeeBar {

    public static void main(String[] args) {
        Drink drink = new LongBlackCoffee();
        System.out.println("咖啡原价 = " + drink.cost());
        drink = new Chocolate(drink);
        System.out.println("加一份巧克力后价格 = " + drink.cost());
        drink = new Chocolate(drink);
        System.out.println("加两份巧克力后价格 = " + drink.cost());
        drink = new Milk(drink);
        System.out.println("再加一份牛奶后价格 = " + drink.cost());
    }
}

4.核心提炼:同时使用继承与聚合,可以通过迭代灵活地实现两个类(接口)之间的一对多关系

五、结语

对于本文提到的两种拓展方式(笔者设想-横向拓展,装饰者模式-纵向拓展),如果只是为了解决文中提出的需求的话,笔者也无法判断两者孰优孰劣,如果这篇博客对你有帮助的话,希望能给笔者留个点赞👍。有何感想或建议,也欢迎在评论区留言讨论。