UML软件工程组织

软件质量之路(4):建立核心框架
林星 (iamlinx@21cn.com) 来源:IBM
框架是一种特殊的软件,它为软件开发带来了高度的重用性,但同时它也需要高技巧的设计。软件组织使用框架来积累知识,对推动软件开发是一种有效的方式,但前提是你能够了解如何设计一个框架。

什么是框架
在软件开发中出现过各种各样的框架,开源软件的兴起,使得各种各样的框架纷纷出现,例如,Apache组织下就拥有诸多的框架类产品,包括国内很熟悉的struts。那么,什么是框架呢?

在设计模式中,Gamma等人为框架给出了一个定义:"框架就是一组协同工作的类,它们为特定类型的软件构筑了一个可重用的设计。"[Gamma 94,p.26]

框架是针对特定的问题领域的,例如,struts是一个针对Web开发的框架。

框架包括了一组的抽象概念。这些抽象概念来源于问题领域。例如,struts是基于MVC模式进行设计的,所以它必须为model、view、control建立抽象概念。

框架使得这些抽象概念相互协作,并提供了一种扩展的形式,以实现重用。这是框架的具体工作。框架在抽象概念上进行工作,定义抽象概念之间的协作方式。框架和普通软件或是类库的区别就在于,用户通过扩展框架来重用该框架。这些扩展点的设计称为框架设计的核心。例如,在struts1.0中,采用的是类继承的扩展方式(对Action进行扩展)。

框架实例
spring(http://www.springframework.org)是一个开源的框架,这个框架定位于一个整体的基于J2EE的应用型框架。他的定位哲学是不重新发明轮子。例如,他提供了数据库访问机制,但是他的数据访问机制是基于JDBC、Hibernate和JDO的。重用和再包装的思路充满了整个spring框架。国内的软件产业大都定位在系统集成,这种思路非常适合于国内的软件组织。使用这个例子的目的也在于此。

框架应该尽可能使用现有的技术。而不要重复投资。

JDBC是一个非常基础的数据存取API。它封装了对关系型数据库的访问,但是应该承认,JDBC仍然属于层次较低的API,所以在使用的时候,我们不得不编写大量的代码,来完成一件简单的工作:


PreparedStatement st = db.prepareStatement("SELECT USER.NAME FROM USER");
ResultSet rs = st.executeQuery();
while(rs.next()) {
     System.out.println(rs.getString(1));
}
rs.close();
st.close();

看到有多麻烦了吧,但是请注意,以上的代码存在严重的缺陷,因为没有任何处理异常的代码,加入异常代码意味着我们还需要增加一些代码,这种重复的劳动使得程序员的工作像是傻瓜一样。记得我们在代码自动化中的自动化原则吗?对于重复性的劳动,我们应该使其自动化。

如何进行呢?注意,我们发现,除了SQL语句的不同以及记录集的处理不同以外,大部分的查询代码都没有太大的差别,所以我们的目标就是抽取共同的部分,而把特定的部分留给开发人员自己。

那么,一个框架该做些什么呢?回忆框架的定义,框架就是定义一组的抽象体,及其抽象体之间的协作,并提供扩展。在JDBC中,抽象体有Datasource、Connection 、PreparedStatement、ResultSet、Statement、SQLException。所以,建立框架的第一步就是分析这些抽象体的行为,哪些是共同的,哪些是专有的。使用spring框架,最终的JDBC客户端代码是这样的:


JdbcTemplate template = new JdbcTemplate(dataSource);
final List names = new LinkedList();
template.query("SELECT USER.NAME FROM USER",
	new RowCallbackHandler() {
		public void processRow(ResultSet rs) throws SQLException {
			names.add(rs.getString(1));
		}
	});

首先,代码创建了一个JdbcTemplate实例,这是核心的JDBC的封装器,我们在后面可以看到它的部分实现。然后,通过给JdbcTemplate的query方法传入一个sql语句和一个回调的匿名类来完成填充names的操作。可以看到,客户端需要做的事情包括获得一个Datasource、提供一个sql Statement、以及一个具体的处理方法。这些动作每个客户端都不相同,但是对于Connection、PreparedStatement、Statement、SQLException的处理,基本上都是类似的。接下来我们就看看JdbcTemplate内部的做法:


		public void query(String sql, RowCallbackHandler callbackHandler) throws DataAccessException {
		Connection con = null;
		PreparedStatement ps = null;
		ResultSet rs = null;
		try {
			con = DataSourceUtils.getConnection(this.dataSource);	
			ps = con.prepareStatement(sql);
			rs = ps.executeQuery();

			while (rs.next()) {
				callbackHandler.processRow(rs);		
			}
			
			rs.close();
			ps.close();

		}
		catch (SQLException ex) {
			throw this.exceptionTranslater.translate("JdbcTemplate.query(sql)", sql, ex);	
		}
		finally {
			DataSourceUtils.closeConnectionIfNecessary(this.dataSource, con);	
		}
	}

首先(1处),代码利用DataSourceUtils从dataSource中获得一个可用的连接,然后使用PrepareStatement的方式处理查询语句。2处是整段程序的精华,代码使用了一个回调接口,把从数据库中取出的记录集交给回调接口来处理。我们把这里的代码和前面的客户端代码结合起来看,就能够明白它的思路。3处用于处理异常,将SQLException转义为更具有意义的异常,即DataAccessException的子类,因为单单靠一个SQLException来表示复杂的数据库操作异常未免过于简单了。最后(4处),不管执行的结果如何,都关闭连接。

应该说,这一段代码并不难理解。但它充分表现了框架的工作方式和以下我们将谈到的框架的意义。

框架对软件开发的意义

知识积累

框架的核心价值是对知识的积累。软件开发是一项知识性的活动。但是知识存在于人的大脑中,是最难进行积累的。而在软件开发中,代码是最确定的知识,人和机器通过浏览代码都能够了解代码的目的,而且不会出现不同的理解。所以,从代码出发进行知识的积累是最佳的办法。框架就是这种思路的产出物。框架包含了大量的代码,这些代码是对某个特定问题领域中抽象概念及这些抽象概念之间关系的描述。所以,框架能够胜任知识积累的工作。

虽然代码是框架的核心,但是光有代码的框架是很难为人所理解的。代码的层次太低,开发人员从代码的角度来完全的理解框架是很困难的。所以,必须要有层次高于代码的工件。这些工件可以是设计文档、领域模型、UML图、JavaDoc。他们的目的都是为了帮助框架的开发人员和使用人员顺利的理解框架。

就像我们上面的实例中,通过一个JdbcTemplate对象,就将一个JDBC的最佳实践给积累起来了。当然,你也可以使用文档要求开发人员按照某种方式来使用JDBC,但在实践中会遇到不少的问题,例如,JDBC的异常处理可能过于繁琐而被忽略。虽然大多数时候都不会有问题,但是当问题发生时往往会很麻烦。采用框架的方式则不会有类似的问题。

资产的保护

知识积累本身就是一项对资产的保护工作。而另一项很重要的保护工作就是软件组织(尤其是企业)需要保证对知识的学习和改进是经过合法授权的。例如,知识的非法外流是任何组织都不希望看到的。将知识积累为框架的形式有助于缓解这种情况。框架可以是以源码形式发布的,也可以是以库形式发布的,为不同的框架用户选择不同的发布形式,可以起到权限控制的作用。

鼓励重用

框架之所以称为框架,是因为它可以重用。在软件组织中形成以框架为核心的开发方式,在开发中使用框架,并在开发完成后改进框架。在这个反覆的过程中,重用的工作就已经开展起来了。

重用其实并没有那么困难。就像上面的例子中,其实代码并不难,思路也很清晰,其实就是将通用的行为抽取出来。

优化架构

框架代表了一种优秀的软件架构。框架定义了扩展方式,从而规范了框架的使用行为。这使得软件能够保持整体架构的稳定性和一致性。

在上面的例子中,使用框架之后,客户端可以节省大量的代码,代码结构会更加清晰。

大规模软件设计

大规模的软件设计的关键在于对应用进行合理的划分,并提供一种一致的方式建立架构。大规模的软件设计要求核心的设计人员工作在一个抽象的层次上。虽然他们属于设计人员,但是同样需要编写代码,而这些代码则是框架代码。

在敏捷方法中,设计师的职业决不意味着你仅仅只需要编写设计文档,如果你常常阅读一些规范的话,你会发现,很多的规范是采用代码编写而成的。只不过这些代码并不提供实现,只提供了抽象接口。

如何进行有效的框架设计
一个好的框架设计是有一定的准则可供遵循的。以下给出的一些概念奠定了框架开发的理论基础。

设计抽象层次。

在框架的定义中,抽象体是至关重要的。抽象体的定义取决于框架的目标。没有目标的框架决不是一个框架,要么是一个类库,要么是一种编程语言。在上面的例子中,首先是有了一个简化JDBC操作的目标,然后从这个目标出发定义抽象体。于是我们得到了Connection 、ResultSet、Statement等抽象体。

在抽象层次中规范行为。光有抽象体还是没有办法工作。还需要定义出抽象体的行为。在上例中,我们定义了获取数据集的行为。但是在JDBC中,除了获取数据集,可能还需要将数据填充到值对象中,还需要能够支持CRUD的所有操作。这些都是抽象的行为。有了这些行为之后,我们就需要规范、穷尽这些行为。

分析抽象行为的通用部分和非通用部分。在抽象体的行为中,有些动作是通用的,有些是特殊的。前者就是框架要实现的部分。而后者则作为扩展留给用户。

将抽象层次提取为框架,并设计扩展点。有了抽象体、抽象体的通用行为之后,就可以设计扩展点了。最简单的扩展点是采用方法调用的方式,复杂的可能通过设计模式或是配置文件等方式。扩展点设计优劣的评价标准是使用起来是否方便,这里的使用包括应用、调试、测试等。

适当的使用设计模式。设计模式代表了先进的软件设计思路。在框架中适当的使用设计模式有助于改进框架的结构。例如在上例中,扩展点的设计就采用了回调的设计模式。在框架设计中不宜采用过多的设计模式,这会使得框架理解起来困难。当然也有反例。

反例-Junit Framework。作为自动化单元测试框架,Junit在简小的设计中采用了大量的设计模式,包括Command模式、Template Method模式、Collecting Parameter模式、Pluggable Selector模式、Adapter模式、Composite模式等。

简化框架的使用。在上例中,框架的扩展点设计完毕后,使用框架的代码仍然比较复杂,回调接口和匿名类仍然会把人弄的有些莫名其妙。所以,为了使框架使用方便,一定程度的简化还是需要的。

在Junit框架中,我们只需要让测试方法以test开头就能够自动进行测试,而这种功能是Command模式无法提供的。按照Command模式的要求,一个测试类只能包括一个测试方法。原因就在于Junit中利用了Pluggable Selector模式、Adapter模式、以及反射技术对Command模式作了新的封装:


protected void runTest() throws Throwable { 
    Method runMethod= null; 
    try { 
        runMethod= getClass().getMethod(fName, new Class[0]); 
    } catch (NoSuchMethodException e) { 
        assert("Method \""+fName+"\" not found", false); 
    } 
    try { 
        runMethod.invoke(this, new Class[0]); 
    } 
    // catch InvocationTargetException and IllegalAccessException 
}

隔离第三方技术。

当前的软件开发向着协作的方向发展。在这种情况下,大量的第三方软件出现了。软件业的分工将会给软件业带来繁荣,但是对于软件组织来说,就需要考虑第三方软件的成本、生命力、本组织系统对其的依赖程度等问题。这部分的工作应该交给框架。让框架来负责把核心应用和第三方技术隔离开来。例如,作为企业应用的开发者,我发现在数据库层次上的变化实在是太大了,新的xquery查询语言、ORM技术,这些都使得应用系统需要不断的变化。这无疑给应用系统增加了风险。因此,我决定设计一个抽象的层次,把这些技术和核心应用隔离起来。抽象层次只负责向数据库询问符合某种条件的数据,至于这个查询采用sql还是xquery来处理那都没有关系。因此技术细节被隔离了。

另一个实例
上文讨论的例子是一个简单的例子,主要是从代码层面来讲述框架的概念。但是为了完全理解框架的威力,我们需要从更高的层次来看待框架的概念。我们选择的例子是Eclipse。从IBM向开放源码界捐赠Eclipse以来,Eclipse迅速成为一种非常优秀的集成开发工具(它绝对不是简单的Java IDE),Eclipse缘何成功呢?关键在于Eclipse的设计理念和根据设计理念发展起来的底层支持框架。

在软件开发中,需要各种各样的技能,需要各种各样的开发工具,但是工具之间如果彼此不能够相互交互,那么开发流程就难以连贯。Eclipse的目标就是要解决这个问题。

Eclipse平台的价值在于它的促进作用:根据插件模型来快速开发集成功能部件。

那么怎么做呢?我们来看Eclipse自己对这个问题的描述:

Eclipse 的核心是动态发现插件的体系结构。平台负责处理基本环境的后台工作,并提供标准的用户导航模型。于是每个插件可以专注于执行少量的任务。有哪些类型的任务?定义、测试、制作动画、发布、编译、调试、图解等等,只要您想象得到的应有尽有。

Eclipse的平台运行环境是框架的支持部分,而平台中涉及的Workbench、Help、Team、Workspace都属于抽象体,而插头表示系统的扩展点(称为hook)。

这里重点要讨论的是Eclipse的定位哲学。Eclipse作为一个集成开发工具,他希望能够将各种各样的开发工具集中到一个平台下,但是所有的事情都由一个组织来做是不可能的,最好的方式就是建立一种组织方式,能够将不同技术提供商的技术集成起来。所以他的定位哲学就是提供做事的方法,而不做具体的事情。

这种思路在优秀的软件设计中是很常见的,另一个优秀的范例是Ant。由于篇幅所限,我们就不进行深入讨论了。

所以Eclipse只是提供针对IDE环境、Java对象模型提供了一组的抽象体,插件的开发者可以根据自己的需要,设计自己的插件,通过扩展点接入到Eclipse。Eclipse是一个成功的软件,也是一个成功的框架。

深入了解
developerWorks 上的 Eclipse 平台入门一文提供了Eclipse的相关信息。

框架过程模式是一本讨论框架设计过程的书籍,您可以从中了解到框架的开发过程和普通软件的开发过程的区别。

关于作者
林星,致力于研究敏捷理论和优秀的软件设计思想,并将之应用于国内的软件组织。可以通过 iamlinx@21cn.com 和他联系,也可以通过访问 www.qca.cn 和 www. aglichina.org 来获得更多的信息。

版权所有:UML软件工程组织