前面我们谈到了功能扩展对维护一个软件的巨大作用。实际上,正是因为功能在不断地扩展,才使得我们的很多软件质量在下降。因此,如何进行功能扩展,我们不得不察。每当新功能到来的时候,不用急急匆匆就开始编码,我们应当仔细思考我们的设计,即使是时间非常紧张的项目。用更多的时间去思考与设计,才会用更少的时间去做更简单的设计与编码。在这里,我提倡的是设计应当简单到发指,因为它体现的是一种精巧绝伦,它会使我们的思路更清晰,维护更简单,变更更容易。只有经过仔细的思考,才会做出精巧绝伦的设计,那是我们的目标。在这方面,“小步快跑”与“两顶帽子”的方法可以大大降低我们的设计难度,因为我们不是神,而是人。
上一章,我们用“抽取接口”的方法,替代万恶的if语句,从而实现了功能扩展。注意,不是所有的if语句都需要这样调整,只有那些真正需要扩展的时候才应该这样调整。如果if语句只有2、3个条件分支,并且不太可能扩展,我们真的不必这样调整,或者当这样的功能扩展到来的时候再进行调整。记住,过度设计与恰到好处的设计只有一线之隔。
除此之外,我们还有很多的办法来扩展我们的功能,其中一种扩展叫过程的扩展,我们可以这样设计:
前面代码复用的部分我们提到,解决处理过程中相同或相似的代码最好的办法就是使用模板模式。首先将那些相同的代码抽取出来形成函数,将这些函数抽象并升级为抽象类及接口,然后将各自不同的代码统一函数名,放在各个实现类中各自去实现。这样,代码复用的问题就解决了。
但是,毫无疑问我们都不是先知,永远都无法预测未来系统会变成什么样儿。比较常见的需求变更之一就是处理步骤的变更。现在我们的处理有1、2、3步,而今后可能还有5、6、7步,甚至某项步骤可能会插入到现有步骤的中间。当日后这样的变更发生的时候,我们又希望符合OCP原则而不改动现有代码时,又应当怎样设计呢?嗯,是个问题。
我过去就曾无数次遇到过这样的问题,其中一个令我印象深刻的就是一次平台控件的设计。在一次平台开发中,我设计了许多的控件,如文本框、下拉框、单选框、复选框……开发这些控件的目的是使其它开发者在设计报表的过滤条件时不用再写任何代码,选择控件就可以了。起初,我为所有控件都提供了draw()和beUsed()方法,用于绘制控件和判断该条件在查询时是否被使用。随着控件品种的增加,一些控件需要在绘制前要执行一个查询,如那些多选框、下拉框等等。为此我准备设计了一个getItems()方法,只要这些控件定义了各自的查询语句,就可以通过该方法查询并返回结果。但问题是,前面已经设计好的控件不用这个方法,我不希望因为这个功能的扩展影响了前面那些控件,这该怎么设计呢?
每次面对这样的问题时,一种叫做“钩子(hook)”的设计就可以派上用场了。什么叫“钩子”?它是一个空函数,调用它就如同什么都没有调用一般。但钩子如果被放在了抽象类中,作用就非常大了。如果抽象类的子类要使用它时,则重载这个函数,为其编写各自的代码,完成相应的操作;而其它的子类如果不使用它,则什么也不用做。当系统在调用各个子类时,被重载的子类就会去调用子类中的函数,而其它没有被重载的子类则会去调用抽象类中的“钩子”,就如同什么都没有做一样。
在该示例中,getItems()就是一个“钩子”,它首先被定义在父类AbstractControl中。AbstractControl是一个抽象类,但getItems()在里面不是被定义成一个抽象方法,而是一个普通方法,因为它不需要每个子类都去实现它,不使用的子类就不用再实现它了。
/* (non-Javadoc)
* @see com...control.Control#getItems(com...model.RptControl)
*/
public List getItems(RptControl control){
//hook only
return null;
}
那些在绘制前不需要查询的控件,如DefaultControl,在继承父类的时候不用去重载getItems(),因此系统在绘制它们时,该函数就如同不存在一般。然而那些需要查询的控件,如QueryControl,就需要重载这个函数:
/* (non-Javadoc)
* @see com...control.Control#getItems(com...model.RptControl)
*/
public List getItems(RptControl control) {
if (control==null) {
throw new IllegalArgumentException("参数为空");
}
String sql = control.getSql();
if (sql==null||"".equals(sql)) {
throw new RuntimeException("SQL为空");
}
BasicQuery query = new BasicQuery();
query.setExpression(sql);
return getJdbcSupport().find(query);
}
这样,当系统在绘制DefaultControl的时候不会去查询数据库,而绘制QueryControl的时候则先去进行一个数据库查询。现在我们来检测一下该可扩展点的设计能否满足OCP原则的要求。现在新需求来了,要绘制这么一个组合控件:
这个组合控件由四个下拉框组成,分别代表2个年度与2个月份,因此它在执行查询时,会提交到后台这4个数据。然而,我们希望这个控件在提交给查询模块时,应当是2个数据:某年某月的1日,和某年某月的最后一天。也就是说,该控件在提交参数给查询模块的时候需要进行一个参数转换,而不是直接传递给查询模块。
先看看我们现有的设计吧:当控件将参数提交给后台以后,控件会直接将参数传递给查询模块。但为了实现这样一个新需求,我们需要所有控件在这个地方硬生生插入一个数据转换的功能。如果真的这样修改了,整个系统就因小失大了。幸运的是,我们在这个地方有可扩展设计。
首先,我们在抽象的父类AbstractControl中加入一个非抽象方法transform():
/* (non-Javadoc)
* @see com...control.Control#transform(java.lang.String, java.util.Map)
*/
public void transform(String ctrlName, Map<String, Object> params){
//hook only
}
然后我们创建新控件MonthRangeControl,重载transform()方法:
/* (non-Javadoc)
* @see com...control.Control#transform(java.lang.String, java.util.Map)
*/
public void transform(String ctrlName, Map<String, Object> params){
int yearLower = getValue(ctrlName, params.get(“yearLower”));
int monthLower = getValue(ctrlName, params.get(“monthLower”));
int yearUpper = getValue(ctrlName, params.get(“yearUpper”));
int monthUpper = getValue(ctrlName, params.get(“monthUpper”));
Date lower = DateUtil.getDate(yearLower, monthLower, 1);
Date upper =
DateUtil.getLastDayOfMonth(DateUtil.getDate(yearUpper, monthUpper, 1));
params.put(ctrlName, lower);
params.put(ctrlName, upper);
}
整个设计修改了父类AbstractControl,增加了transform()方法,然后创建了新的控件类MonthRangeControl,不能说完全没有修改原程序,但已经在最大限度上满足了OCP原则。整个设计如图:
(续)
相关文档
遗留系统:IT攻城狮永远的痛
需求变更是罪恶之源吗?
系统重构是个什么玩意儿
我们应当改变我们的设计习惯
小步快跑是这样玩的(上)
小步快跑是这样玩的(下)
代码复用应该这样做(1)
代码复用应该这样做(2)
代码复用应该这样做(3)
做好代码复用不简单
软件可以这样功能扩展
过程扩展与放置钩子
特别说明:希望网友们在转载本文时,应当注明作者或出处,以示对作者的尊重,谢谢!
- 大小: 29.7 KB
- 大小: 45.3 KB
分享到:
相关推荐
特征: 完全基于最新的React钩子。 Redux商店已经有了传奇作为中间件。 对开发和生产版本的webpack完全控制。 随附所有eslint标准规则,以实现一致的代码库。 由于其原子设计,因此具有高度可扩展性。 具有懒惰放置...
秉承极简、极速、极致的开发理念,为开发集成了基于数据-角色的权限管理机制,集成多种灵活快速构建工具,可方便快速扩展的模块、插件、钩子、数据包。统一了模块、插件、钩子、数据包之间的版本和依赖关系,进一步...
React钩子使您可以制作组件屏幕截图并获得不同扩展名的图像。 安装 注意,该包具有peerDependencies : react和html2canvas 。 由于我们假设您已经安装了react ,因此只需安装html2canvas 。 要安装程序包,请运行...
DolpinPHP快速开发框架简介 ...DolphinPHP为大家提供了侧栏构建器,方便开发者把一些常用的设置,提示等放置在右侧,增强用户体验。 DolpinPHP快速开发框架页面展示 相关阅读 同类推荐:程序框架
秉承极简、极速、极致的开发理念,为开发集成了基于数据-角色的权限管理机制,集成多种灵活快速构建工具,可方便快速扩展的模块、插件、钩子、数据包。统一了模块、插件、钩子、数据包之间的版本和依赖关系,进一步...
前后台分离式开发(项目中也包含博客的后台管理系统),以便方便记录扩展开发过程,笔者将放置也一起放在同一个项目文件夹中。博客样式几乎最早antd这个优秀的UI框架,主打简约风格,是笔者插入了antd官方的风格所...
同时针对基于LSM框架的SELinux模型保护LKM安全的不足之处,提出了使用LSM扩展技术在init_module函数的前后位置各放置安全钩子函数的解决方案,从而防止已经躲过安全性检查链接进内核的恶意模块对诸如sys_call_table...
特征平铺WM,包括浮动窗口支持用纯python编写,简单小巧且可扩展使用yaml文件配置如果需要,包括python代码的钩子支持多个屏幕,并具有自动更新它使WM成为父项(因此可与Java一起使用) 包括异步主循环,因此没有小...
22.1.5 放置TDDGHintWindow 659 22.2 动态组件 659 22.2.1 走马灯组件 659 22.2.2 编写这个组件 659 22.2.3 在内存中的位图上输出 659 22.2.4 输出组件 661 22.2.5 使组件动起来 661 22.2.6 测试TddgMarquee组件 668...
完全基于最新的React钩子。 Redux商店已经有了传奇作为中间件。 对开发和生产版本的webpack进行完全控制。 随附所有eslint标准规则,以实现一致的代码库。 由于其原子设计,因此具有高度可扩展性。 具有懒惰放置...
1.2.4 数据库结构的灵活性和可扩展性 5 1.2.5 框架对设计和使用模式的扩充 5 1.3 历史回顾 5 1.3.1 Delphi 1 5 1.3.2 Delphi 2 6 1.3.3 Delphi 3 6 1.3.4 Delphi 4 7 1.3.5 Delphi 5 7 1.3.6 未来 7 1.4 Delphi 5的...
一种简单的方法是在每个ActionMailer :: Base祖先方法的末尾放置一些SentMailLog.create()。 看起来不是真的DRY,但是可以使用。 我通过ActionMailer API进行了查看,但没有像AR甚至ActionController那样的钩子。...
需要在论坛某些程序和模板中按放钩子代码,存在风险,请您慎重,本作者不对您的站点负任何 责任。安装前请备份好相关文件。 -------------------------------------------------------------------------------...
88 <br>0136 如何进行文本加密与解密 88 <br>0137 如何区别0、空字符串、Null、Empty和Nothing 89 <br>0138 从字符串中分离文件路径、文件名及扩展名 89 <br>0139 如何批量替换某一类字符串 89...