桥梁模式(Bridge)-组合比继承优先使用
桥模式对于理解开闭原则与组合聚合复用原则很有帮助,同时也可以加深对于继承的认识.这也是一个听起来奇妙,用起来好像似曾相识,或者说早就不经意间就使用过的模式.
用意
将抽象与实现解耦,使二者可以独立变化.
毫无疑问,用意概括的非常简练,初次接触这个模式的人很难参悟到其中的奥妙.软件的模式要在一个时间线上去动态的观看演变,才可以体现出模式的意义.否则只是静太的代码,是无法提现模式带来的好处的.
其实桥梁模式主要解决的问题是在过度化继承结构中使用耦合性更低的组合来替代,在新学模式不久的同学眼里,总是不由自主的到处使用继承,其实并不恰当,在组合聚合复用原则中强调组合是优先于继承的.除非强列的IS A的关系,否则不要使用继承.
我们的例子
假设有这样一个场景,我们要开发一款红警那样的游戏,我们目前只考虑最基本的一个功能,我们要绘制其中的坦克.坦克可能有不同的型号,比如天启坦克和幻影坦克.同时,需要在不同平台实现这个游戏.这里的举例当然只是为了说明我们的桥模式,可能有些程序员会局限在不同的细节中,大可不必考虑太多,只考虑概念既可,比如不同平台实现技术不同,而我们在这里却以一种语言在描述.
最早期的代码
我们需要在一个时间线上动态的观察软件的演化才能看到有会什么问题,而模式解决了什么问题,如果直接扔出模式的代码与UML图静态观看,是没有什么实际的意义的,代码很容易看懂,也不并代表学会了这个模式,这也就是好多同学为什么感觉模式代码一看就懂,很容易嘛,甚至感觉,这么简单的东西还要提出来讲吗?其实不然,我们学模式就是要看在实际的演化过程中模式带来的效果,对于扩展,复用,应对变化时的从容,这才是模式的真正意义.
如例子中所讲,一个坦克需有不同的型号,这显然是一种变化,我们设计一个Tank的接口,让不同的型号坦克来实现Tank既可,如果碰到新坦克加入时.我们只需要扩展方式加入新的子类,这是符合开闭原则的.另外因为需要在不同的平台实现,这里也同样抽象一个平台实现接口Platform,然后分由IosTankHuanyingImpl , IosTankTqianqiImpl来实现在Ios平台上的两种坦克的动作,绘制. PcTankTqianqiImpl,PcTankHuanyingImpl对应在PC机上的2种坦克的实现.
Tank为坦克的抽象,Platform为不同实现平台的抽象,让Tank扩展Platform接口.
对应的代码结构应该就是这样
package com.j2kaka.coolka.examples.pattern.bridge.bridge; /** * 不同平台实现 * * @author aladdinty * @create 2018-01-29 **/ public interface Platform { public void drawTank() ; } package com.j2kaka.coolka.examples.pattern.bridge.bridge; /** * 坦克基类 * * @author aladdinty * @create 2018-01-29 下午2:24 **/ public interface Tank extends Platform { /**开火*/ public void fire() ; /**发动*/ public void run() ; } package com.j2kaka.coolka.examples.pattern.bridge.bridge; /** * IOS系统实现幻影坦克的绘制,实且实现坦克的具体功能 * * @author aladdinty * @create 2018-01-29 **/ public class IOSTankHuanYingImpl implements Tank { @Override public void drawTank () { System.out.println ("在IOS上绘制了一个幻影坦克"); } @Override public void fire () { System.out.println ("IOS上幻影坦克开火了"); } @Override public void run () { System.out.println ("IOS上幻影坦克跑起来了"); } } package com.j2kaka.coolka.examples.pattern.bridge.bridge; /** * IOS系统实现天启坦克的绘制,实且实现坦克的具体功能 * * @author aladdinty * @create 2018-01-29 **/ public class IOSTankTianqiImpl implements Tank { @Override public void drawTank () { System.out.println ("在IOS上绘制了一个天启坦克"); } @Override public void fire () { System.out.println ("IOS上天启坦克开火了"); } @Override public void run () { System.out.println ("IOS上天启坦克跑起来了"); } } package com.j2kaka.coolka.examples.pattern.bridge.bridge; /** * PC系统实现幻影坦克的绘制,实且实现坦克的具体功能 * * @author aladdinty * @create 2018-01-29 **/ public class PCTankHuanYingImpl implements Tank { @Override public void drawTank () { System.out.println ("在PC上绘制了一个幻影坦克"); } @Override public void fire () { System.out.println ("PC上幻影坦克开火了"); } @Override public void run () { System.out.println ("PC上幻影坦克跑起来了"); } } package com.j2kaka.coolka.examples.pattern.bridge.bridge; /** * IOS系统实现天启坦克的绘制,实且实现坦克的具体功能 * * @author aladdinty * @create 2018-01-29 **/ public class PCTankTianqiImpl implements Tank { @Override public void drawTank () { System.out.println ("在PC上绘制了一个天启坦克"); } @Override public void fire () { System.out.println ("PC上天启坦克开火了"); } @Override public void run () { System.out.println ("PC上天启坦克跑起来了"); } } package com.j2kaka.coolka.examples.pattern.bridge.bridge; /** * 客户端调用处 * * @author aladdinty * @create 2018-01-29 **/ public class ClientApp { public static void main(String[] args ) { //ios printTank( new IOSTankHuanYingImpl()) ; printTank( new IOSTankTianqiImpl()) ; //pc printTank( new PCTankHuanYingImpl()) ; printTank( new PCTankTianqiImpl()) ; } public static void printTank( Tank tank ) { tank.drawTank (); tank.run (); tank.fire (); } }
运行结果
好,到此为止,我们基础实现了游戏中描述的功能,如果我们的系统在以后的扩展中,不存在更多的变化,目前为止也可以算一个良好的实现,Tank对不同的坦克进行了抽象,可以应对坦克增加的变化.如果我们需要引入新的平台实现,比如在Android或者Xbox上实现,需要对应增加不同的实现类,继承自Tank,同时也需要实现不同平台上边的drawTank方法.虽然是符合开闭原则,但是违反了单一责任原则因为一个实现类既要做平台的drawTank实现,也要负现具体的动作业务,run()与fire(). 这样代码会变的重复内容很多,难以复用.
臃肿的继承
在上面的代码中,我们的产品(Tank)结构变化是两个维度的,一方面需要沿着产品不同型号方向变化,另一方面不同平台实现也是在变化的,但是我们通常会在没有做过多设计时,写成上边那种代码,也就是一个结构中去应对多个维度的变化,这样最后的结果一定是变得一团糟.同时这也是一个典型的继承滥用的例子,当我们要修改需求时,发现要到处修改改多个点,不停的子类父类之间寻找要修改的地方,经过几次修改后,这样的继承结构变得很难读懂.
桥模式的思考
在桥接模式中的意图有说明,将抽象化与实现化分离,使二者可以单独的变化.现在为止我相我们应该有一点明白了,在我们的例子中其实 Tank与它的子类们,也就是说坦克的业务代码这一部分,全部为抽象化,这里所讲的抽象化并不是指一个类封装一部分内容这样的狭义抽象,而是指整个部分的广义抽象. 而Platform则是实现化的代码部分,虽然他这部分也是Interface来定义.
我们将两个维度的变化分解开,让他们各自有自己的实现,让Tank与Platform变成组合的关系,这样Tank的子类只需要实现fire与run等与自身坦克业务相关的动作,而不需要再关心不同平台的实现方法,不需要再写一堆PCXXtank IosXXtank这种被继承关系绑架的类出来,同时也省去了N多个fire与run这样的方法的重复实现.而Platform的变化也是一样,不再需要与tank的主体变化绑定在一起.
这个模式的关键部分是抽象化与实现化部分的区分,这两部分一定是有关系的,而且就像上面例子中一样,一个坦克的业务动作是与他在不同平台上的实现是有关系的,
组合的关系就是主体要管理起部分体的生命周期,他们之间是整体与部分的关系,如果是本来就可以独立的功能,那又何必要用这样的模式,那一定是一种误用.在这里可以把桥形象化的看关是Tank与Platform之间的这条组合的线,他们本来是竖型化的继承关系,现在变在了组合,互相可以独立变化,这条组合的线就像是在两个村庄之间搭了一座桥,既可以互通,又不会因为各自的变化影响对方.
桥模式版本的实现
package com.j2kaka.coolka.examples.pattern.bridgereal.bridge; /** * 平台实现基类 * * @author aladdinty * @create 2018-01-29 **/ public interface Platform { void drawTank() ; } package com.j2kaka.coolka.examples.pattern.bridgereal.bridge; /** * 坦克基类 * * @author aladdinty * @create 2018-01-29 **/ public abstract class Tank { private Platform plat ; public Tank( Platform plat ) { this.plat = plat ; this.plat.drawTank (); } public abstract void fire() ; public abstract void run() ; } package com.j2kaka.coolka.examples.pattern.bridgereal.bridge; /** * IOS平台绘制实现 * * @author aladdinty * @create 2018-01-29 **/ public class IOSTankPlatform implements Platform { @Override public void drawTank () { System.out.println ("ISO绘制坦克"); } } package com.j2kaka.coolka.examples.pattern.bridgereal.bridge; /** * PC平台绘制实现 * * @author aladdinty * @create 2018-01-29 **/ public class PCTankPlatform implements Platform { @Override public void drawTank () { System.out.println ("PC平台绘制坦克."); } } package com.j2kaka.coolka.examples.pattern.bridgereal.bridge; /** * 幻影坦克业务实现 * * @author aladdinty * @create 2018-01-29 **/ public class TankHuanyingImpl extends Tank { public TankHuanyingImpl (Platform plat) { super (plat); } @Override public void fire () { System.out.println ("幻影开火"); } @Override public void run () { System.out.println ("幻影发动"); } } package com.j2kaka.coolka.examples.pattern.bridgereal.bridge; /** * 天启坦克业务实现 * * @author aladdinty * @create 2018-01-29 **/ public class TankTqianqiImpl extends Tank { public TankTqianqiImpl (Platform plat) { super (plat); } @Override public void fire () { System.out.println ("天启开火"); } @Override public void run () { System.out.println ("天启发动"); } } package com.j2kaka.coolka.examples.pattern.bridgereal.bridge; /** * @author aladdinty * @create 2018-01-29 **/ public class Client { public static void main(String[] args ) { Platform plat = new IOSTankPlatform() ; Tank tank = new TankTqianqiImpl (plat) ; Tank tank2 = new TankHuanyingImpl (plat) ; tank.fire (); tank.run (); tank2.run (); tank2.fire (); //pc Platform pcplat = new IOSTankPlatform() ; Tank pctank = new TankTqianqiImpl (pcplat) ; Tank pctank2 = new TankHuanyingImpl (pcplat) ; pctank.fire (); pctank.run (); pctank2.run (); pctank2.fire (); } }
运行结果
最后总结
桥接模式虽然基本讲完,但模式的应用是非常灵活,变化场景很多同时也会涉及到与其它模式组合使用的场景,希望读者在今后的学习过程中能够围绕着分析变化点的路线来进行,否则我们上边的讲解似乎全是徒劳的,直接给出这么一份实现代码,我相信没有人读不懂,但这代码真不能表示出什么意义,一定是在动态环境中的演化,这样的代码能更好的应对变化,如果大家有什么心得,欢迎与我交流.