如何开发一个可复用的软件系统

如何创建一个可复用软件系统

**译者序 : ** 本文是设计模式“ ** 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 文件是因为他可以包含一个可分级的信息)。你已经编写了一个可配置的钩子组件,你为了测试不同的情形而不得不改变它,你将只需要修改这个配置文件。这样的修改对开发者是很容易的,对测试者也有很显著的用处,他们都不用修改代码。

你的可配置钩子允许

Published At
Categories with Web编程
Tagged with
comments powered by Disqus