前言
在这篇文章中,我将以记述我整个重构过程的方式来得到桥接模式,也许很多人学习设计模式都有我这样的历程,看过很多模式的书籍,但还是无从下手,很多人都说模式往往是重构得来的,但是如何重构?如何去思考?
在这个设计模式初学者系列里我将改变以前的风格,今后每一篇都以真实的项目重构例子来描述重构到模式,所有的描述都来源于现实,如果没有现实的案例那我宁愿不写。希望以这种方式记录能够给一些像我一样曾经很困惑的朋友帮助。
背景
PM:小Y,你看,这个Dundas的Chart(注1)控件效果很好,功能也很强大,但是使用的时候太麻烦了,你能不能将其封装一下,这样其他人使用的时候也简单多了
我:嗯,这个我看看吧,应该可以。
注1
Dundas Chart控件是一个既可以用在WinForm里又可以用在WebForm里的图表控件,可以绘制各种曲线图、柱状图和饼图等等,功能十分强大。在本文中我们要了解Dundas Chart的三个概念:Chart、ChartArea、Series
如是我拿起纸和笔去收集需求:
A:这个我觉得当这个控件上的一个Series的Enabled属性值变化的时候应该触发一个事件,这样我们就可以在这个事件触发时干些事情了,但是现在没有,我不得不做很多工作
B:和A一样的问题,我觉得控件上一个ChartArea的Visible属性值变化时也应该触发一个事件。
C: ………
我的头慢慢的大起来,需求收集了一大堆,怎么弄呢?我还是做了下面这样的个设计
第一次重构:使用继承封装Chart原生API,使其更好使用
我相信很多人第一个设计也许是这样,一下子就想到继承,这样我们可以复用Dundas Chart里面的很多东西了,以免重写很多东西。
下面只选取一段Code:
public class JurassicSeries : Series
{
//...更多扩展
public event EventHandler<EventArgs> SeriesEnabled;
protected void OnSeriesEnabled()
{
if (SeriesEnabled != null)
SeriesEnabled(this, new EventArgs());
}
public new bool Enabled
{
get { return base.Enabled; }
set {
base.Enabled = value;
OnSeriesEnabled();
}
}
//.....更多扩展
}
使用继承扩展Series后,我们终于有了这个事件了,但是因为Dundas并没有将他们的一些属性加上virtual,没办法,我只好在这些属性前加上new了,看着总是挺别扭。然后我将新的控件发布给项目组成员,大家都很高兴,几乎不需要改什么代码就可以享受到更好的“服务”了。
但是问题也接着而来
A:使用你这个控件后,我将控件上的内容保存为xml的时候怎么和原来的xml模板不再一样了啊,而且有的时候还老出现一些怪异行为,控件好像工作的不怎么稳定
原来Dundas的保存xml文件功能是通过反射将属性一个个作为XmlNode的Attribute,在我扩展的时候我给控件添加了一些属性,这样保存的XML文件当然不同了,而且由于使用了继承这种白箱复用机制,有的地方改变了控件的默认行为,所以带来控件工作不稳定,怎么办?看来继承也不是灵丹妙药,我不得不再一次陷入思考,寻找别的良方。
这时大部分开发者耳旁肯定响起了这句话:优先使用组合而不是继承。是的,当时我的耳旁也响起了这句OO原则之一:
优先使用组合而不是继承
思考了下,我又做出了这种设计
第二次重构:使用组合而不是继承
没有再使用继承,我们把Dundas的Chart和Series当作一个黑盒子来复用,还是选取一段代码看看:
public class JurassicChart : UserControl
{
//..................
private Chart Chart;
private IList<JurassicSeries> _series;
public IList<JurassicSeries> Series
{
get { return this._series; }
}
public JurassicChart()
{
this.Chart = new Chart();
}
//.................
}
public class JurassicSeries
{
//...更多扩展
public event EventHandler<EventArgs> SeriesEnabled;
protected void OnSeriesEnabled()
{
if (SeriesEnabled != null)
SeriesEnabled(this, new EventArgs());
}
private Series Series;
public JurassicSeries(Series series)
{
this.Series = series;
}
public bool Enabled
{
get { return this.Series.Enabled; }
set {
this.Series.Enabled = value;
OnSeriesEnabled();
}
}
//.....更多扩展
}
由于我们对Chart的扩展是黑盒进行的,所以无需担心我们更改了Chart控件的默认行为,为了使用方便,可以很快的过渡过来,我封装的时候提供的API和原来的控件的API一模一样,现在的实现只是在原来的API上附加了些东西:
这是一个什么模式呢?
好像是装饰者模式吧,我记得装饰者模式也是给对象添加职责的,不过装饰者模式是动态的给对象添加职责,你看这个能动态的添加职责么?那这个是什么模式呢?我也不知道-_-。
我将重新封装好的控件说明发给大家,然后将代码签入到TFS,由于API的兼容性,很快项目里面的Chart都迁移过来了,经过几天的使用不断的完善控件,功能也趋于稳定了。我想也许可以告一段落了。不过软件里面永恒不变的还是变化。
一天, PM开完会回来:
PM:小Y,刚才会上,公司的图形平台部展示了一下他们的成果,可能我们以后开发中图形控件将要使用公司的,毕竟是我们自己的,定制起来要简单些,但是我也看到了,他们的那些控件还处于研发阶段,还远远没有达到产品的要求,所以你能不能做出这样的设计,当我们的图形平台稳定后能快速的迁移到那个上面,但是我们也不能完全依赖那个,如果那个不成功的话我们还可以使用Dundas的这个。
我:这个。。。。
PM:应该是可以的,你下去想想。哦,还有一点,这个控件的样式要求既可以从Xml文件加载也可以编程实现,控件的数据要求既可以从数据库获取数据绑定或从Xml文件加载,但是图形平台的那个控件只能从Xml加载,所以在从数据库加载的时候你要自己处理一下
我:…………………..
哎,刚做完一个满意的设计,又要修改了。
现在的需求是,我们封装的控件可能有两种实现,Dundas和我们自己的图形平台,还要求有两种模式,Xml模式和数据库模式。貌似可以这样做个设计:
好深的继承层次啊,好像还能满足需求,还是先仔细想想,免得又要返工。
如果又有一种新的控件需要添加进来呢呢?比如图形平台的控件在发布的时候修改了一些接口,这样我们完全可以把它当作一个新控件添加进来,那么我们就需要添加两个类,或者在XmlChart,DataBaseChart这个地方我们添加一个类:控件样式从Xml模板加载,数据从数据库加载(这实际上是我们的常规做法),那么我还要新增两个类,看到没?每次变化类的数目都是以乘法增加。而且很不灵活,Chart的实现都依赖于相应的控件。但是经过这样一分析思路也更明确了,我们要的是两种模式的控件:Xml和数据库模式。但是这两种模式的控件不能知晓到底是依靠哪种第三方控件实现的,那我们只好把第三方控件放在接口后面”躲起来”,将两种模式和实现他们的控件之间解耦,这样各自才可能相互不依赖,可以独自的去变化。
第三次重构:重构到桥接模式
GOF:桥接模式的意图是将抽象与其实现解耦,使它们都可以各自独立的变化
这里的JurassicChart就是我们的抽象,我们使用Dundas’s Chart或者Ours JChart来实现这个JurassicChart,使用IChart这个接口来隔离抽象和变化,现在XmlChart和DataBaseChart对使用何种第三方控件实现毫不知情,让他们都可以按照自己的意愿独立的变化而相互不影响,当然,这个IChart接口肯定不能是随意变化的(一般来讲接口都是需要稳定的,如果接口不能稳定那你的设计明显有问题)
上面是设计的类图,详细的代码先有各位读者自己实现了,或许有一天我公开我的源码,不过几率不大,还是自己写下吧。想想,在你们自己的项目中是否有类似的需求?
后记
这篇文章来源于我真实的开发中,整个重构过程比文章中的更繁琐,写的比较啰嗦,希望能起到抛砖引玉。
从去年开始写一个设计模式初学者系列,写到第六篇了,无奈再也写不下去了,后来检查了下原因,还是那些模式并没有在真实项目中使用过,所以也无法描述,凭空想象的一些例子难免陷入泛泛而谈。所以今后我将改变原来的风格,以真实项目中重构的时间顺序记述下整个模式演化的过程。
|