在上一篇(你可以点击这里查看上一篇)中我描述了我的项目中如何从一般化设计重构到桥接模式,发表后我一直在想,我是完成那个未完成的《设计模式初学者系列》还是就此终止,开始一个新的旅程?左思右想,还是开始一个新的系列吧,设计模式的文章园子里已经有很多很多了,我是无能如何也不可能超过那些前辈的。所以我还是以重构的步伐来讲述我在实际项目中应用模式的历程。如果你在文中找到了自己的影子,不要见怪,文中所有的项目案例,设计,重构步骤都来源于我真实的项目,我只是事后把他用文字记录下来了而已。
那好吧,开始我们今天的重构之旅吧:重构到组合模式
场景
还是啰嗦两句,我觉得任何设计脱离了实际的业务环境那完全是扯淡,所以我在这里还是费一些笔墨说说我们的业务。
在石油开发中各种图件是石油工程师用来分析的非常重要的资料。我们今天要做的一个东西就是“产液剖面图”,如下图所示:
左边的圆柱体代表着一口“油井”,圆柱体上面的红色块代表一个“层位”,油井里面的油和水就是从这些层位渗透到井里面,然后抽上地面的。而右边两张表格
上面画出的是两个测试时间,各个油层产油,产水的一个情况,井和表格上面的坐标是一一对应的,这样石油工程师拿到这个图就可以清楚的看到井上的某个层位
在某个时间产油、产水是多少,而相应的做出一些决策。
需求说完了,来谈谈我的设计思路变迁吧。
第一步: 最初的设想
拿到需求后,我的第一反应是有没有现成的控件?在上一篇文章中我提到了Dundas Chart这个控件,看起来好像能完成这个任务,经过几次试验我还是失败了。
这也许是大多数人拿到需求后的第一反应,找一找是否有参考的东西。
第二步: 混乱的开始
好吧,既然找不到这样的控件那我就自己做了,我在纸上写下了我的步骤:
1.在这个UserControl里放一个PictureBox控件
2.然后建立一个Chart控件,这个控件用来管理绘图区域,坐标转换等,这样我们就有了统一的坐标系。将这个图的标题,还有右下角的图例也放在这个Chart里,
我认为这是公共的东西。
3.然后下面就是画图了,写几个方法,按顺序画图:
//PictureBox的OnPaint事件触发时执行
private void mainPicBox_Paint(object sender, PaintEventArgs e)
{
Draw(e.Graphics);
}
private void Draw(Graphics graphics)
{
DrawWell(graphics);
DrawSection(graphics);
}
//绘制油井
private void DrawWell(Graphics graphics)
{
//绘制油井的方法实现
}
//绘制油井上的层位(红色块)
private void DrawSection(Graphics graphics)
{
//绘制层位的方法实现
}
//绘制网格
private void DrawGrid(Graphics graphics)
{
//
}
//....等等更多绘制方法
我心惊胆战的编写着代码,生怕哪个地方把代码逻辑弄错了。上面的代码是我写出的第一个版本,
图画出来了,而且还不错,任务是完成了。
但是编写代码时的体验并不是很好,不知道大家有没有相同的感受,在使用面向过程的方式编写
代码的时候大脑必须保持足够的清晰,总是记着那条主线,而且如果一天之内没有把这个任务完
成,如果拖延到第二天,总是要看半天才能进入状态。但是使用OO的方式就不一样了,在设计的
时候把各个对象的职责划分清楚,那我们在一个时候关注的面就很小,每次只要完成一些东西就
可以了,对其他部分没有多大影响。 OK,上面的代码已经可以运行了,而且运行的很好,我为啥还要重构他呢,本来我可以就此搁笔,
这篇文章也没有必要写下去了,我不是个完全的完美主义者,只要代码不是特别的“恶心”,我
不会对他作什么更多的修改。但是无奈,我的图在PM的眼里并不是如我所看的那么完美,在这个版本中,我并没有画上一些文
字标注,比如各个产油,产水的bar后面的数字,如是我又找到那个方法,看呀看呀,找到可以
修改的地方。如果就是这些小修小补我也可以忍受。ok了,工作完成了,我把代码CheckIn到
TFS,然后CheckOut我的下一个WorkItem:注水剖面绘制,看了需求说明上的简图,怎么和
这个“产液剖面”如此的相像呢,在确认没有弄错之后,我陷入了沉思,我是拷贝那里的代码,
还是怎么做?虽然我并不是追求绝对的完美,但是在我眼里容不得重复的代码,而且这里拷贝代
码也需要做很多修改,虽然两个模块的图形非常类似,绘制图形所需要的数据则很大的不同,这
个好说,我可以用泛型解决。还有一些地方绘制的方式也不一样,不过大体上的绘制步骤应该是
一样的,我一想,这不是模板方法么?
模板方法:父类定义算法的骨架,将算法中的一些步骤推迟到子类实现。(GOF)
第三步:重构到模板方法 但是由于刚才并没有设计良好,几个方法之间耦合性很强,而且方法里面的逻辑混乱,我费了好
大的劲才弄成模板方法的,代码是比拷贝少了很多,但貌似还是一种过程式的,这次重构完全是
为了整体上看起来代码少些,对程序的结构没有任何改良。
至于模板方法模式是个什么样子的,我这里就不给出例子了,园子里已经有很多相关经典的文章,
不过你也可以看我的这篇:
设计模式初学者系列-模板方法
还是一样CheckIn代码,CheckOut WorkItem,一看怎么还是什么XXX剖面,再看看所有的
WorkItems,好多剖面的绘制啊,功能也大同小异,难道我就这样用模板方法一个的实现?在上
一篇文章里我们就谈到了,继承这种白箱复用有很多局限性,子类有一丁点与父类不同我们就必
须重写,如果这么多模块都要用模板方法实现,那肯定要不断的调整,有没有什么更好的方法? 第四步:重构到组合模式 再去看一眼那个图,难道他就是铁板一块么?我们可以把他分成很多小对象,每个对象管理自己
的数据并且绘制自己,在这个图里面有哪些可以划分的对象呢?
Well //油井
Section //层位
Grid //网格
Axis //坐标轴
Text //图上的文本
....其他一些对象
每个对象都应该有坐标数据,还应该有一个绘制的方法,对象自己绘制自己,其他的事情都无需
关心。这里引出了OO的另外一个原则:
对象应该对自己负责,而且职责单一
设计完几个类后,突然有种感觉,这个图怎么样一颗对象树啊,图里面有油井,网格,油井里面
有层位,网格里面有坐标轴等东西,难道我们要和这一个个对象?想一想,哪一个模式是将对象
组织成树结构一样?她就是今天的主角-组合模式,或者叫部分-整体模式 组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。Composite模式使得用
户对单个对象和组合对象的使用具有一致性。【GOF】
设计如下图所示:
部分代码:
public class UIElement
{
private IList<UIElement> Children;
public UIElement()
{
this.Children = new List<UIElement>();
}
public float X
{get;set;}
public float Y
{get;set;}
public void Add(UIElement item)
{
this.Children.Add(item);
}
public void Draw(Graphics graphics)
{
foreach(UIElement item in this.Children)
{
item.Draw(graphics);
}
}
}
public class Chart : UIElement
{
public void Add(UIElement item)
{
//在chart里我们要处理一些坐标变换等东西,
//所以这里的Add方法需要重写
}
}
public class Well : UIElement
{
public Well():base()
{}
public float Width
{get;set;}
public float Height
{get;set;}
public void Draw(Graphics graphics)
{
//绘制油井代码实现
base.Draw(graphics);
}
}
//.................
好了吧,用这样的设计,在同一时刻你只需要考虑一个很小的对象如何去绘制,而且也非常好扩展,如果有更多的东西需要绘制在界面上,只需要从UIElement继承
就可以了,客户调用的时候只需要构建一个Chart对象,向其Children里添加子对象,然后调用其Draw方法就可以绘制了。在这里如果构建Chart对象呢?我们可以使用一个创建型模式解决。
后记
实际上一般绘图程序都是使用组合模式来解决,但是我开始的时候并没有认识到这个绘图的东西需要多处复用,所以想以最容易想到的方式做掉,但往往最容易想到的方式并不是最简单和最好的方式。所以也有了这篇文章。
本文只是记述重构的旅程,并没有详细描述GUI绘图的相关知识,也没有对组合模式更多的细节进行介绍,我这里想表达的只是一个设计思路,如果你想更多的了解组合模式你可以点击
这里 查看TerryLee的设计模式系列中的组合模式
祝你编程愉快
|