结构型模式——桥接(Bridge)
问题背景
当对象有两个可能的扩展维度时,应该考虑使用桥接。现在我们对动物进行建模,将它们从两个维度分为陆生生物和海洋生物、植食动物和肉食动物。根据语义,这四种动物和动物这个抽象概念之间均为is-a的关系,因此我们直接提取一个动物接口IAnimal,并让四个实现类都直接实现IAnimal。这种设计的类结构是这样的:
这种设计在稳定的系统中是没问题的,但如果系统需要迭代新版本,就要考虑有没有扩展的可能。比如:在某次更新中,我们要将肉食动物和腐食动物区分开。
害!不就加两个类的事吗,简单的鸭皮。你很快完成了任务,然后愉悦地打开了LOL。
过了几天,又来新的需求,要求将寄生生物从陆生和海洋动物中区分出来。
害!不就加三个类的事吗!
又过了几天,来了个吃土的……又过了几天,来了个外星人……现在的系统已经不成样子了,因为两个维度的交替变化使得类的数量成平方式增长,完全Hold不住了……
解决方案
为什么会出现平方式增长?以前从来没遇到过这种情况。我们透过现象看其本质,原来是因为在这个系统中,动物的特性会在两个维度(栖息地、食性)独立变化。由于这两种变化的正交性,它们放在一起就会是乘的关系,因此是平方式增长。但现实并不仅仅是两个维度,很有可能是三个、四个……
既然继承是如此的辣鸡,那干脆就不要用啦,我们不是有一句秘诀叫“组合优于继承”吗?通常情况下,组合和继承可以实现的功能是相同的,但继承是静态的,组合是动态的,因此组合会比继承更加灵活。
我们将栖息地或食性中的至少一个抽象成属性(书中的标准做法是抽象一个,但我个人更喜欢全部抽象成属性),使用组合的方式实现以上需求。按照面向接口编程的惯例,我们抽象出两个接口IHabitat和IFoodHabit,分别表示栖息地和食性,并让Ocean和Land、Plant和Meat分别实现两个接口。改进后的程序结构是这样的:
类图中的动物类像一座桥梁,将栖息地和食性两个独立的类联系了起来,因此这个模式被称为桥接模式。
使用桥接后,扩展系统带来的成本被降低到了线性级,我们可以毫无顾虑地扩展各个维度而不用担心类爆炸了。
效果
- 提高了系统的可扩展性,降低了扩展所需的成本。
- 可以在运行时对每个独立维度进行动态绑定,灵活性更高。
- 对用户隐藏了实现细节,增强了系统的可用性。
缺陷
“组合优于继承”这个道理并非绝对,而是针对复杂系统而言的。在类比较少或系统相对稳定时,桥接反而会增加系统的复杂度,不推荐使用。我们不应该将某个说法当成万金油,奉为圣经,而应该认识到其适用范围,理性接受。在某些情况下,继承确实有着组合无可替代的优势。
相关模式
- 抽象工厂:使用了桥接的对象一般比较复杂,可以考虑使用抽象工厂来创建。
实现
using System;
namespace Bridge
{
class Client
{
public interface IHabitat { }
public interface IFoodHabit { }
public class Animal
{
public IHabitat Habitat { get; set; }
public IFoodHabit FoodHabit { get; set; }
public Animal(IHabitat habitat, IFoodHabit foodHabit)
{
Habitat = habitat;
FoodHabit = foodHabit;
}
public void Show()
{
Console.WriteLine($"我是{Habitat.ToString()}{FoodHabit.ToString()}动物");
}
}
public class Ocean : IHabitat
{
public override string ToString()
{
return "海洋";
}
}
public class Land : IHabitat
{
public override string ToString()
{
return "陆生";
}
}
public class Plant : IFoodHabit
{
public override string ToString()
{
return "植食性";
}
}
public class Meat : IFoodHabit
{
public override string ToString()
{
return "肉食性";
}
}
static void Main(string[] args)
{
Console.WriteLine("创建海洋植食性动物...");
var ocean_plant = new Animal(new Ocean(), new Plant());
ocean_plant.Show();
Console.WriteLine("创建海洋肉食性动物...");
var ocean_meat = new Animal(new Ocean(), new Meat());
ocean_meat.Show();
Console.WriteLine("创建陆生植食性动物...");
var land_plant = new Animal(new Land(), new Plant());
land_plant.Show();
Console.WriteLine("创建陆生肉食性动物...");
var land_meat = new Animal(new Land(), new Meat());
land_meat.Show();
}
}
}