配置编程:让项目开发从多样到统一
暗夜在火星 2012年12月11日星期二
前记:
在再一次(前面省略N次重构不计)通过重构,把目前正在开发的一个项目模块系统化后,忽然有些开发心得,故记如下。
一直都说,软件开发是一门艺术。
而笔者前段时间也有接触摄影,从中了解到摄影从入门到精通也分开三个阶段,分别就是:简单、多样、统一。在做了这么多项目开发后,笔者也发现,软件开发也同样经历着以上三个阶段。开发人员像摄影师一样,通过不断的实践,从中摸索,略有所悟。
笔者作为众多开发人员之一,也将自己的一些感悟在此与大家分享。
简单:
记得刚上大学的时候,教我们编程的那位老先生就说:编程,越简单越好。
诚然,开发领域也一直信奉着KISS原则,把软件开发得简单明了以致看不到的缺陷(而不是把项目搞得复杂以致找不到明显的缺陷)。
然后理想总是很丰满,现实总是很露骨。
基本上所有的项目都是要求拥有各种各样的功能、多个模块、N个子系统。虽然秉承着“高内聚、低耦合”的思想,还是逃避不了复杂多变的命运,更别说客户频频变更的需求。于是也就有了以下的多样。
多样:
当一个项目拥有一定人用户时,或者成为产品时,其功能多样性导致系统的复杂性,总让开发人员很纠结。特别当没有统一的策略或者解决方案时,更会让项目陷入混乱,甚至让项目中止。
比如(以笔者正在开发的项目为例):一个需要同步本地和服务器邮件联系人的模块,当联系人发生改变时,需要同步更新本地和服务器相应联系人的最后更新时间,因为在本地维护了一份所有联系人最后更新时间的列表,用于对比与服务器的时间,如果一致则不用更新。那么,何时调用更新最后修改时间操作呢?
目前假设在以下几个时机需要更新:
1. 客户修改本地联系人时
2. 从服务器更新联系人到本地时
3. 同步本地联系人到服务器时(此情况发生在客户有多个客户端时,类似SVN)
如果让以上时机(可能还会有更时机),再由开发人员手动调用的话,问题就来了。因为开发人员往往不会意识到需要更新本地最后修改时间而忘了调用。特别是当有多个开发人员分别负责以上各个模块时!当发现因为没有更新最后修改时间而出问题追究到开发人员时,他或许很无辜并惊讶问道:哈?!需要更新最后修改时间吗?什么时候的事。。。
这时,就需要一种统一的手段进行控制,也就是以下将要重点讨论的:统一。
统一:
不得不说,软件开发行业虽然发展才几十年,但已经有了很多前人宝贵丰富的系统体系理论,并蕴含着各种哲理。
现在,很多框架,系统或者网站都采用了统一的处理方案,如统一的异常处理机制、统一的日记纪录方式、统一的接入方式。正是这样高度统一的思想,使得系统集成更加方便。像Eclipse就提供了一致的插件接入,从而吸引了众多的开发人员;像Yii开发框架使用了一致的日记纪录方式,使得开发人员很好查看错误信息;像新浪的开放平台也一样。
可以看到,统一的思想已经普遍并成熟,更重要的是得到了很好的应用。
本文的主题是重构,但正如真正的作家的功底不是在于捏词造句,而是在于其人生阅历。同样,重构只是一种开发的手段,途径,真正需要的还是一种思想,一种指导思想。比如单职责设计原则、“优先使用对象组合,而不是类继承”的面向对象设计原则、敏捷开发中提到的测试驱动编程等。
而我现在想要分享的是:优先考虑配置编程,而不实现编程。
同样,结合笔者刚才那个项目进行详细讲解。
在此项目中,有一模块是需要对本地和服务器间的邮件联系人进行同步。而同步策略有以下4种:
1. 冲突时提示
2. 双向同步
3. 单向同步,Outlook到ExtMail
4. 单向同步,ExtMail到Outlook
尽管这样的设计在整体上效果很好,但当到了各个策略具体实现时,问题就来了。因为源自于以下的项目需求:
发现如果让各个策略类各自完成相应的同步操作话,会导致每个策略类都会很复杂,业务层面不易统一维护。而通过分析(其实笔者是重构了,再重构,才得出来的分析)可知,同步最终的操作,也就是实现可分别以下5种情况:
1. 不需要处理(很诱人,但不是唯一的一种情况)
2. 同步本地到服务器
3. 更新到本地
4. 从本地删除
5. 从服务器删除
试想一下,如果没有一种统一的处理方式,各个策略类各自需要处理以上各种情况会是怎样的复杂?虽然有些策略不需要考虑其中某些操作情况。
但,如果具体的策略类不需要考虑实现,而是考虑配置编程,开发难度会不会大大降低?
恩,这里停顿一下,因为关键的时候来了。先刷下微博吧,清醒一下头脑先。
。。。
。。。
好,回来继续。
配置?对,就是配置。
我们可以就以上5种情况分别作相应的标记,如下所示:
protected bool isLocalNeedUpdate;
protected bool isServerNeedUpdate;
protected bool isLocalNeedDelete;
protected bool isServerNeedDelete;
那么再结合模板方法,在父类的策略类中加下以下的模板方法函数:
class ExtSyncPersonalContactStrategy { protected bool isLocalNeedUpdate; protected bool isServerNeedUpdate; protected bool isLocalNeedDelete; protected bool isServerNeedDelete; /// <summary> /// 模板方法 /// </summary> /// <param name="strvCardName">等待同步的联系人</param> virtual public void DownloadContactFormServerByNames(List<string> strvCardName) { foreach (string strName in strvCardName) { isLocalNeedUpdate = isServerNeedUpdate = false; isLocalNeedDelete = isServerNeedDelete = false; //分情况处理 if (IsLocalExits(strName) && IsServerExits(strName)) { //1. 本地与服务器都存在 SyncContactBetweenLoaclAndServer(strName); } else if (IsLocalExits(strName) && ! IsServerExits(strName)) { //2. 本地存在,但服务器没有 SyncContactIfLocalExist(strName); } else { //3. 本地没有,但服务器存在 SyncContactIfServerExist(strName); } if(isLocalNeedUpdate) { //更新到本地操作 } if(isServerNeedUpdate) { //同步到服务器操作 } if(isLocalNeedDelete) { //从本地删除操作 } if(isServerNeedDelete) { //从服务器删除操作 } } } virtual protected void SyncContactBetweenLoaclAndServer(string strName) { } virtual protected void SyncContactIfLocalExist(string strName) { } virtual protected void SyncContactIfServerExist(string strName) { } }//end of class ExtSyncPersonalContactStrategy
补充说明:
以上代码是简化版,以突出需要分享的思想:优先考虑配置编程,而不实现编程。
其中,bool IsLocalExits(string strName) 与 bool IsServerExits(string strName)分别用来判断是否存在本地或者存在服务器。
而三个虚函数则是分别针对三种不同情况的具体不同处理,主要是修改相关配置。
目前,策略父类的类图如下所示:
经过这样统一后,开发人员就可以根据不同的具体策略类选择重写感兴趣的虚函数,从而修改不同的配置,即可实现同步。而无须再编写各种实现操作代码(虽然很可能也是复制,但同样也是个问题)。
下面再以双向同步子类的实现深度说明。
由于双向同步策略的业务规则是:本地、服务器那方最新则以哪方为准,一方不存在则以存在的一方为准,则其子类实现代码如下:
class ExtBothwaySyncPersonalContact : ExtSyncPersonalContactStrategy { override protected void SyncContactBetweenLoaclAndServer(string strName) { int cResult = CompareLocalAndServerByName(strName); if (cResult > 0) isServerNeedUpdate = true; else if (cResult < 0) isLocalNeedUpdate = true; } override protected void SyncContactIfLocalExist(string strName) { isServerNeedUpdate = true; } override protected void SyncContactIfServerExist(string strName) { isLocalNeedUpdate = true; } }//end of class ExtBothwaySyncPersonalContact 其中CompareLocalAndServerByName函数存在于父类,用于比较本地与服务器的最后更新时间,如下所示: /// <summary> /// 比较本地和服务器的规则 /// </summary> /// <param name="strName"></param> /// <returns>-1表示本地版本旧,0表示相同,1表示本地版新</returns> protected int CompareLocalAndServerByName(string strName) { // }
是不是子类的实现变得如此简单?!
或许你感觉不到,好吧,让你看一下,我在没有领悟出以上解决方案前的双向同步类的实现代码,对比一下,你就会发现有明显的不同了。
算了,代码太长,看着也累。
其实想想也会发现,每个操作都不是简单的几行代码,虽然在很好的封装情况下,会精简到一两行,通过委托的方式实现。但是依然要对各个不同的情况进行处理,而达不到一致的最终处理方式。
另外一个项目例子?
笔者之前在某游戏后台系统开发时,曾需要根据玩家的ID或者名称查询游戏中与该玩家相关的所有纪录数据。如玩家的物品、副本等大类,其中大类中又有各小类查询。基本算起来有8个大类,共40多个小类。大概类结构图如下:
考虑到查询的操作流程一致,唯一不同的是不同的查询限制条件,如时间范围、玩家ID或者名称。因此,我们可以将操作流程统一到查询父类,扩展子类只需要简单修改以上配置条件即可。
如扩展的一子类:
<?php require_once('LogQuery.class.php'); /** * 盟会查询类 */ class GangLogQuery extends LogQuery { /** * 实现父类初始化函数 * 值得注意的是这里的匹配规则 */ protected function initialize() { //在此配置。。。 } } ?>
经过这样重构(原来的情况不得不说有点混乱),最直接的成效就是,当用户需要添加一个大类查询或者小类查询时,原来要3个小时(包括修改代码、测试、发布),现在只需要5分钟!而且,正确率高。因为它是如此简单而明显没有问题。
小结:
最后小结一下。
“优先考虑配置编程,而不实现编程”适用在以下情况(也是本文讨论分享的):
当模块内有一系列不同的处理方式,最终根据不同情况以相同方式进行处理时。
当然,还有另外的情况,如用在工厂方法里。
但这里,暂且只讨论上述情况。此时可以结合设计模式里的模板方法进行重构。
好处:
通过将开发重点转移到配置上,使得开发难度大减低,而且让项目开发达到了一致性。因为开发人员需要做的,仅仅是修改配置即可完成相应的功能。
特别在类似游戏开发中,有各种任务、各种装备、各种场景,而有时同一模块内,操作流程一相,不同的是参数。因此,更可以考虑配置编程。
先前,笔者在一家游戏公司开发时,就学习了这种“策略”,而且通过编写宏命令,直接将保存在Excel里面的各个参数转成相应的配置实现的类。这样,既避免了烦锁的代码编写,又保证了数据的正确性。
说得通俗一点,配置编程的好处就是:类似使用QQ,用户设置了不同的在线状态,系统就人执行相应不同的操作。
说得专业一点,其好处就是:你可以告诉系统你想要什么,让系统知道做什么,而不用再去考虑如何实现。犹如SQL查询一样,在此我们不妨把配置编程称做为“仿4GL”的语言。
最后,仿造正在看的《领域驱动设计与模式实战》书中经常提到的一句话来结语,那就是:多样,统一?重构。(原话是:经灯,绿灯,重构)