[系列]设计模式在实际应用中的演化和改良(1)-Visitor
Apr6
〔从这篇《模式的演化和改良》系列开始,我会不定期的更新这个系列的文章,皆在于分享自己在日常工作中的一些心得体会,并整理成较系统的文章,以此和大家沟通分享,也请大家指出不足或提出新的见解〕
基于OO中的职责单一原则,我们需要将一个类的自身行为和外部实体作用于它的行为区分开来。例如我们需要将“商品”类自身的行为(如“获取商品价格”),和其他实体作用于“商品”类的行为(如“统计所有商品的价格”或者“将商品持久化”)区分开来。如果用这些本不属于“商品”类的行为去污染“商品”类的话,我们会发现随着系统的维护,原始的“商品”类将会变得异常的复杂和不可理解,并且这些行为也会分散到“商品”类以及它的各个子类中去,使得维护和扩充这些行为变的复杂。基于这些原因,《设计模式》一书中提出了Visitor模式,将类和对类进行的一些操作解耦开来,使得我们可以方便的在不改变原始类的情况下定义对原始类的新操作。(结构图略,请自行参阅《设计模式》一书218页关于Visitor模式的描述)
在我们使用Visitor模式解耦类和对类进行的一些操作的同时,也引入了一些附作用(同样在《设计模式》一书中有详细的描述)。其中我觉得除了潜在的破坏封装的风险外,其最大的问题是不能使原始类和对其操作的Visitor类独立的进行变化,我们可以很容易的通过扩充新的Visitor类来扩充新的操作,但是却让增加新的原始类变的很困难,Visitor模式假设了原始类族群很少发生变化,但不幸的是在实际应用中这些原始类族群经常发生变化。
考虑设计一个对某些类进行持久化的方案,“持久化”方法并不属于某个特定类自身应具有的行为,另外这些“持久化”行为本身可能多种多样,例如我们可以有两种持久化方案:持久化到数据库或者持久化到Xml文档中;我们当然希望对这种持久化方案的选择不会影响到我们需要持久化的对象自身,于是我们按照Visitor模式得到了如下的结构:
![]() |
| 点击图片看大图Visitor1 |
我们分离了Persist行为,使得我们比较容易扩充新的PersistentProvider而不用重新编译PersistentObject及其子类DerivePersistentObject1。然而这个设计假设了PersistentObject及其子类需要持久化的字段不容易发生变化,举个例子,如果PersistentObject还有一个子类DerivePersistentObject2,它需要持久化的字段也只能是Field1,Field2,Field3;如果它还有第四个字段Field4,那么这个字段无法获得持久化的支持。因为PersistentProvider只认识基类PersistentObject的3个字段Field1,Field2,Field3,当PersistentObject及其子类需要发生变化(加入新的可持久化字段)时,我们不得不修改每一个IPersistentProvider的实现类,这个代价显然是比较大的。
解决的办法是让IPersistentProvider不依赖于具体的PersistentObject类,而是依赖于自身所需要的某种抽象,并让PersistentObject 及其子类去实现这种抽象。考虑持久化一个数据所需要的常用信息:数据的名称,数据的值和数据的类型,把这些常用信息作为我们的抽象根本,我们可以得到如下的一个改良Visitor模式:
![]() |
| 点击图片看大图Visitor2 |
我们使用IPersistentDataProvider来表示IPersistentProvider对需要持久化的类所提出的要求。即需要这些可以被持久化的类实现该接口,并通过该接口告诉IPersistentProvider需要持久化的主键和字段,这些主键和字段都是一种在持久化时所需要的一些通常的数据PersistentElement(数据名称,值和类型)。现在IPersistentProvider和PersistentObject可以独立的发生变化,IPersistentProvider依赖于自身所提出的抽象,扩展新的PersistentObject对象族,只用在新的类中实现IPersistentProvider所提出的那个抽象IPersistentDataProvider接口。无论是修改PersistentProvider以便扩充新的持久化方法,还是修改PersistentObject用以扩充需要持久化的类族,这两部分都可以单独的进行变化,并单独的编译。另外,用PersistentObject实现IPersistentDataProvider,降低了破坏PersistentObject封装性的风险。
当然,这个新的模型仍然存在一些缺陷:
1. 某些场景下可能我们没有办法抽象Visitor对象所需要的数据(即很难提取出类似IPersistentDataProvider这样的接口)。
2. Visitor类(IPersistentProvider及其实现类)的行为比较单一,例如只是持久化。如果Visitor类以后需要支持其他的特性,而这些新的特性和已有特性对目标类的要求差别比较大;例如扩充Visitor使其不只是支持持久化,还可以将目标类图形化,显然图形化要求目标类提供的数据不只是PersistentElement的集合这么简单。这时我们需要定义新的数据抽象,例如IGraphicDataProvider,并要求目标类实现IPersistentDataProvider接口的同时也实现IGraphicDataProvider接口。当Visitor的行为扩充可控时,这样做尚可,但如果Visitor的平行行为(功能完全不同的行为)过多时,我们可能会要求目标类实现过多的接口,从而增加目标类的变化复杂度。幸运的是,在大多数场景中Visitor的变化(特别是平行变化)都少于目标类本身的变化,这种改良Visitor模式在大多数场景下都很适用或者说可控。从另一个角度来说,如果Visitor类有过多的平行行为,根据职责单一原则,这些Visitor类本身的组织结构是不是也需要进行仔细的设计和划分呢?
在Win上离开了MS的Win Developer们怎么活
Apr4
开发Windows程序离开了MS就不行了吗?当我们扔掉了VC,扔掉了MFC,扔掉了.Net Framework,可能因为买不起正版的原因连Delphi和C++ Builder都没的用的时候,我们如何在Win上开发呢?经过我一段时间的切身体会,我觉得如果你能接受以下几点还是可以的:
1.学会使用类似Eclipse这样的开源IDE,虽然被MS宠坏了的你可能觉得这些IDE“很原始”。
2.弄个标准的C++编译器,例如MinGW,尽量弄明白gcc那一堆能让你抓狂的编译选项和g++的另外一堆link选项(当然,你得搞明白link的原理,知道你要link的库的顺序)。
3.学会看懂和使用makefile(当然你也得弄明白里面的一堆make指令),下载了非MS平台的开源程序后习惯去找makefile而不是去找“工程”文件,因为根本就不会有“工程”文件!
4.去找个开源的windows sdk库,wxWidgets是个不错的选择,不过你得花一个小时用gcc编译你下载的这些SDK
5.当然,你还得再熟悉一点win32 api的知识,大概了解下MinGW里面的那一大堆win32 api头文件;如果你能忍耐到你变得够牛气的那一天话,可以用这堆api头文件来开发win程序。
6.最后,花点时间把你下载的那些SDK,编译器和你的IDE集成起来,成为一个可用的环境,不要以为IDE已经帮你弄好了,那是MS…., Eclipse里面的gcc和linker设置就够你头疼的,一个好建议是当你设置你的IDE时记得参考makefile
好了,如果以上几点你基本能搞定的话,恭喜你,你可以摆脱MS,用免费的,无版权问题的软件开发你的windows程序了。 不过有一点要提醒你,你得接受从被MS伺候的舒舒服服的“皇帝”变成一切都要自己动手的“劳动人民”这样的落差,如果这一切你都忍耐过来了,相信你要去开发linux程序也没啥问题了。
诡异的Symbian C++
Apr3
最近买了一本《Symbian OS C++高效编程》,雾里看花的翻了翻前两章,结果才发现Symbian C++比在某些语法上已经稍显晦涩的标准C++还要诡异一些。
比如规定用T,C,R,M的类名前缀来区分不同用途,不同生命周期,不同资源释放方式的类(其实这可以理解为一种不错的编码规范)。T前缀的类表示在栈上分配,并一定没有析构函数的类(注意是“一定”),通常表示简单类型,例如TInt;C前缀的类表示在堆上分配,并一定要具有析构函数,一般表示用户定义的复杂对象类型;R前缀的类需要在析构函数之外,提供另外的类似Close这样的资源释放函数,一般表示资源,例如“文件”;M前缀的类表示接口。看看,要严格遵守这个规定,可能并不是一件容易的事情。
再说Symbian C++里面的异常处理,放弃了标准C++里面的try,catch,throw异常机制,而是使用一种“清除栈”+异常退出函数(User::Leave())+TRAP宏的异常处理方法(注意,Symbian C++还规定可能产生异常的函数必须用L做为后缀);这样做据说一是因为在设计Symbian C++的时候大部分编译器都不支持C++异常处理(很强大的理由…),二是因为这是C++异常处理的一个“嵌入式”版本,比起标准C++异常处理开销更小,更适合手机这样的资源有限系统。
嗯,看来有必要先把标准C++搞清楚,去做做什么练练手,再开始学习Symbian C++,不然越来越混乱了。
OO还是非OO
Apr1
和Anrs的一篇blog用了同样的题目
这个问题根本不用回答,因为是不是要OO这完全取决于应用的规模和问题所处的维度。虽然OO,PO和函数式是3种不同的范式,但是其所要解决的问题和所着眼的维度是不同的;OO在程序达到一定规模时是目前为止能解决问题的最好的编程范式,其着眼的维度也不同,OO在更大,更全局的维度上考虑一个程序的内部组织结构,PO和函数式在更局部或者说更小的维度上考虑问题,当然,在这个维度上他们十分有效。
既然着眼问题的维度不同,那谁说一个OO的程序里面不会包含PO和函数式的写法呢?
另外要多啰嗦一下的是,很多号称OO的程序或语言,其实都不是OO的,只能称之为基于对象,而不能叫做面向对象。基于对象和面向对象是有很大差别滴。

