假如需要设计这样一个业务场景:某公司管理系统登录账户有销售员工、研发员工用户,现系统规划考勤系统、薪资系统需要针对三种员工做不业务逻辑。该系统有两个维度方面的变化,一个是员工的变化,即可以是销售员工,可以是研发员工;另一个是业务维度的变化,即考勤系统和薪资系统的变化,针对这一系统设计,我尝试从单继承结构到使用桥接模式来尝试感受桥接模式带来的好处。
1.单继承结构设计
对于以员工为抽象父类进行设计,类图如下:
简单的单继承结构,虽然可以满足初期的业务需求,但是假如这时候,需要再添加一类员工:财务员工,这时候就需要在与销售和研发员工同层类中增加一个新的员工类继承抽象员工类,并且在此基础上,还需要再增加两个子类:财务员工考勤、财务员工绩效,增加一类员工,我一共增加了三个新的类。由此可见,单继承结构,会随着业务需求的增长,类的数量剧烈增长,容易引起类爆炸。此外,如果业务类增加,比如增加一个薪资管理系统,那么就需要再每类型员工下再增加一个子类:xx员工薪资管理。再有,如果考勤系统方法发生了变化,那么,就需要去修改每个xx员工考勤系统下的相关方法,这样显然违背了开放-封闭原则,使得维护难度大大提升。
造成单继承结构的该管理系统维护困难的原因,在于业务逻辑和员工之间的紧耦合,单一的分支结构中,既处理业务逻辑,也体现员工分类,显然,这也违背了单一职能原则,造成紧耦合的根本原因是混合不同的逻辑于一个继承结构当中,使得该单继承层次结构的职能不单一。因此,要解决维护困难的问题,就需要对两个维度的逻辑松耦合,而桥接模式,就可以达成这一目的。
2.桥接模式
桥接模式(Bridge),将抽象部分与它的实现部分分离,使它们都可以独立变化。 ----《大话设计模式》
当时看到这里的时候,我是不太理解的,什么叫做“抽象部分与它的实现部分分离”, 不禁发出疑问:“抽象类本来就是和其子类分离的呀?”, 但是书中说,这里的抽象部分,并不是抽象类,而实现部分,也不是其子类。先看下桥接模式的结构类图:
这里的Abstraction及其子类RefinedAbstraction是“抽象部分”, Implementor及其实现类ConcreteImplementorA,ConcreteImplementorB是“实现部分”。个人理解的通俗含义是:
实现一个系统,该系统可能有两个维度(员工类型和具体业务类型如考勤,绩效)或者更多,那么,就把这两个维度分离出来,让它们分别自己去实现各自的逻辑从而降低他们之间的耦合度,于是,将桥接模式应用于上面的例子,
我首先画出了这样的类图:
这样,员和管理系统就分离开来了,而两者之间还缺乏一座“桥”将它们联系起来,这座桥,代表了一种聚合方式,提到聚合,这里就需要了解一个 设计中的重要原则:合成/聚合复用原则。
合成/聚合复用原则(CARP), 尽量使用合成/聚合, 尽量不要使用类继承。 ----《大话设计模式》
所谓合成,代表强的“拥有”关系,比如说人和人的四肢,体现了整体和部分的关系,整体和部分拥有同样的生命周期;
所谓聚合,代表弱的“拥有”关系,比如说我和我的电脑,我拥有电脑,但是电脑不是我的组成部分,我可以使用电脑做一些事情,我和我的电脑不需要有同样的生命周期。
按照聚合的思路,我在员工部分和管理部分筑起一座“桥梁”,使两者联系起来。
下面,我按照设计的结构图进行代码的编写。
3.代码实现
首先定义了“员工”的抽象类Stuff,一般而言,桥接模式抽象部分的父类,定义为抽象类,可以统一定义一些派生子类的共有逻辑,我这里定义了一个抽象类Stuff, 表示所有员工的抽象,拥有一个关系系统类,以执行相应业务,
并且有setSys方法,以设置sys.
定义了一个抽象方法:doBuiness(),员工通过该方法,结合ManagementSystem, 来执行具体的业务。
abstract class Stuff { protected ManagementSystem sys; public void setSys(ManagementSystem sys){ this.sys = sys; } protected abstract void doBusiness(); }
接着定义管理系统的接口,也可以用抽象类,我这里使用了接口。
定义了一个doBusinessImpl(), 使不同管理系统执行相应的逻辑, 这里有个参数是Class类型的,是为了后面调用该方法的员工类型,在实际业务中,也可以使用反射机制来判断员工类型从而执行不同业务。
interface ManagementSystem { void doBusinessImpl(Class<? extends Stuff> cl); }
然后写具体的“销售员工”、“研发员工”类
class SaleStuff extends Stuff { private String name; public SaleStuff(String n) { name = n; } @Override protected void doBusiness() { System.out.print(name + " "); sys.doBusinessImpl(this.getClass()); } } class ResearchStuff extends Stuff { private String name; public ResearchStuff(String n) { name = n; } @Override protected void doBusiness() { System.out.print(name + " "); sys.doBusinessImpl(this.getClass()); } }
写管理系统的实现类
class AttendanceSystem implements ManagementSystem { @Override public void doBusinessImpl(Class<? extends Stuff> cl) { System.out.println(cl.getSimpleName() + " 执行考勤。"); } } class PerformaceSystem implements ManagementSystem{ @Override public void doBusinessImpl(Class<? extends Stuff> cl) { System.out.println(cl.getSimpleName() + " 绩效考核。"); } }
最后,是客户端调用演示
public class BridgeDemo { public static void main(String[] args) { //考勤系统 ManagementSystem m1 = new AttendanceSystem(); //绩效系统 ManagementSystem m2 = new PerformaceSystem(); //销售人员小明 Stuff xiaoming = new SaleStuff("小明"); //小明进行考勤 xiaoming.setSys(m1); xiaoming.doBusiness(); //研发人员小红 Stuff xiaohong = new ResearchStuff("小红"); //小红进行绩效考核 xiaohong.setSys(m2); xiaohong.doBusiness(); } }
输出结果:
小明 SaleStuff 执行考勤。
小红 ResearchStuff 绩效考核。
以上代码采用桥接模式,实现了一个系统中两个维度的解耦, 可能例子不是很恰当,但是整体的思想应该是大致地体现了出来。
5.总结
通过一个场景:员工和管理系统, 从代码设计的最初思路(单继承)到后面使用桥接模式,实现了不同维度业务之间的分离,降低耦合度。我认为桥接模式是体现代码设计中开闭原则、单一职能原则、依赖反转原则、合成/聚合复用原则等的最佳应用之一,体现得恰如其分。通过学习桥接模式,我想到,在我们做的web应用开发中,经常使用和熟知的java三层架构(表现层、服务层、持久层),不就是使用了桥接模式的思想吗?我们通常在Controller中定义Service,使Controller拥有这个Service,这是聚合,在Controller中的方法中,借助Service提供的更细致的方法,解决问题,这不就是Conroller和Service相互分离通过一座聚合之“桥”来产生联系的思路么。只不过,实际项目中,我们通常使用基于反射机制的注解,并借助xml配置文件,实现动态注入,而不是想我的例子那样,直接调用setSys()方法给service赋值。
通过学习桥接模式,以下几点应该成为我日后编码的经验:
- 面向对象思想中,继承很重要,但是,不要滥用继承,首先尽量尝试使用聚合/合成,以达到松耦合目的;
- 如果设计一个系统,它有两个维度上的变化,如同笛卡尔坐标系的x轴和y轴,那么,就要考虑使用桥接模式了;
- 单继承结构中,如果继承层次过多,不仅业务之间的耦合度提高,还可能导致类爆炸,所以应该尽量避免多层继承;
- 虽然在本例中没有体现,但是通过三层架构注入的思考,意识到反射可以进一步降低代码耦合度,简化代码,把一些“静态”的东西和“动态”的东西分离;
- 多思考,勤练习。