如何创建一个可复用软件系统
译者序:本文是设计模式“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: " & _
objUser.UserID & ", Step: " & vParam3 )
'...do whatever processing is required...
sHookId = HOOK_OK
Case else
'Unknown hook, possibly introduced to product
' after this component was written.
'Just allow it, but do nothing.
Call WriteLog( sHookId, "Unrecognised Hook" )
sReturnValue = HOOK_OK
End Select
CallIn = sReturnValue
Exit Function
CallIn_Handler:
'Handle the error...
End Function
Sub WriteLog( sHookID as String, sText as String )
Dim sFullText as String
sFullText = Time$ & " " & sHookID & " " & sText
Debug.Print sFullText
'Log to file used mainly for testing
'and for diagnosing production systems
If IsFileLoggingOn(sHookID) then
'Output log info to text file...
End If
End Sub (to be continue )
你也可以使用你熟悉的com 实现这个功能,他们通过分派调用方法的技术很相似
你在发行新版本的核心产品时必须保证不需要重新编译已经存在的钩子组件,所以钩子组件应该忽视所有没有被公认的钩子标志符而且仅仅返回一个简单的“成功”代码(参考代码列表的case else 部分),通过这个方法,钩子组件只需为功能需求执行被认可的钩子调用,如果日后需要为核心产品增加新的钩子调用点时,既有的钩子组件不需要改变任何行为就可以增加新的钩子标志符
在“下一步”的例子中,核心产品可能在几个执行点调用钩子,例如:当用户请求下一步操作然后选择步骤名称再次请求,这两个例子将会是:
Hook
Param1
Param2
Param3
Param4
Next step Started
"NS-START"
objUser
objWorkitem
""
Next step Step Selected
"NS-SELECTED"
objUser
objWorkitem
sStepName
这样简单的一个例子,三个变量(加上钩子标识符)可能就已经足够了,可是为了避免了将
来接口改变带来的麻烦,你总是不得不增加更多的函数,列表1显示了一个核心产品的简单
代码:一个钩子组件,一个钩子的调用实例
为了执行客户三个复选框的功能,你应该从钩子组件项目中添加一个复选框窗体,并且通过
下面代码调用:
Case NEXTSTEP_SELECTED
'...Cast variables and Call WriteLog()...
'Show the checklist
frmChecklist.Show vbModal
If frmCheckList.AllRequiredItemsTicked Then
sReturnValue = HOOK_OK
Else
MsgBox "You have not ticked all the " & _
"required items. " & _
"Next Step cannot continue", vbExclamation
Call WriteLog( sHookId, "Cancel sent back -- " & _
"all required items not ticked" )
sReturnValue = HOOK_CANCEL
End If
' ... more code
钩子组件影响核心产品:
核心产品为可能发生变化的需求设置点,然后将对他们的控制委托给钩子组件,接着响应钩
子组件返回的状态,
数据对象引用
当钩子组件接收到一个对数据对象的引用时,核心产品需要捕获钩子组件引起了什么变化,
如果你不想钩子组件修改一个对象的参数,可以通过使用一个宣传对象或者锁对象,或者一
个在调用方法前设置部分特性的对象,如果你允许改变,对象可以验证他们,或者记录他们,
当方法返回时通过核心产品验证他们,例如用户需要用两个钩子更新一个有引用关键字的工
作项目
Case NEXTSTEP_SELECTED
'...cast variables and call WriteLog()...
sRefKey = GetMainframeKey(objWorkitem.WorkitemID)
objWorkitem.Reference = sRefKey
sReturnValue = HOOK_OK
然后核心产品可以查询返回的工作项目对象:
mobjCurrentItem.ResetChangedFlag
sHookResponse = _
mobjHookComponent.CallIn(NEXTSTEP_SELECTED, _
mobjCurrentUser, mobjCurrentItem, sSelectedStep)
If mobjCurrentItem.Changed then
'Take appropriate action, such as validating
'and/or saving the changes...
End If
逻辑控制
钩子组件可以返回影响核心产品逻辑的值,这种执行方法是很有限的,例如:可以指定
一组钩子组件返回的标志(取消当前操作,显示标准对话框),否则你的核心产品和钩子组
件就会产生紧耦合危险。通过预定义一组客户之间不需要改表的标志,你就可以为核心产品
捕获任何可能返回的值
使用交互
在一个基于windows 窗体的典型应用程序中,钩子组件可以用vb窗体发行,当用户需要时
交互调用
对于web应用程序,核心产品和钩子组件需要在服务器上运行,所以vb 窗体就不适用了,
不过你可以用下面的几种方法 :
通过为核心产品返回一个url ,来调用一个新的window浏览器,将一个调用浏览窗口的
Url 返回给核心产品,所有从这个窗口的后继请求都能被非核心调用。因为这个窗口是非模
式化的,所以用户很清楚的在核心产品屏幕和钩子窗口间进行嵌套
第一步操作的一个变量用一个钩子的url内容临时替换核心产品的浏览器,这是一个很
有效的模式接口,通过屏幕的请求可以被定制的组件捕获,当控制被返回到核心产品时,控
制可以存储自己的屏幕
返回一个html 或者xml 给核心产品,可以合并成正在创建的页面,这使得钩子接口通
过使用核心产品而变得更好,然而核心产品不得不将请求从屏幕的钩子部分转化为相应的定
制组件
实践:
二进制兼容性确保了你的核心产品和钩子组件总是使用相同的接口,可以在办公室特定
的电脑上记录你创建的核心产品,项目小组也经常在站点上创建钩子组件,使任何计算机
都变得可用。这样有时候可以引起二进制很难修复的问题,如果你这样运行,要尽量同步
这两个创建环境,尽量包含服务组件。如果还是不能正常工作,也可以放弃二进制兼容性,
使用更缓慢的后期绑定作为最后的手段
测试:
钩子组件提供很有用的调试和测试帮助,就像演示过的那样,你的基本钩子组件分之应该要记录所有的调用和参数,响应每个钩子的典型方法是为每一个客户定制代码,然而即使是一个内部使用,编写一个可配置的钩子组件也是值得的。组件的行为不是硬编码的,它的响应是通过list2配置文件控制的(使用xml 文件是因为他可以包含一个可分级的信息)。你已经编写了一个可配置的钩子组件,你为了测试不同的情形而不得不改变它,你将只需要修改这个配置文件。这样的修改对开发者是很容易的,对测试者也有很显著的用处,他们都不用修改代码。
你的可配置钩子允许你可以为每一个钩子标志符指定不同的返回值,通过为每一个基本用户定义行为的例子精确到了一比特。我还添加了一个暂停设置和用于显示的消息框,这对模拟延迟是很用的,
最后我还添加了一个解释钩子组件如何改变传入参数的设置(请看代码列表2)
发布:
如果没有除了日志记录以外的功能,一定要记住你必须至少提交和核心产品一致的钩子组件分支,因为你的核心产品需要能够调用执行它
更多的参考:
这个钩子概念是基于”模板”设计模式的,如果你没有听过这个设计模式,你应该读“设计模式”这本书,他们假设你使用的是一个有完全继承功能的语言,虽然这不是必要的。通过vb.net 我们可以实现,vb60通过一点点努力也可以实现这个目的如果你感觉这个文章有点难,你可以从Stamatakis 编写的“vb设计模式”开始,虽然它没有覆盖所有的模式,但是他对于vb开发者来说是一个很好的开始
如何开发一个可复用的软件系统
80酷酷网 80kuku.com