延续上一篇装饰器模式的话题,我们继续对需求进行升级。
示例
需求
还是以奶茶店为例,但是我们不再仅仅考虑奶茶的成分了,要想奶茶卖的好,还得需要一个响亮的品牌,奶茶有很多品牌,如一点点,COCO,喜茶等,除此之外,我们还要对奶茶的规格进行区分,如大杯、中杯、小杯等,不同品牌价格不同,不同规格价格也不同(不考虑太复杂的情况,就假设每种品牌和规格都有一个价格基数,总价直接累加)。
初级方案
乍一看,跟上一篇装饰器模式中的需求没什么区别,不就是把配料换成了品牌和规格吗?我们还是先看看继承的方案:
再看看部分实现代码:
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;
}
}
问题
初级方案和遇到的问题也跟装饰器模式几乎是一模一样的:
- 类爆炸;
- 大量代码重复;
- 如果增加品牌,或者调整规格价格,代码维护困难,严重违反开闭原则。
思考
到了这里,我们不妨思考一下如下两个问题:
- 考虑能否使用装饰器模式实现?
- 这里的品牌、规格跟装饰器模式中用到的配料有什么区别?
很明显,通过装饰器模式肯定是可以达成目标的,毕竟他们看起来差不多,但是这样好不好呢?这就需要回答第二问题,品牌、规格跟配料有区别吗?
- 首先,一杯奶茶可以同时加多种配料,但是不能同时属于多种品牌或者多种规格,例如可以有红豆布丁奶茶,却不可以有一点点COCO奶茶;
- 其次,奶茶配料可以加多份,例如多冰,多糖,双份布丁等,但是奶茶只能属于一种品牌、一种规格;
- 最后,一杯奶茶可以不加配料,但是一定会属于某一个品牌或者规格。
改进
发现了吗?区别还是很大的。由于有了这些区别,实现方式自然也应该是有所不同的。其实,这里没有那么复杂,不需要一步就跳到装饰者模式,我们依旧用最老套的改进方式---继承转组合---就可以了。
改进后的类图如下所示:
再看看部分实现代码,这里直接将品牌和规格聚合到了饮品基类中,其中,品牌和规格面向抽象编程,我想就不用细说了:
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类图了:
- Abstraction:抽象化角色,并保存一个对实现化对象的引用。
- RefinedAbstraction:修正抽象化角色,改变和修正父类对抽象化的定义。
- Implementor:实现化角色,这个角色给出实现化角色的接口,但不给出具体的实现。
- ConcreteImplementor:具体实现化角色,这个角色给出实现化角色接口的具体实现。
定义
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。
有的人可能会说,既然是桥接模式,那么桥在哪里呢?其实,Abstraction就是桥,从奶茶店的例子来看,Drink
就是桥,桥接的是Drink
,BrandBase
,SkuBase
三个维度,你没看错,这里的Drink
起到了两个作用,一个作用是饮品基类,另一个作用就是桥。其实,桥的目的就是为了将多个维度联系起来,因此,也可以单纯的通过多继承实现,或者单纯的通过组合实现。但是,高级语言一般都不支持多继承,并且,我们也知道,继承并不是一个好的设计方式,因此不选择多继承;另外,如果纯粹的通过组合实现,就需要额外定义一个无意义的桥类,这个类同时将Drink
,BrandBase
,SkuBase
组合进来,虽然可行,但这明显是不够优雅的,因此,桥接模式依然采用的是继承+组合的模式。
优缺点
优点
- 分离抽象部分与它的实现部分
- 相对于继承有更少的子类,使用更灵活,可在多个维度上自由扩展
缺点
- 增加系统的理解与设计难度;
- 独立变化的维度的识别比较困难;
- 客户端使用成本较高,需要对各个维度进行构造。
跟装饰器模式的区别
- 装饰器模式是为了动态地给一个对象增加功能,而桥接模式是为了让类在多个维度上自由扩展;
- 装饰器模式的装饰者和被装饰者需要继承自同一父类,而桥接模式通常不需要;
- 装饰器模式通常可以嵌套使用,而桥接模式不能。
总结
有看过前一篇装饰器模式相关介绍的朋友可能会有所疑惑,我们这里同样改动了Drink
基类啊,前面不是说不应该修改原有的类吗?其实原因很简单,因为目的不一样了,装饰器模式是为了动态附加职能,而桥接模式是为了可以在多个维度上自由扩展。说到底,桥接模式适用在设计阶段,也就是在设计Drink
类的时候,目的是为了把Drink
设计的更好用,而不是动态的在原有的Drink
类上额外增加内容,正因如此,在设计之初,维度的识别也就显得至关重要了。