本文介绍在CodePlex上的一款开源项目在线平面图形编辑器。
在线地址:http://geometry.osenkov.com
源码地址:http://livegeometry.codeplex.com/(如果方便请下载源代码在本地Visual Studio中打开代码,方便理解文章)
该项目是相对复杂的交互型项目,对于以后致力于是用silverlight开发网络应用的朋友,应该尝试去理解习惯熟练使用这种复杂充斥着Windows消息循环理念和各种设计模式的应用程序。
主要类型说明:
类名 |
职责 |
Drawing |
控制类。调度控制各种事件响应。 |
IFigure |
图像接口。用户存放画图板上的对象集合。 |
Behavior |
图形创建类。负责完成画图以及创建画图工具图标。 |
ActionManager |
用于管理操作记录,以便进行undo & redo工作。 |
CoordinateSystem |
用于控制坐标变换,定位,真实值与逻辑值的转换(用于工程作图)。 |
Canvas |
用于显示图形,以及响应鼠标事件。 |
结构图1:
设计模式分析:
上图罗列出了项目主要的接口以及类型。整体结构上乍一看是由Drawing作为Mediator的中介者模式(Mediator),内部的细节通过观察者模式(Observer)实现。(由于观察者模式非常容易在C#里面实现,所以不用Observer而用Mediator的情况非常少见)
中介者模式意图: |
用一个中介对象来封装一系列对象的交互。中介者使个对象之间不需要显示的相互引用,从而使其耦合松散,而且可以独立的改变他们的交互。(但是很多时候为了洁身开发时间,我们依然) |
观察者模式意图: |
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有以来他的对象都得到通知并自行更新。 |
在LG项目中,图像类 – 图像创建类 – 动作管理类 – 画布类 – 坐标系统 之间存在相当数量的复杂交互响应。所以这里采用中介者模式与观察者模式来降低类型之间的耦合度,并提高代码的可维护性。
结构图 2:
观察者模式分析:
用户画图通过以下3个步骤
1. 用户在画图板上按下鼠标左键,产生图形初始状态。
2. 移动鼠标,改变图形状态。
3. 放开鼠标左键确定图形状态。
如图Canvas是UIElement的子类。在UIElement中定义了上述用户3个操作的响应事件。分别为:
1. MouseLeftButtonDown(Event).
2. MouseMove(Event).
3. MouseLeftButtonUp(Event).
最终响应这3个事件地方法在Behavior中
1. MouseDown(Method)
2. MouseMove(Method)
3. MouseUp(Method)
在Behavior类中这3个方法都是虚方法,最终将由Behavior的子类来完成具体实现
结构图 2 - 关系表
通过结构图2我们可以看到项目中存在这样一些引用关系的表
属性名 |
类型 |
所属类型 |
Parent |
Canvas |
Behavior |
Drawing |
Drawing |
Behavior |
Behavior |
Behavior |
Drawing |
Canvas |
Canvas |
Drawing |
我想看到图的时候大家都会有这样的疑问,既然Drawing作为中介者类包含了Behavior 和 Canvas,那么Behavior就不应该显式的引用Canvas,而应该通过Drawing中的一个中介方法来形成交互。 非常正确,我也是这样想的,而且我觉得Canvas应该把他的方法抽象出一个接口,那么以后 只要实现了接口,想往什么地方画就往什么地方话。但是我要告诉大家生活中很重要的几件事情:
1. 不要幻想完全解耦。
2. 很多时候我们徘徊在效率和设计之间。
3. 更多时候我们徘徊在时间和设计之间。(时间就是金钱)
4. 可扩展性也需要界限,不要幻想自己的项目横着也可以扩展,竖着也可以扩展,那你抽象到那年那月去。
设计者放弃了对于画布的可扩展性,换来几分钟时间上的胜利:)。
我想如果是我将在Drawing类型中重新将Canvas中的事件进行封装,这样可以有效地将Behavior和Canvas的显示引用取消。好处自然是可扩展性得到了增强,当你单方面修改Canvas的时候不需要考虑实际上跟Canvas发生关系的其他多个对象,只需要修改Canvas和Drawing的关系。10分钟后我修改完毕。
修改完毕后。我依然忧心忡忡,一旦我修改Canvas就要修改Drawing。意识我索性定义一个ICanvas借口,Canvas继承这个接口实现内部细节。这样即使我修改Canvas,也不会影响到Drawing,于是我们又花了10分钟,将Canvas中Drawing交互的关系抽象成了接口并让Canvas集成ICanvas接口。此时我们花了20分钟处理了一个只有3个事件的接口,并幻想着有一天有什么东西可以华丽的继承与该接口让我们的画笔可以随意画到什么地方。
我不反对这样,这取决你的时间和你的需求。当你对于画布扩展的需求达到了80%的时候你用80%的时间去实现这样的可扩展性绝对是一件睿智的决定。反之,则属于浪费时间。(这跟用户需求不一样,请大家不要类比)
状态模式意图: |
允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了他的类。 |
这里的类是指Drawing类型。而状态指的是Drawing中的Behavior属性。
当用鼠标点击图像工具栏上的图标时,Drawing的Behavior属性将被替换为相应的图像创建类或者拖拽类,当然也可以是其他类型。此时Drawing的Behavior成为了Drawing行为的状态标志,Drawing的行为取决于当前的Behavior类型中的具体实现。(注意:Behavior指的是Drawing的属性,而不是Behavior类型)
结构图 - 不是工厂方法的工厂
工厂方法意图:
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。
设计模式是为了减少耦合度,提高可扩展性。但是这个代码仅仅提高了开发效率,却严重降低了代码的可扩展性。
这里没有创建对象的接口,也没有穿件对象的子类。FigureCreator作为调用者 用Factory对象创建各种实现了IFigure接口的子对象。也就是说每当我们扩展一个IFigure子类,那么我们需要修改Factory的表现(添加一个静态方法创建该类)。或许我们会这样想,难道一定要在FigureCreator中使用Factory创建IFigure么。我们为什么不直接使用IFigure子类的构造方法来创建对象。
首先找到设计者在代码中用于创建对象的代码。
var segment = Factory.CreateSegment(Drawing, new FigureList() { p1, p2 });
Drawing.Add(segment);
var!!!!!!!!!!大家一定很惊叹。这个C#3.0出来的鬼东西。在这个位置使用了var,从代码外观上来讲完全符合了隐式引用的形态,但是这里是赤裸裸的显示引用。(别以为穿了马甲我们就认不出来)。编译器在最终会将这个var变回他原本的类型,记住这里是编译器,而不是我们期待的推迟到运行时。那么我们到底应该如何实现来避免显示引用呢?
1. 我们应该创建一个抽象类FigureFactory.
2. 我们为每个IFigure子类创建一个继承于FigureFactory的子类。
3. 在FigureCreator中,我们将FigureFactory的子类对象作为参数传入FigureCreator中用来创建IFigure对象。
但是读者会问我们 明明可以直接将IFigure的对象作为参数传入到FigureCreator中,为什么我们还要多此一举传一个工厂方法。答案就是:之所以是工厂,是因为不单单要生产一个实例,而是要生产消费者所需要数量的实例,这由消费类FigureCreator来决定。
PS:传入对象实例也是可以满足消费者对于产生多个同类型对象的需求,那样的话就要使用 原型模式(prototype),这在C#语言中有良好的集成。
组合模式意图:
将对象组合成树形结构以表示“部分-整体”的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。
代码分析
先请大家闭上左眼只看右半边(开玩笑)。请用双眼看IMultiAction(组合对象)观察IAction(单个对象)之间的关系。
1. IMultiAciton继承自IAction
2. IMultiAction本身是一个IAction的集合
这就是典型的组合模式。这样对于单一动作以及复合动作的操作具备了一致性。
问题分析
为了让代码工整可读性强,我总是幻想树形结构的继承和引用关系,所以对于组合模式我以前一直有一个问题:
我们是否可以用一个超类型 ISuperAction,让IAction 以及 IMultiAction继承自ISuperAction,之后让IMultiAction实现Ilist<IAciont>的方案呢?
答案是否定的,虽然这样对于实现代码的使用具备了一致性,但是这个结构类型完全变了。改变之前的类型可以理解森林,而改变之后的类型仅仅是一个队列。森林可以包含队列表现的,而队列很难表现为森林。
PS:我之所以列出了左半边的图是为了告诉大家,设计者巧妙地运用了C#语言的内置类型List<T>,降低了开发成本。
请注意:IMultiAction继承自IList<iAction>,MultiAction继承自IMultiAction以及List<IAction>。这就等于说List<Action>替代MulitiAction实现了IList的所有方法。相当犀利的降低开发成本的方案:)。
备忘模式意图:
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
为了实现Undo Redo这一块的功能,设计者使用了备忘模式。Drawing是被记录状态的对象。ActionManager是用来管理状态的类,ITransaction为状态对象。
ITransaction实际上是逻辑意义上的状态,记录一次性的状态变换过程。
IAction是物理级上的状态,记录一个原子级物体的状态变换过程。
ITranscation由一系列IAction构成。
状态机模式意图:
允许一个对象在其内部状态改变的时候改变其自身行为。对象看起来似乎修改了它的类。
现有代码:
这里实际上没有状态机模式。仅仅是作者在这里使用了状态机的概念去处理用户响应
左图这个类是一个处理用户画图动作的基类。比如用户点击鼠标,用户移动鼠标等。当然这时候类响应用户动作所要完成的功能是不同的,这取决于该类当前的状态。
比如说,你要画一个线条,你画第一个点和第二个点的时候同样是按下鼠标但是获得了不同的效果。
代码分析:
首先这里有三个大的状态:
1. 没有开始画图
2. 正在画图
3. 画图结束
三者是顺序状态
1 –> Started –> 2 –> stopping –> 3
由Started 方法和 stopping方法 作为状态装换方法。
在画图状态中又存在了多个子状态,这些子状态是动态生成的,这正是我本节要描述的重点。
画图中的状态转换:
如上所述,我们要画一条线段,那么我们就需要两个点来确定这条直线。
状态转换:
没有画点 –> 画下第一个点 – > 画下第二个点
但是当我们要画一条线的平行线的时候,
状态转换:
没有任何东西 –> 选择一条直线 –> 画下一个点
当然我们还可能面对多边形或者更加复杂的几何图形,状态流的不确定性很大,所以我们要把这一块做很好的处理。
作者的做法:
这里有3个属性:
属性名 |
类名 |
作用 |
ExpectedDependencies |
DependenyList:List<type> |
当前图形所需要的 子依赖项(点线或者其他类型)类型队列 |
ExpectedDependency |
Type |
当前状态 所需要构建的子依赖项类型(要画点的时候就是IPoint) |
FoundDependencies |
FigureList:List<IFigure> |
已完成的 子依赖项堆栈 |
关系如下:
If( FoundDependencies.Count < ExpectedDependecies.Count)
ExpectedDependency = ExpectedDependecies[FoundDependencies.Count];
else
return null;
每当我们在转换一个状态的时候,FoundDependencies就会多加一个子依赖项,那么ExpectedDependency就会在ExpectedDependencies队列上移动,直到FoundDependencies.Count = ExpectedDependecies.Count返回null,此时所有的依赖项都已经在画布上了。
也就是说,作者是依靠ExpectedDependencies 作为状态图,ExpectedDependency以及FoundDependencies作为状态存储来实现对于变换型状态的适应。
个人观点:
我觉得作者的思路很好,但是我看了半天,一直不理解明明是一个堆栈就可以搞定的事情,为什么设计者折腾出了3个对象,还搞的这么复杂。如果有读者思考出来麻烦通知小弟一下,不胜感激。
作者在这个类中似乎下了很多功夫,各种范型,各种重载,各种单例。
从项目当前的功能和规模来看,属于超级过度设计,不过代码量到不是很多(没有很多时间成本),我很喜欢。
还不太会在blog里面贴代码,所以大家有空可以自己看一下。
作者为了放回一个类型列表,并为了让这个类型列表可以轻易地被初始化和满足各种初始化的变换,使用了比较复杂的设计。比较有意思。
刚才尝试了一下发布草稿,结果网络出了问题,想想也差不多将项目的大体情况分析完毕,到此结束吧。代码这东西还是要自己看看,才能理解。
虽然对于设计者的一些设计存在分歧,但是从观看作者的代码来说依然学习到了很多东西。
作者的核心代码是WPF - Silverlight兼容形态的,很有意思,从这方面也学习到了很多东西。
PS: 依然不太会用cnblgos,怎么把页面宽度加大,困惑了好久。图片不是显示不是很清晰,请见谅。
本文意图:
作者:Runaway
出处:http://runaway.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。