延续上一篇装饰器模式的话题,我们继续对需求进行升级。

示例

需求

还是以奶茶店为例,但是我们不再仅仅考虑奶茶的成分了,要想奶茶卖的好,还得需要一个响亮的品牌,奶茶有很多品牌,如一点点,COCO,喜茶等,除此之外,我们还要对奶茶的规格进行区分,如大杯、中杯、小杯等,不同品牌价格不同,不同规格价格也不同(不考虑太复杂的情况,就假设每种品牌和规格都有一个价格基数,总价直接累加)。

初级方案

乍一看,跟上一篇装饰器模式中的需求没什么区别,不就是把配料换成了品牌和规格吗?我们还是先看看继承的方案:
设计模式-桥接模式-LMLPHP

再看看部分实现代码:

public abstract class Drink
{
    public string Name { get; set; }

    public int Price { get; set; }

    public abstract string Desc { get; }

    public abstract int Cost { get; }
}

public class Naicha : Drink
{
    public Naicha()
    {
        Name = "奶茶";
        Price = 8;
    }
    public override string Desc => this.Name;
    public override int Cost => this.Price;
}

public class CoCoNaicha:Naicha
{
    public CoCoNaicha()
    {
        Name += "[CoCo]";
        Price += 2;
    }
}

public class DaCoCoNaicha : CoCoNaicha
{
    public DaCoCoNaicha()
    {
        Name += "+大杯";
        Price += 3;
    }
}

问题

初级方案和遇到的问题也跟装饰器模式几乎是一模一样的:

  • 类爆炸;
  • 大量代码重复;
  • 如果增加品牌,或者调整规格价格,代码维护困难,严重违反开闭原则。

思考

到了这里,我们不妨思考一下如下两个问题:

  1. 考虑能否使用装饰器模式实现?
  2. 这里的品牌、规格跟装饰器模式中用到的配料有什么区别?

很明显,通过装饰器模式肯定是可以达成目标的,毕竟他们看起来差不多,但是这样好不好呢?这就需要回答第二问题,品牌、规格跟配料有区别吗?

  • 首先,一杯奶茶可以同时加多种配料,但是不能同时属于多种品牌或者多种规格,例如可以有红豆布丁奶茶,却不可以有一点点COCO奶茶;
  • 其次,奶茶配料可以加多份,例如多冰,多糖,双份布丁等,但是奶茶只能属于一种品牌、一种规格;
  • 最后,一杯奶茶可以不加配料,但是一定会属于某一个品牌或者规格。

改进

发现了吗?区别还是很大的。由于有了这些区别,实现方式自然也应该是有所不同的。其实,这里没有那么复杂,不需要一步就跳到装饰者模式,我们依旧用最老套的改进方式---继承转组合---就可以了。
改进后的类图如下所示:
设计模式-桥接模式-LMLPHP

再看看部分实现代码,这里直接将品牌和规格聚合到了饮品基类中,其中,品牌和规格面向抽象编程,我想就不用细说了:

public abstract class Drink
{
    private readonly BrandBase _brand;

    private readonly SkuBase _sku;
    public Drink(BrandBase brand, SkuBase sku)
    {
        this._brand = brand;
        this._sku = sku;
    }

    public string Name { get; set; }

    public int Price { get; set; }


    public string Desc
    {
        get
        {
            return this.Name + this._brand.BrandName + this._sku.SkuType;
        }
    }

    public int Cost
    {
        get
        {
            return this.Price + this._brand.Price + this._sku.Price;
        }
    }
}

public class Naicha : Drink
{
    public Naicha(BrandBase brand, SkuBase sku):base(brand,sku)
    {
        Name = "奶茶";
        Price = 8;
    }
}

品牌和规格的部分代码如下:

public abstract class SkuBase
{
    public abstract string SkuType { get; }

    public abstract int Price { get; }
}

public class Dabei : SkuBase
{
    public override string SkuType => "大杯";

    public override int Price => 3;
}

public abstract class BrandBase
{
    public abstract string BrandName { get; }

    public abstract int Price { get; }
}

public class CoCo : BrandBase
{
    public override string BrandName => "[CoCo]";

    public override int Price => 2;
}

比想象中的要简单,一步就到位了。我们再来看看,如果要扩展增加瑞辛咖啡呢?

public class Kafei : Drink
{
    public Kafei(BrandBase brand, SkuBase sku) : base(brand, sku)
    {
        Name = "咖啡";
        Price = 12;
    }
}

public class Ruixin : BrandBase
{
    public override string BrandName => "[瑞辛]";

    public override int Price => 2;
}

没错,就是这么简单,直接在饮品和品牌两个维度上增加咖啡和瑞辛就可以了,原有的代码不用做任何修改,一步改造直接就满足了开闭原则。没错,这就是桥接模式。

简化UML

将上述案例中的类图简化并抽象就可以得到桥接模式的UML类图了:
设计模式-桥接模式-LMLPHP

  • Abstraction:抽象化角色,并保存一个对实现化对象的引用。
  • RefinedAbstraction:修正抽象化角色,改变和修正父类对抽象化的定义。
  • Implementor:实现化角色,这个角色给出实现化角色的接口,但不给出具体的实现。
  • ConcreteImplementor:具体实现化角色,这个角色给出实现化角色接口的具体实现。

定义

桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。

有的人可能会说,既然是桥接模式,那么桥在哪里呢?其实,Abstraction就是桥,从奶茶店的例子来看,Drink就是桥,桥接的是DrinkBrandBaseSkuBase三个维度,你没看错,这里的Drink起到了两个作用,一个作用是饮品基类,另一个作用就是桥。其实,桥的目的就是为了将多个维度联系起来,因此,也可以单纯的通过多继承实现,或者单纯的通过组合实现。但是,高级语言一般都不支持多继承,并且,我们也知道,继承并不是一个好的设计方式,因此不选择多继承;另外,如果纯粹的通过组合实现,就需要额外定义一个无意义的桥类,这个类同时将DrinkBrandBaseSkuBase组合进来,虽然可行,但这明显是不够优雅的,因此,桥接模式依然采用的是继承+组合的模式。

优缺点

优点

  • 分离抽象部分与它的实现部分
  • 相对于继承有更少的子类,使用更灵活,可在多个维度上自由扩展

缺点

  • 增加系统的理解与设计难度;
  • 独立变化的维度的识别比较困难;
  • 客户端使用成本较高,需要对各个维度进行构造。

跟装饰器模式的区别

  • 装饰器模式是为了动态地给一个对象增加功能,而桥接模式是为了让类在多个维度上自由扩展;
  • 装饰器模式的装饰者和被装饰者需要继承自同一父类,而桥接模式通常不需要;
  • 装饰器模式通常可以嵌套使用,而桥接模式不能。

总结

有看过前一篇装饰器模式相关介绍的朋友可能会有所疑惑,我们这里同样改动了Drink基类啊,前面不是说不应该修改原有的类吗?其实原因很简单,因为目的不一样了,装饰器模式是为了动态附加职能,而桥接模式是为了可以在多个维度上自由扩展。说到底,桥接模式适用在设计阶段,也就是在设计Drink类的时候,目的是为了把Drink设计的更好用,而不是动态的在原有的Drink类上额外增加内容,正因如此,在设计之初,维度的识别也就显得至关重要了。

源码链接

09-04 02:48