2007-01-16

如何在struts+spring+hibernate的框架下构建低耦合高内聚的软件

关键字: struts spring hibernate 软件工程 敏捷开发
 
问题的提出

我常常在思考一个问题,我们如何能设计出高水平、高质量的软件出来。怎样是高水平、高质量的软件?它应当是易于维护、易于适应变更、可重用性好的一个系统。如何做到这一点呢?答案当然是“低耦合、高内聚”了。低耦合就是软件在构造的时候,各个模块、各个功能、各个类都不会过度依赖于它周围的环境。只有这样,才能使我们的模块(功能、类)在周围发生变更时不受影响,做到易于维护和易于适应变更。正因为如此,也使它更易于重用到其它功能类似的环境中,提高了重用性。高内聚则使软件中的各个模块(功能、类)能够各尽其能而又充分合作,也就是对于软件问题空间中需求的各个功能,系统可以合理地把它分配给各个模块(功能、类)来共同完成,而不是一个或几个八面玲珑、包打天下的超级类一个人完成。而对于该系统中的某一个模块(功能、类),具有自己高度相关的职责,即该职责中的几个任务是高度相关的。每一个模块(功能、类)都决不去完成与自己无关职责的任务。

那么怎样能构造一个低耦合、高内聚的系统能,时下最流行的框架结构之一的struts+spring+hibernate为我们提供了方便。使用struts我们可以应用MVC模型,使页面展现与业务逻辑分离,做到了页面展现与业务逻辑的低耦合。当我们的页面展现需要变更时,我们只需要修改我们的页面,而不影响我们的业务逻辑;同样,我们的业务逻辑需要变更的时候,我们只需要修改我们的java程序,与我们的页面无关。使用spring我们运用IoC(反向控制),降低了业务逻辑中各个类的相互依赖。假如类A因为需要功能F而调用类B,在通常的情况下类A需要引用类B,因而类A就依赖于类B了,也就是说当类B不存在的时候类A就无法使用了。使用了IoC,类A调用的仅仅是实现了功能F的接口的某个类,这个类可能是类B,也可能是另一个类C,由spring的配置文件来决定。这样,类A就不再依赖于类B了,耦合度降低,重用性提高了。使用hibernate则是使我们的业务逻辑与数据持久化分离,也就是与将数据存储到数据库的操作分离。我们在业务逻辑中只需要将数据放到值对象中,然后交给hibernate,或者从hibernate那里得到值对象。至于用OracleMySQL还是SQL Server,如何执行的操作,与我无关。

然而我要说的是,即使我们使用了struts+spring+hibernate框架构建我们的软件,就可以做到“低耦合、高内聚”了吗?我认为这是远远不够的!我认为我们在使用struts+spring+hibernate框架的时候常常会有以下几个问题值得改进。

 
分析与决策

1.       编写DAO的时候不要直接去使用hibernatespringhibernate的支持。

现在我们在编写DAO的时候普遍都是直接继承springhibernate的封装类HibernateDaoSupport,然后使用该类提供的诸如saveOrUpdate(), saveOrUpdateCopy(), find()等等。另外,在使用excute()方法实现一些更复杂的hibernate功能的时候还会使用hibernate的类,诸如Query, Session, Type等。这样直接使用springhibernate的类存在的问题在于,你的代码将不得不依赖与springhibernate的某个版本。比如说,现在hibernate3出来了,改动挺大,实际上最要命的是包结构,hibernate2的包结构是net.sf.hibernate.*,然而hibernate3org.hibernate.*。同样,spring为了支持hibernate3,包名也改为org.springframework.orm.hibernate3.*。假如,你现在新开发一个项目,这没什么关系,如果是升级一个项目问题就来了。如果你希望将你的一个项目从hibernate2升级为hibernate3,你不得不修改DAO中所有对hibernatespring-hibernate的引用。如果你的代码中出现hibernate2hibernate3不兼容的方法和类,比如saveOrUpdateCopy()(在hibernate3中已经没有了),你还将不得不改写。那么你可能会说,我不会这样升级。如果你的软件生命周期有好多年,hibernate升级到4,升级到5,你还是依然使用hibernate2?如果你以这种方式开发一个平台,你能要求所有使用你平台的软件项目都只能使用hibernate2?更进一步说,我现在开发一个产品,今后的客户将是成千上万。经过12年我需要升级了,这时我的升级包有几十M,几乎把所有的DAO都换了个遍,这样的升级无异于重装。也许,有人会提出另一个方案,在HibernateDaoSupportDAO中间增加了一个基础类,这样将基础类中的org.springframework.orm.hibernate.support.HibernateDaoSupport,改为了org.springframework.orm.hibernate3.support.HibernateDaoSupport,这样其下面继承的DAO就不用改动了。然而在源码上是小小的改动,但对于类来说,两个不同版本的HibernateDaoSupport其相关的属性和方法还是有不少变化,那么在基础类重新编译的同时,你的继承类重新编译否。既然已经重新编译了,因此你的所有DAO在升级的时候依然要打入升级包,问题依然存在。以上问题,究其原因,是我们项目中的DAO依赖于hibernatespring,因为我们对它们的使用是继承,是一种很强的关联,就是一种依赖。我们只需要稍微进行一些调整,就可以解决这个问题,那就是不使用直接继承,而使用接口进行分离。可以使用Façade模式,先建立一个叫BasicDao的基础类,从名称我们可以看出,它是所有DAO的基础类,实现DAO操作所需的所有诸如save()delete()load()query()等方法,除了一些基本的方法,诸如翻页查询、getCount、解析查询条件形成HQL语句等功能也在这里实现,但是不要使用与hibernatespring有关的任何方法和类。同时,BasicDao调用一个叫DaoSupport的接口,DaoSupport的接口则是提供持久化所需的基本方法,最原始的元素。然后,我为DaoSupport接口提供各种不同的实现,比如hibernate2的实现DaoSupportHibernateImphibernate3的实现DaoSupportHibernate3Imp,整个结构如下图所示。BasicDao可以使用hibernatespring提供的方法,但是不是直接使用,而是通过调用DaoSupport的实现类来使用。然而BasicDao到底是使用的那个实现类,我们通过springIoC,通过配置文件来决定到底使用哪个实现。同时,BasicDao也不要使用诸如SpringContext的类来实现IoC,而是通过建立setDaoSupport()getDaoSupport()方法,然后在spring配置文件中建立引用。

2.       编写Action的时候不要直接使用springspring的继承类

前面我说了应当避免DAO引用springhibernate及其继承类。同样的事情也发生在Action中。由于Action通常不纳入spring的管理,因此Action在通过spring调用某个BUS的时候,往往是去引用一个叫SpringContext的类(spring的类ContextLoaderServlet的继承类),然后使用它的getBean()方法。如此的使用,我们的Action将依赖与spring。我们同样可以使用一个叫BasicAction的父类,然后用一个接口来隔离spring。由于Action通常不纳入spring的管理,我们通过一个*.property的配置文件来决定接口到底调用哪个实现类。这样的结构的另一个好处是,我们还可以将所有Action都必须使用的诸如写日志、用户校验、异常处理都放在父类BasicAction中,提高系统的可维护性。 

3.       BUS需要获取别的模块的数据的时候,不要直接去使用该模块的DAO

我举一个简单的例子:我需要设计一个软件评审的管理软件,该软件分为评审组织者制订评审计划、评审者分别填写评审表后由评审组织者汇总评审表、评审组织者制作评审报告。这是一个非常简单的项目,分成了三个人来完成。但是项目进行快结束的时候却出现了问题。填写评审表需要获得评审计划中的一些数据,制作评审报告的数据来源于评审表。项目组在开始编程前先开了一次会,大家约定好了各个部分的数据格式及其规则,然后开始工作。然而数天后项目组把各个模块整合以后发现,系统根本跑不起来,为什么呢?设计评审计划的人发现,所有评审计划应当按照产品编号来进行管理而不是项目编号。由于这个变更,填写评审表模块在待评审列表中什么都无法显示;同样,设计评审表的人发现,在一个评审计划中评审表与评审者不是一对多的关系,而是一对一的关系,因而修改了这两个表的关联。因为这样,在制作评审报告时就不能正确得到评审表数据。其实一个软件项目在整个进行过程中总是不断变更。我们需要做的不是去抑制这些变更,而应当是通过软件的结构去适应这些变更,即是降低各模块间的依赖(耦合),提高内聚。拿这个实例来说,当评审表需要调用评审计划的数据的时候,不应当是自己写一个DAO去调用评审计划的数据,而应当是调用评审计划的接口,将这个任务交给评审计划类来完成。当评审报告需要调用评审表的数据的时候,同样应当去调用评审表的接口,由评审表来实现。同时,这种调用应当是去调用BUS层的接口。为什么呢?比如在评审计划中的一个业务逻辑是只有在评审计划发布以后才能制作评审表,那么怎样才是已发布的评审计划呢?这个业务逻辑应当由谁来定义?当然是评审计划。在什么地方定义?当然是BUS而不是DAO,因为DAO仅仅是实现数据的持久化,而BUS才是实现业务逻辑的地方。既然如此,如果评审表去调用评审计划的DAO,那么已发布评审计划的业务逻辑必然包含在了评审表的业务逻辑里了。我们假设有一天,已发布评审计划的业务逻辑发生变更了(实际上这样的会在你毫不经意间就发生了),编写评审计划的人会很快就修改了评审计划的业务实现并且测试通过了。他不知道评审表里也包含了这样的业务逻辑,因而修改后的程序在运行到评审表的时候就很可能会出错。不幸的是,在实际工作中,同样一个业务逻辑可能包含在无数个你可能知道,但你也可能不知道的代码中。这样的结构就是一个不易于维护的差的结构。 
总结:从技术升级和需求变更两方面适应变化

软件开发专家Alistair Cockburn在《敏捷软件开发》中说过,软件在整个生命周期中变更是无时无刻不发生的。我认为,软件的变更一方面是技术的更新,今天我们使用struts+spring+hibernate,明天呢,我们将使用什么呢?正因为技术变更得太快,我们的系统应当不要太依赖于某个具体的技术或框架,以便于明天的技术更新。同时,来自客户的需求变更也是我们必须面对的另一个压力。一句经典的话是这样描述客户的变更:“当我看到时我的需求就变更了。(I changed just when I saw it.)”过去我们用需求说明书来抑制用户的变更,现在发现不能这样了。敏捷软件开发提出了许多应对用户变更的办法,其中建立低耦合高内聚的软件结构也是办法之一。系统中的所有对象都有自己的明确职责,这个职责应当不多且高度相关。每个对象都应当只完成自己的职责,而把其它的任务交给别人去做。正如我前面提到的例子,评审表对象只完成与评审表相关的操作,而在它需要完成的任务中,需要使用评审计划数据的相关功能,交给评审计划对象去完成,评审表只管调用。这样的构造要求开发者相互协调,彼此多交流,同时,也需要有人来统一规划,站在全局的设计这个系统。通过这些,我们才可以适应变化,提高设计水平。

相关链接:回复:《如何在struts+spring+hibernate的框架下构建低耦合高内聚的软件
  • Caeb8220-1b1e-4efe-93d2-eda8a63db6fc-thumb
  • 描述:
  • 大小: 40.7 KB
评论
zlsunnan 2007-06-21
真是受益匪浅啊 自己只知道用 但是从来也没有考虑这些 惭愧啊 向楼主学习
fangang 2007-06-20
kevinming 写道
这样做是使得持久层简单了很多,但我觉得大型开发还是不大妥当。
参数是sql/hql的话,就把这些sql/hql写在业务逻辑层service里面了。我觉得这样做不大合适。我还是喜欢sql/hql全部都在DAO里面,虽然DAO大了,但要统一维护sql/Hql简单很多了。也做到了分层的实际意义,不然你一个DAO提供这些传入sql/hql的方法,那也只不过是一个数据库入口而已吧。

实际上,按照我的理论,在不论DAO或BUS中都不再写任何hql/sql,也不再需要任何表连接。我们执行任何表更新或查询的操作都是针对单个值对象的操作,然而这单个值对象实际上包含了与它相关的所有表的信息。比如“员工”表,其值对象已经包含了它的部门的属性,我们在查询的时候只是查询“员工”对象,不需要将“员工”与“部门”进行关联查询。当然,hibernate在实现的时候当然需要表关联,当对于DAO和BUS根本就不用考虑这些,它们只需要以对象的思维,运用hibernate的一对多、多对一、多对多、继承的关系,去操作值对象就可以了。这也是DAO的基本思想所在。
当然,我们不能否认,hibernate的值对象关系在处理异常复杂的查询和报表的时候是脆弱的。当你在处理这些东西的时候,不论是DAO、值对象、hibernate,还是hql其实都不是好的方案。直接采用sql和jdbc也许更合适一些。
kevinming 2007-06-20
giscat 写道
持久层的接口只需要很少的几把函数就可以了
save(Object) 用于insert
update(object) 用于 update
executeUpdate(sql or hql)
executeQuery(sql or hsql)

不管啥业务,都是以上几把函数的组合,
调用入口多了,就会混乱
四把函数就足够了
没必要为每个实体去写DAO,因为都是通用的



这样做是使得持久层简单了很多,但我觉得大型开发还是不大妥当。
参数是sql/hql的话,就把这些sql/hql写在业务逻辑层service里面了。我觉得这样做不大合适。我还是喜欢sql/hql全部都在DAO里面,虽然DAO大了,但要统一维护sql/Hql简单很多了。也做到了分层的实际意义,不然你一个DAO提供这些传入sql/hql的方法,那也只不过是一个数据库入口而已吧。
fangang 2007-06-19
tsingn 写道
伴随着JDK1.5被广泛使用,利用范性可以很好地解决大量DAOs的问题
可以参考http://www.hibernate.org/328.html

虽然泛型可以简化很多的问题,但不用泛型也可以解决大量DAOs的问题。在我以后的版本中,整个DAO层只有一个BasicDao和它的接口GeniricDao。你可以参考一下我后面写的博客的附件。
fangang 2007-06-19
classicbribe 写道
楼主哪里的??我和你有共同的愿望...设计优秀的软件...我从毕业后做第一个项目就是个边做边想的项目..到现在还是..受够了...拜你为师吧......行吗?行的话 加我QQ:214725178

拜师不敢当,相互交流,我的MSN:fan_gang2004@hotmail.com,没有QQ。
tsingn 2007-05-16
伴随着JDK1.5被广泛使用,利用范性可以很好地解决大量DAOs的问题
可以参考http://www.hibernate.org/328.html
qianqian_1216 2007-05-16
[quote="fangang"][quote="giscat"]持久层的接口只需要很少的几把函数就可以了
save(Object) 用于insert
update(object) 用于 update
executeUpdate(sql or hql)
executeQuery(sql or hsql)

不管啥业务,都是以上几把函数的组合,
调用入口多了,就会混乱
四把函数就足够了
没必要为每个实体去写DAO,因为都是通用的

我同意,我们可以通过泛型类型绑定,可以多个实体公用一个DAO,大量减少了代码量.
fangang 2007-05-16
JJYAO谈的问题我的理解是对于第三方产品是否需要解藕,这个问题正如我在《(原创)一个优秀软件开发人员的必修课:GRASP(2)低耦合》中提到的,耦合不好,但过度的解藕也不好,关键看你自己的需求。现在我提出解藕,是因为我看到了不同版本的hibernate和spring存在的差异需要我们解藕,以适应各个版本的差异带给我们的影响。
JJYAO 2007-05-16
本质上说,LZ的还是用代码级别的封装,以达到屏蔽具体实现这种古老的方式。做这种封装粒度难以控制,粗了则不够用,细了则极其烦琐,维护成本也很高。想象一下系统会使用如此众多的Third-party的Jar包吧,难道你都要封装?难道只会升级Struts,spring,hibernate?

LZ只提到了产品Non-Functional方面的升级,其实产品很关键的还有Functional的升级,在这里就不谈Functional的升级,
对于Non-Functional的升级,jar包升级是一方面,还有一方面是完全的功能性,甚至是整个实现的升级,比如从传统web升级到RIA。代码封装这种低级别的实现是没有办法做到的。

对于真正的产品公司,一般不会采用低级别的代码封装,从大的方向看,我的建议是
1. 选对你的第三方框架
2. 采用高度抽象的元数据描述系统,采用代码生成方式
3. 做好你的核心引擎
fangang 2007-05-15
zhaonjtu 写道
分析决策的第一点感觉很象桥接模式啊,不知道理解有无错误.

确实比较像桥接模式,但桥接模式是对类中的某个方法进行抽象,即桥接模式能使类在运行过程中动态地确定某个方法的实现,但这似乎还不是本方案的基本目的。
本方案的基本目的是使我们的业务系统不依赖于spring系统,即一个系统与另一个系统的解藕。从这样一个思路来讲更应当理解为一个适配器模式(虽然并不是标准的适配器模式),DaoSupportHibernate3Imp或DaoSupportHibernateImp是那个适配器,业务系统提供DaoSupport接口与外界交流,然后通过那个适配器与spring衔接而又不依赖于它。
zhaonjtu 2007-05-15
分析决策的第一点感觉很象桥接模式啊,不知道理解有无错误.
fangang 2007-04-27
robinjim 写道
看了楼主的例子,收益不少,只是有个疑问DaoSupportHibernateImp里面的query方法调用太繁琐了点,应该是对单表操作,为什么不用Criteria呢,传这么多参数容易产生接口耦合问题

谢谢robinjim的提醒,这确实是个问题
robinjim 2007-04-27
看了楼主的例子,收益不少,只是有个疑问DaoSupportHibernateImp里面的query方法调用太繁琐了点,应该是对单表操作,为什么不用Criteria呢,传这么多参数容易产生接口耦合问题
fangang 2007-04-27
xchlove 写道
这个问题很容易解决,BasicDao(以及其子类们)均实现了某一个接口,DAO们需要使用到的方法在接口中定义好就行了……


不太明白 不是还都要继承吗?为什么可以解决重新编译的问题了?

在类与类的关系中,接口与实现是松耦合的,父类与子类的继承是强耦合的。在我的结构中,BasicDao与spring的HibernateDaoSupport不是直接的继承关系,而是通过DaoSupport这样一个接口,实现了解藕。实现这个解藕是我的根本目的,因为这样就使我的项目不依赖于spring了。至于DaoSupport的各个实现依然继承自HibernateDaoSupport,这已经不重要了,因为对于不同的spring版本,由于它们存在一定地差异,每个版本我都需要去给一个实现,也就是说,当更换了spring和hibernate,我需要重新编译和升级更新的只有DaoSupport的实现和相应的配置文件。
xchlove 2007-04-26
这个问题很容易解决,BasicDao(以及其子类们)均实现了某一个接口,DAO们需要使用到的方法在接口中定义好就行了……


不太明白 不是还都要继承吗?为什么可以解决重新编译的问题了?
justshare 2007-04-11
楼主与我们现在分层的应用基本相似,业务逻辑的实现通过接口隔离,大概的流程是这样的:action---bussiness---dao---daoImpl.
fangang 2007-02-08
woodhead 写道
我们就是使用的整个系统一个DAO的方式。当时主要目的倒也不是考虑到Hibernate升级之类的问题,而是想要简化持续层的调用接口(我们的系统用.net开发,很多同事对hibernate不熟悉),最后效果还不错。还带来了些额外的好处,例如我们在DAO中加入拦截,就能在某些对象发生改变是自动触发一组相关操作。

现在我也在思考整个项目使用一个DAO的方式,这样可以大大简化我们的开发工作。
woodhead 2007-02-08
我们就是使用的整个系统一个DAO的方式。当时主要目的倒也不是考虑到Hibernate升级之类的问题,而是想要简化持续层的调用接口(我们的系统用.net开发,很多同事对hibernate不熟悉),最后效果还不错。还带来了些额外的好处,例如我们在DAO中加入拦截,就能在某些对象发生改变是自动触发一组相关操作。
fangang 2007-02-04
谢谢downpour。实际上downpour的疑问也是许多人的疑问,我的许多朋友和同事也常常跟我讨论这个问题。我认为,目前国内的软件开发可以分为几个不同的层次。

1、就是做项目,即为一个或几个特定的客户进行定制开发。这样的开发往往一旦完成就很少或者少量地需要升级。

2、当完成了一个项目以后,有的公司会努力将该软件项目推广到同行业的数家企业中。在把一个项目推广到另一个企业的时候,往往需要在许多地方进行修改和增加新的功能。

3、开发产品。公司根据市场的需要事先开发出一个通用产品,然后努力销售到数百上千家企业。这样的产品往往不断升级,整个软件周期将可能持续十多年。

4、软件开发平台。有的公司为了提高自己的软件开发速度,制作出自己的软件开发平台。在这个软件开发平台使用的数年间,该公司的所有软件项目或产品都将使用这个软件开发平台进行软件开发。

分析这4个层次,第一个层次是我们目前软件行业比较普遍的一种开发模式,它很少需要软件升级,或者升级所需要的修改量不大,因此它不怎么需要我在这篇文章中提出的方案。第二个层次也是比较常见的模式,它需要一些升级维护,但对于本文提出的方案,对于眼光比较长远的公司可能需要,但迫切程度也不是非常大。

对于第三个层次,产品开发的特点是,前期投入大,投向市场的客户群大,软件生命长,软件大规模升级的次数也多。处于这个层次的公司需要考虑的问题就比较多,眼光需要比较长远。他们需要考虑的其中一点就是,当该产品使用数年以后现在需要升级了,这个升级依然使用过去的旧技术,还是与时俱进采用目前的新技术。为了解决这个问题,他们正需要本文提出的方案。

对于第四个层次,一个平台往往需要使用数年,在这数年间将会发生数次技术更新。因此,一个公司在开发自己的平台的时候,必须要考虑技术更新的问题。可以试想,软件开发平台使用数年以后,公司要用平台开发一个新的项目了。这是难道公司因为要使用开发平台就依然沿用数年前的旧技术?他们需要软件开发平台能够经过少量的更新维护就可以应用目前流行的新技术。本文提出的方案正是解决他们的问题的方法之一。

我相信,随着中国软件业的发展和最大利润的追求,会有更多的公司采用第三、第四层次的软件开发。
downpour 2007-02-03
不是很明白。这样就能称之为解耦?

事实上,我们在开发一个J2EE程序的时候,会有很多Assumption。比如说,我们会倾向于使用相对成熟的框架来构建我们的开发。不会轻易的由于使用的Jar包升级而直接更换Jar包。所以楼主所谓的耦合我感觉实在不在我们的讨论范围之内。

如果你真要降低耦合,并不是说你的某些类不应该继承HibernateDAOSupport,而是将Jar包的依赖降低到相对比较轻的程度从而便于单元测试,或者说将Jar包的依赖控制在某个特定的层次(例如DAO层),其他的层次诸如Service层不依赖于这些Jar包而独立存在,这样才能比较轻松地在各种实现上切换。
fangang
搜索本博客
我的相册
10f6052e-a238-46fd-b6d3-b62321b2c51d-thumb
MultiSessionFactory
共 4 张
存档
最新评论