桥梁模式

Published on 2016 - 09 - 11

应用示例

我们每个人都有理想,但不要只是空想,理想是要靠今天的拼搏来实现的。今天咱们就来谈谈自己的理想,如希望成为一个富翁,身价过亿,有两家大公司,一家是房地产公司,另一家是服装制造公司。这两家公司都很赚钱,天天帮你累积财富。其实你并不关心公司的类型,你关心的是它们是不是在赚钱,赚了多少,这才是你关注的。商人嘛,唯利是图是其本性,偷税漏税是方法,欺上瞒下、压榨员工血汗是常用的手段,先用类图表示一下这两个公司,如图1所示。

类图很简单,声明了一个Corp抽象类,定义一个公司的抽象模型,公司首要是赚钱的,做义务或善举那也是有背后利益支撑的,还是赞成这句话“天下熙熙,皆为利来;天下攘攘,皆为利往”。我们先看Corp类的源代码,如代码清单1所示。

public abstract class Corp {
     /*
      * 如果是公司就应该有生产,不管是软件公司还是制造业公司
      * 每家公司生产的东西都不一样,所以由实现类来完成
      */
     protected abstract void  produce();
     /*
      * 有产品了,那肯定要销售啊,不销售公司怎么生存
      */
     protected abstract void sell(); 
     //公司是干什么的?赚钱的
     public void makeMoney(){     
             //每个公司都是一样,先生产
             this.produce();
             //然后销售
             this.sell();
     }
}

怎么这是模板方法模式啊?是的,这是个引子,请继续往下看。合适的方法存在合适的类中,这个基本上是每本Java基础书上都会讲的,但是到实际的项目中应用的时候就不是这么回事儿了。我们继续看两个实现类是如何实现的,先看HouseCorp类,这是最赚钱的公司,如代码清单2所示。

public class HouseCorp extends Corp {
     //房地产公司盖房子
     protected void produce() {
             System.out.println("房地产公司盖房子...");
     }
     //房地产公司卖房子,自己住那可不赚钱
     protected void sell() {
             System.out.println("房地产公司出售房子...");
     }
     //房地产公司很High了,赚钱,计算利润
     public void makeMoney(){
             super.makeMoney();
             System.out.println("房地产公司赚大钱了...");
     }
}

房地产公司按照正规翻译来说应该是realty corp,这个是比较准确的翻译,但是我问你把房地产公司翻译成英文,你的第一反应是什么?house corp!这是中式英语。我们再来看服装公司,虽然不景气,但好歹也是赚钱的,如代码清单3所示。

public class ClothesCorp extends Corp {
     //服装公司生产的就是衣服了
     protected void produce() {
             System.out.println("服装公司生产衣服...");
     }
     //服装公司卖服装,可只卖服装,不卖穿衣服的模特
     protected void sell() {
             System.out.println("服装公司出售衣服...");
     }
     //服装公司不景气,但怎么说也是赚钱行业
     public void makeMoney(){
             super.makeMoney();
             System.out.println("服装公司赚小钱...");
     }
}

两个公司都有了,那肯定有人会关心两个公司的运营情况。你也要知道它是生产什么的,以及赚多少钱吧。通过场景类来进行模拟,如代码清单4所示。

public class Client {
     public static void main(String[] args) {
             System.out.println("-------房地产公司是这样运行的-------");
             //先找到我的公司
             HouseCorp houseCorp =new HouseCorp();
             //看我怎么挣钱
             houseCorp.makeMoney();
             System.out.println("\n");
             System.out.println("-------服装公司是这样运行的-------");
             ClothesCorp clothesCorp = new ClothesCorp();
             clothesCorp.makeMoney();
     }
}

这段代码很简单,运行结果如下所示:

-------房地产公司是这样运行的-------
房地产公司盖房子...
房地产公司出售房子...
房地产公司赚大钱了...
-------服装公司是这样运行的-------
服装公司生产衣服...
服装公司出售衣服...
服装公司赚小钱...

上述代码完全可以描述我现在的公司,但是你要知道万物都是运动的,你要用运动的眼光看问题,公司才会发展……终于有一天你觉得赚钱速度太慢,于是你上下疏通,左右打关系,终于开辟了一条赚钱的“康庄大道”:生产山寨产品!什么产品呢?即市场上什么牌子的东西火爆我生产什么牌子的东西,不管是打火机还是电脑,只要它火爆,我就生产,赚过了高峰期就换个产品,打一枪换一个牌子,不承担售后成本、也不担心销路问题,我只要正品的十分之一的价格,你买不买?哈哈,赚钱啊!

企业的方向定下来了,通过调查,苹果公司的iPod系列产品比较火爆,那咱就生产这个,把服装厂改成iPod生产厂,看类图的变化,如图2所示。

好,我的企业改头换面了,开始生产iPod产品了,看我IPodCorp类的实现,如代码清单5所示。

public class IPodCorp extends Corp {
     //我开始生产iPod了
     protected void produce() {
             System.out.println("我生产iPod...");
     }
     //山寨的iPod很畅销,便宜嘛
     protected void sell() {
             System.out.println("iPod畅销...");
     }
     //狂赚钱
     public void makeMoney(){
             super.makeMoney();
             System.out.println("我赚钱呀...");
     }
}

服装工厂改成了电子工厂,你这个董事长还是要去看看到底生产什么的,场景类如代码清单6所示。

public class Client {
     public static void main(String[] args) {
             System.out.println("-------房地产公司是按这样运行的-------");
             //先找到我的公司
             HouseCorp houseCorp =new HouseCorp();
             //看我怎么挣钱
             houseCorp.makeMoney();
             System.out.println("\n");     
             System.out.println("-------山寨公司是按这样运行的-------");
             IPodCorp iPodCorp = new IPodCorp();
             iPodCorp.makeMoney();
     }
}

确实,只用修改了黑色字体这几句话,服装厂就开始变成山寨iPod生产车间,然后你就看着你的财富在积累。山寨的东西不需要特别的销售渠道(正品到哪里我就到哪里),不需要维修成本(大不了给你换个,你还想怎么样,过了高峰期我就改头换面了,你找谁维修去?投诉?投诉谁呢?),不承担广告成本(正品在打广告,我还需要吗?需要吗?),但是也有犯愁的时候,这是一个山寨工厂,要及时地生产出市场上流行的产品,转型要快,要灵活,今天从生产iPod转为生产MP4,明天再转为生产上网本,这都需要灵活的变化,不要限制得太死!那问题来了,每次我的厂房,我的工人,我的设备都在,不可能每次我换个山寨产品厂子就彻底不要了。这不行,成本忒高了点,那怎么办?

Thinking,Thinking...I got an idea!(跳跳虎语),既然产品和工厂绑得太死,那我就给你来松松,改变设计,如图3所示。

公司和产品之间建立关联关系,可以彻底解决以后山寨公司生产产品的问题,工厂想换产品?太容易了!看程序说话,先看Product抽象类,如代码清单7所示。

public abstract class Product {
     //甭管是什么产品它总要能被生产出来
     public abstract void beProducted();
     //生产出来的东西,一定要销售出去,否则亏本
     public abstract void beSelled();
}

简单!忒简单了!House产品类如代码清单8所示。

public class House extends Product {
     //豆腐渣就豆腐渣呗,好歹也是房子
     public void beProducted() {
             System.out.println("生产出的房子是这样的...");
     }
     //虽然是豆腐渣,也是能够销售出去的
     public void beSelled() {
            System.out.println("生产出的房子卖出去了...");
     }
}

既然是产品类,那肯定有两种行为要存在:被生产和被销售,否则就不能称为产品了。我们再来看iPod产品类,如代码清单9所示。

public class IPod extends Product {
     public void beProducted() {
             System.out.println("生产出的iPod是这样的...");
     }
     public void beSelled() {
             System.out.println("生产出的iPod卖出去了...");
     }
}

产品是由公司生产出来的,我们来看公司Corp抽象类,如代码清单10所示。

public abstract class Corp {
     //定义一个抽象的产品对象,不知道具体是什么产品
     private Product product;
     //构造函数,由子类定义传递具体的产品进来
     public Corp(Product product){
             this.product = product;
     }
     //公司是干什么的?赚钱的!
     public void makeMoney(){     
             //每家公司都是一样,先生产
             this.product.beProducted();
             //然后销售
             this.product.beSelled();
     }
}

这里多了个有参构造,其目的是要继承的子类都必选重写自己的有参构造函数,把产品类传递进来,再看子类HouseCorp的实现,如代码清单11所示。

public class HouseCorp extends Corp {
     //定义传递一个House产品进来
     public HouseCorp(House house){
             super(house);
     }
     //房地产公司很High了,赚钱,计算利润
     public void makeMoney(){
             super.makeMoney();
             System.out.println("房地产公司赚大钱了...");
     }
}

理解上没有多少难度,不多说,继续看山寨公司的实现,如代码清单12所示。

public class ShanZhaiCorp extends Corp {
      //产什么产品,不知道,等被调用的才知道
     public ShanZhaiCorp(Product product){
             super(product);
     }
     //狂赚钱
     public void makeMoney(){
             super.makeMoney();
             System.out.println("我赚钱呀...");
     }
}

HouseCorp类和ShanZhaiCorp类的区别是在有参构造的参数类型上,HouseCorp类比较明确,我就是只要House类,所以直接定义传递进来的必须是House类, 一个类尽可能少地承担职责,那方法也一样,既然HouseCorp类已经非常明确地只生产House产品,那为什么不定义成House类型呢?ShanZhaiCorp就不同了,它确定不了生产什么类型。

好了,两大对应的阵营都已经产生了。我们再看Client程序,如代码清单13所示。

public class Client {
     public static void main(String[] args) {
             House house = new House();
             System.out.println("-------房地产公司是这样运行的-------");
             //先找到房地产公司
            HouseCorp houseCorp =new HouseCorp(house);
             //看我怎么挣钱
             houseCorp.makeMoney();
             System.out.println("\n");     
             //山寨公司生产的产品很多,不过我只要指定产品就成了
             System.out.println("-------山寨公司是这样运行的-------");
             ShanZhaiCorp shanZhaiCorp = new ShanZhaiCorp(new IPod());
             shanZhaiCorp.makeMoney();     
     }
}

运行结果如下所示:

-------房地产公司是这样运行的-------
生产出的房子是这样的...
生产出的房子卖出去了...
房地产公司赚大钱了...
-------山寨公司是这样运行的-------
生产出的iPod是这个样子的...
生产出的iPod卖出去了...
我赚钱呀...

突然有一天,老板良心发现了,不准备生产这种“三无”产品了,那我们程序该怎么修改呢?如果仍重操旧业,生产衣服,那该如何处理呢?很容易处理,增加一个产品类,然后稍稍修改一下场景就可以了,我们来看衣服产品类,如代码清单14所示。

public class Clothes extends Product {
     public void beProducted() {
             System.out.println("生产出的衣服是这样的...");
     }
     public void beSelled() {
             System.out.println("生产出的衣服卖出去了...");
     }
}

然后再稍稍修改一下场景类,如代码清单15所示。

public class Client {
     public static void main(String[] args) {
             House house = new House();
             System.out.println("-------房地产公司是这样运行的-------");
             //先找到房地产公司
             HouseCorp houseCorp =new HouseCorp(house);
             //看我怎么挣钱
             houseCorp.makeMoney();
             System.out.println("\n");
             //山寨公司生产的产品很多,不过我只要指定产品就成了
             System.out.println("-------山寨公司是这样运行的-------");
             ShanZhaiCorp shanZhaiCorp = new ShanZhaiCorp(new Clothes());
             shanZhaiCorp.makeMoney();
     }
}

修改后的运行结果如下所示:

-------房地产公司是这样运行的-------
生产出的房子是这样的...
生产出的房子卖出去了...
房地产公司赚大钱了...
-------山寨公司是这样运行的-------
生产出的衣服是这样的...
生产出的衣服卖出去了...
我赚钱呀...

就修改了这一条语句就完成了生产产品的转换。那我们深入思考一下,既然万物都是运动的,我现在只有房地产公司和山寨公司,那以后我会不会增加一些其他的公司呢?或者房地产公司会不会对业务进行细化,如分为公寓房公司、别墅公司,以及商业房公司等呢?那我告诉你,会的!绝对会的!但是你发觉没有,这种变化对我们上面的类图来说不会做大的修改,充其量只是扩展:

  • 增加公司,要么继承Corp类,要么继承HouseCorp或ShanZhaiCorp,不用再修改原有的类了。
  • 增加产品,继承Product类,或者继承House类,你要把房子分为公寓房、别墅、商业用房等。

你唯一要修改的就是Client类。类都增加了,高层模块也需要修改,也就是说Corp类和Product类都可以自由地扩展,而不会对整个应用产生太大的变更,这就是桥梁模式。

桥梁模式的定义

桥梁模式(Bridge Pattern)也叫做桥接模式,是一个比较简单的模式,其定义如下:Decouple an abstraction from its implementation so that the two can vary independently.(将抽象和实现解耦,使得两者可以独立地变化。)

桥梁模式的重点是在“解耦”上,如何让它们两者解耦是我们要了解的重点,我们先来看桥梁模式的通用类,如图4所示。

我们先来看桥梁模式中的4个角色。

  • Abstraction——抽象化角色

它的主要职责是定义出该角色的行为,同时保存一个对实现化角色的引用,该角色一般是抽象类。

  • Implementor——实现化角色

它是接口或者抽象类,定义角色必需的行为和属性。

  • RefinedAbstraction——修正抽象化角色

它引用实现化角色对抽象化角色进行修正。

  • ConcreteImplementor——具体实现化角色

它实现接口或抽象类定义的方法和属性。

桥梁模式中的几个名词比较拗口,大家只要记住一句话就成:抽象角色引用实现角色,或者说抽象角色的部分实现是由实现角色完成的。我们来看其通用源码,先看实现化角色,如代码清单16所示。

public interface Implementor {
     //基本方法
     public void doSomething();
     public void doAnything();
}

它没有任何特殊的地方,就是一个一般的接口,定义要实现的方法。其实现类如代码清单17所示。

public class ConcreteImplementor1 implements Implementor{
     public void doSomething(){
             //业务逻辑处理
     }
     public void doAnything(){
             //业务逻辑处理
     }
}
public class ConcreteImplementor2 implements Implementor{
     public void doSomething(){
             //业务逻辑处理
     }
     public void doAnything(){
             //业务逻辑处理
     }
}

上面定义了两个具体实现化角色——代表两个不同的业务逻辑。我们再来看抽象化角色,如代码清单18所示。

public abstract class Abstraction {
     //定义对实现化角色的引用
     private Implementor imp;
     //约束子类必须实现该构造函数
     public Abstraction(Implementor _imp){
             this.imp = _imp;
     }
     //自身的行为和属性
     public void request(){
             this.imp.doSomething();
     }
     //获得实现化角色
     public Implementor getImp(){
             return imp;
     }
}

各位可能要问,为什么要增加一个构造函数?答案是为了提醒子类,你必须做这项工作,指定实现者,特别是已经明确了实现者,则尽量清晰明确地定义出来。我们来看具体的抽象化角色,如代码清单19所示。

public class RefinedAbstraction extends Abstraction {
     //覆写构造函数
     public RefinedAbstraction(Implementor _imp){
             super(_imp);
     }
     //修正父类的行为
     @Override
     public void request(){
             /*
              * 业务处理...
              */
             super.request();
             super.getImp().doAnything();
     }
}

想想看,如果我们的实现化角色有很多的子接口,然后是一堆的子实现。如果在构造函数中不传递一个尽量明确的实现者,代码就很不清晰。我们来看场景类如何模拟,如代码清单20所示。

public class Client {
     public static void main(String[] args) {
             //定义一个实现化角色
             Implementor imp = new ConcreteImplementor1();
             //定义一个抽象化角色
             Abstraction abs = new RefinedAbstraction(imp);
             //执行行文
            abs.request();
     }
}

桥梁模式是一个非常简单的模式,它只是使用了类间的聚合关系、继承、覆写等常用功能,但是它却提供了一个非常清晰、稳定的架构。

桥梁模式的应用

桥梁模式的优点

  • 抽象和实现分离

这也是桥梁模式的主要特点,它完全是为了解决继承的缺点而提出的设计模式。在该模式下,实现可以不受抽象的约束,不用再绑定在一个固定的抽象层次上。

  • 优秀的扩充能力

看看我们的例子,想增加实现?没问题!想增加抽象,也没有问题!只要对外暴露的接口层允许这样的变化,我们已经把变化的可能性减到最小。

  • 实现细节对客户透明

客户不用关心细节的实现,它已经由抽象层通过聚合关系完成了封装。

桥梁模式的使用场景

  • 不希望或不适用使用继承的场景

例如继承层次过渡、无法更细化设计颗粒等场景,需要考虑使用桥梁模式。

  • 接口或抽象类不稳定的场景

明知道接口不稳定还想通过实现或继承来实现业务需求,那是得不偿失的,也是比较失败的做法。

  • 重用性要求较高的场景

设计的颗粒度越细,则被重用的可能性就越大,而采用继承则受父类的限制,不可能出现太细的颗粒度。

桥梁模式的注意事项

桥梁模式是非常简单的,使用该模式时主要考虑如何拆分抽象和实现,并不是一涉及继承就要考虑使用该模式,那还要继承干什么呢?桥梁模式的意图还是对变化的封装,尽量把可能变化的因素封装到最细、最小的逻辑单元中,避免风险扩散。因此读者在进行系统设计时,发现类的继承有N层时,可以考虑使用桥梁模式。

最佳实践

大家对类的继承有什么看法吗?继承的优点有很多,可以把公共的方法或属性抽取,父类封装共性,子类实现特性,这是继承的基本功能。缺点有没有?有!即强侵入,父类有一个方法,子类也必须有这个方法。这是不可选择的,会带来扩展性的问题。我举个简单的例子来说明:Father类有一个方法A,Son继承了这个方法,然后GrandSon也继承了这个方法,问题是突然有一天Son要重写父类的这个方法,他敢做吗?绝对不敢!GrandSon要用从Father继承过来的方法A,如果你修改了,那就要修改Son和GrandSon之间的关系,那这个风险就太大了!

这里讲的这个桥梁模式就是这一问题的解决方法,桥梁模式描述了类间弱关联关系,还说上面的那个例子,Father类完全可以把可能会变化的方法放出去,Son子类要拥有这个方法很简单,桥梁搭过去,获得这个方法,GrandSon也一样,即使你Son子类不想使用这个方法也没关系,对GrandSon不产生影响,它不是从Son中继承来的方法!

不能说继承不好,它非常好,但是有缺点,我们可以扬长避短,对于比较明确不发生变化的,则通过继承来完成;若不能确定是否会发生变化的,那就认为是会发生变化,则通过桥梁模式来解决,这才是一个完美的世界。

参考文档