如何创建一个可复用软件系统
译者序:本文是设计模式“Template Method”模板方法的一个延伸,将模板应用到了整个软件开发产品。首先将软件产品核心不变的业务逻辑部分抽象出来,对于在不同产品中的不同的部分,核心产品通过钩子调用钩子组件重的具体实现,这样开发不同的系统时,只要更改钩子组件的内容就实现了不同的产品。开发的关键就是抽象核心产品的功能。当然这种开发思想也是局限的,不是适合所有的开发项目(抽象所有项目的核心是没有意义的),这种开发思路比较适合针对于一个领域的产品开发,同一个领域抽象出来的核心产品才有实际的使用价值。
创建可复用的组件是学习如何创建可复用程序的第一步――――by Mike Cahn
如果你连续的开发几个软件项目,将会发现自己编写了许多重复功能的代码,当你认识到这点,你肯定会产生复用代码的想法。事实上很多开发团队都建立了自己的代码库和组建库以方便的将他们应用到新的项目中。下面我将讲述如何将复用提高到一个更高的级别:复用整个应用程序
如果你开发一个particular business函数或者specific vertical market,你就会发现客户的需求之间存在着很多重复使用核心代码的组件的重叠,但是需求之间的不同点却要求你不得不为每一个客户开发一个全新的系统
尤其不幸的是重写会增加设计和开发时间还有更复杂的维护,例如如果你存在多个版本,你就会面对这样的问题:传播一个bug修复、产品增加功能要求为每一个客户指定更改和测试,复用组建帮助等!但是如果所有应用程序的核心逻辑是相同的,那为什么不更好的复用这部分核心逻辑呢?
答案是你当然可以做到,开发一个核心产品,然后在不同的功能层之间你都可以使用它而没有必要改变核心代码
什么是钩子?
一个钩子就是你设置客户实例化代码的地方,它是一个丛核心产品调用到定制组件的方法,一个客户化层(钩子组件)需要知道正在发生什么的地方,或者需要修改核心产品行为的地方,你的核心产品都可以使用钩子调用
典型的应用是:一个钩子方法通过数据和钩子组件当前的上下文关联性进行调用,钩子组件处理这个调用以返回正确的信息
当设计核心产品和钩子组件时,你要让它们尽量保持松耦合关系,将来如果核心产品发生变化不会影响已经配置的钩子组件。你也要必须保证在配置钩子组件时,核心产品增加计划范围之外新的钩子调用后 钩子组件也能执行正确的事件
确定核心功能:
使用钩子创建一个核心应用程序时,评定的第一步就是确定哪些东西将要作为核心来处理,如果核心产品设置了太少的功能,那么你将不得不针对每一个项目重复执行通用的功能,这样增加了很大的工作量!重复的代码操作破坏了核心应用程序开发的目的。但是如果包括了不是所有客户都有的通用功能将是更糟糕事情,所有开发项目在使用核心代码时都会产生很多问题,最后你将不得不修改核心产品
核心产品后期增加可移植的功能比从中移除本来就不应该加入的功能要有益的多,不过这样可能会产生过于保守的错误!能够适合核心产品的功能性能够作为一个可以共享的组件产生,当你做好准备时你也可以移植它
确定核心应用程序功能性(functionality)最关键的是定义个一个所有客户都通用的程序流,让我们来看一个例子,假设你正在开发三个系统,它们都有一个基于移动工作项目的工作流,在这三个系统中用户都可以通过执行“下一步”,从一个工作项目执行到下一个工作项目,下面是这三个系统的详细不同点:
系统1:这个系统仅仅是移动工作项目到下一步,但是不执行任何操作
系统2:系统在一个授权(legacy)系统中记录工作项目和操作步骤的详细信息,同时写入一个从授权(legacy)的系统到工作项目内部的引用关键字
系统3:系统在工作项目移动到下一步之前显示一个用于用户更新的复选框,如果用户标志的所有必须的操作没有成功的完成时,系统撤销操作“下一步”操作
图表1显示了功能化是如何在核心产品和三个不同的客户化层之间划分的,在这个例子中
客户化层需要下面的能力:
使用和更新核心产品对象(在这个例子中指工作项目)
用使用者进行交互
改变核心产品的工作流(比如撤销当前操作)
连接其他应用程序或者组件
这些需求是很典型的,你要在你的核心产品的所有函数中考虑到
构建钩子接口:
在钩子组件内部设置的参数依赖上下文改变,要比维护一堆钩子接口强的多,所以创建一个通用的钩子接口是较好的解决方案. 使用变量集合或者参数数组适应你将加入的不同数据类型,在第一个参数中存储钩子的标志符,钩子组件工作的第一件事就是解释这个标志符然后调用相应的函数或者组件,下面的代码通过使用select …..case 结构实现了这一功能:
列表1:核心产品调用钩子组件(一个很小的钩子组件)
'Constants would normally be defined in a module or class
Const NEXTSTEP_START = "NS-START"
Const NEXTSTEP_SELECTED = "NS-SELECTED"
Const HOOK_OK = "OK"
Const HOOK_CANCEL = "CANCEL"
'...
'User just selected Next Step operation
sHookResponse = mobjHookComponent.CallIn(NEXTSTEP_START, _
mobjCurrentItem, mobjCurrentUser, "")
...
'User just selected the Step to move the item to
sHookResponse = mobjHookComponent.CallIn(NEXTSTEP_SELECTED, _
mobjCurrentItem, mobjCurrentUser, sSelectedStep)
Hook component Code:
'Note: Hook constants file would need to be included
Function CallIn(ByVal sHookId as String, vParam1 as Variant, _
vParam2 as Variant, vParam3 as Variant) as String
Dim sReturnValue as String
Dim objWorkitem as clsWorkitem
Dim objUser as clsUser
Dim sSelectedStep as String
On Error Goto CallIn_Handler
Select Case sHookId
Case NEXTSTEP_START
'Processing to be done when NextStep is first invoked
Set objWorkitem = vParam1
Set objUser = vParam2
Call WriteLog( sHookId, "Item ID: " & _
objWorkitem.WorkitemID & ", User ID: " & _
objUser.UserID )
sHookId = HOOK_OK
Case NEXTSTEP_SELECTED
Set objWorkitem = vParam1
Set objUser = vParam2
sSelectedStep = vParam3
Call WriteLog( sHookId, "Item ID: " & _
objWorkitem.WorkitemID & ", User ID: " & _
obj