过程扩展与放置钩子

前面我们谈到了功能扩展对维护一个软件的巨大作用。实际上,正是因为功能在不断地扩展,才使得我们的很多软件质量在下降。因此,如何进行功能扩展,我们不得不察。每当新功能到来的时候,不用急急匆匆就开始编码,我们应当仔细思考我们的设计,即使是时间非常紧张的项目。用更多的时间去思考与设计,才会用更少的时间去做更简单的设计与编码。在这里,我提倡的是设计应当简单到发指,因为它体现的是一种精巧绝伦,它会使我们的思路更清晰,维护更简单,变更更容易。只有经过仔细的思考,才会做出精巧绝伦的设计,那是我们的目标。在这方面,“小步快跑”与“两顶帽子”的方法可以大大降低我们的设计难度,因为我们不是神,而是人。

上一章,我们用“抽取接口”的方法,替代万恶的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原则的要求。现在新需求来了,要绘制这么一个组合控件:

[img]http://dl2.iteye.com/upload/attachment/0095/4553/7ab043bb-c2c2-3388-988b-f0a37967af87.jpg[/img]

这个组合控件由四个下拉框组成,分别代表2个年度与2个月份,因此它在执行查询时,会提交到后台这4个数据。然而,我们希望这个控件在提交给查询模块时,应当是2个数据:某年某月的1日,和某年某月的最后一天。也就是说,该控件在提交参数给查询模块的时候需要进行一个参数转换,而不是直接传递给查询模块。
先看看我们现有的设计吧:当控件将参数提交给后台以后,控件会直接将参数传递给查询模块。但为了实现这样一个新需求,我们需要所有控件在这个地方硬生生插入一个数据转换的功能。如果真的这样修改了,整个系统就因小失大了。幸运的是,我们在这个地方有可扩展设计。
首先,我们在抽象的父类AbstractControl中加入一个非抽象方法transform():
[code]
/* (non-Javadoc)
* @see com...control.Control#transform(java.lang.String, java.util.Map)
*/
public void transform(String ctrlName, Map<String, Object> params){
//hook only
}
[/code]
然后我们创建新控件MonthRangeControl,重载transform()方法:
[code]
/* (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);
}
[/code]
整个设计修改了父类AbstractControl,增加了transform()方法,然后创建了新的控件类MonthRangeControl,不能说完全没有修改原程序,但已经在最大限度上满足了OCP原则。整个设计如图:

[align=center][img]http://dl2.iteye.com/upload/attachment/0095/4555/92eabfaa-d3c9-33b0-ad3c-b66e04636deb.jpg[/img][/align]

(续)

相关文档
[url=http://fangang.iteye.com/blog/1985399]遗留系统:IT攻城狮永远的痛[/url]
[url=http://fangang.iteye.com/blog/1986749]需求变更是罪恶之源吗? [/url]
[url=http://fangang.iteye.com/blog/1988080]系统重构是个什么玩意儿[/url]
[url=http://fangang.iteye.com/blog/1998147]我们应当改变我们的设计习惯[/url]
[url=http://fangang.iteye.com/blog/1999066]小步快跑是这样玩的(上)[/url]
[url=http://fangang.iteye.com/blog/1999068]小步快跑是这样玩的(下)[/url]
[url=http://fangang.iteye.com/blog/2002581]代码复用应该这样做(1)[/url]
[url=http://fangang.iteye.com/blog/2002582]代码复用应该这样做(2)[/url]
[url=http://fangang.iteye.com/blog/2002585]代码复用应该这样做(3)[/url]
[url=http://fangang.iteye.com/blog/2010722]做好代码复用不简单[/url]
[url=http://fangang.iteye.com/blog/2035360]软件可以这样功能扩展[/url]
[url=http://fangang.iteye.com/blog/2037879]过程扩展与放置钩子[/url]

特别说明:希望网友们在转载本文时,应当注明作者或出处,以示对作者的尊重,谢谢!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值