逸言

模块间的职责分配

| Comments

职责分配不仅限于对象之间。如果将对象看做是细粒度的封装,则模块作为组合多个对象的设计元素,可以看做是粗粒度的封装。倘若在模块之间分配职责失当,带来的影响会比对象职责分配不合理更大。这是因为只要在OO设计中进行了适度的封装,在一定程度上体现了隐藏细节的设计理念,对象的职责分配失当就不会溢出边界,对其余对象不会产生太大的影响。

模块间的职责分配好似对对象进行一种归类,只有属于同一类别的对象才能被分配到一个模块中。这可以说是对“高内聚”原则的简单理解。注意,模块常常是对架构层次结构的横向切割,不同的模块可能处于不同的逻辑层。因此,大的设计问题可能牵涉到分层问题。从架构层面来讲,或许我们有必要在获得模块的详细设计之前,审慎地给出一个系统逻辑架构,会有助于模块间的职责分配。

与对象职责分配一样,模块间的职责分配也有“坏味道”存在。常见的坏味道就是“循环依赖”,即模块A依赖模块B,模块B依赖模块C,模块C又依赖模块A。这有点像三角恋爱关系,扯不清,理还乱,让人头疼。所以我的一个设计实践,就是从设计一开初,就要绘制软件系统的包图,通过直观的图形来帮助我们识别这种依赖的乱麻。

另一种坏味道我称之为“爱屋及乌”,简单说来,其实就是违背了Robert Martin提出的有关包设计的“共同复用原则(CRP)”,即“一个包中的所有类应该是共同重用的。”如果一个包的职责分配出现问题,违背了这种共同重用,就可能导致一个包为了使用另一个包的某一个特性,必须把整个包都放进来,哪怕这个包中的其他特性对于使用的包而言完全无用。这就好像我贪恋钱财,娶了一个垂垂老矣的富婆,为了钱,也不在乎丑陋的容颜和高我数倍的年龄了。

例如在我之前设计的一个系统中,出现了这样的包图:

该系统属于基于元数据的报表系统,engine.report包负责读取定义在配置文件中的报表模板,并在运行时绑定数据,最终输出报表。报表的数据则由engine.data包来承担,它会根据配置的元数据生成SQL语句,访问数据库,动态得到报表所需的数据。engine.entity是在之后引入的一个包,它承担了类似ORM的功能,能够将engine.data包返回的数据,通过配置的映射信息,生成Entity对象。而该Entity对象则作为报表数据绑定的基本元素。至于tool.reportdesigner,则是一个Swing界面的报表设计器应用程序,用户通过界面的设计操作,可以生成我们需要的报表模板。

倘若没有绘制这个包图,而是直接阅读代码,或许很难发现这其中蕴含的设计问题。包图清晰直观,首先我们看到了engine.report与engine.entity之间存在双向依赖(它是循环依赖的特殊形式),并在engine.report、engine.entity与engine.data三者之间发现了循环依赖。其次,我们发现tool.reportdesigner依赖于engine.report,但由于它只负责报表模板的设计,并不需要engine.report包中的运行时数据绑定,以及报表导出功能。显然,前者违背了“循环依赖”,后者违背了“爱屋及乌”。

究其原因,我们发现罪魁祸首就在于engine.report包中的xml组件。该组件的功能与配置相关,其中还包含了许多配置在xsd文件中自动生成的jaxb对象。它并不仅仅是Report独有的功能,相反,这个包图的所有包事实上都需要用到这个功能。换言之,这些包均需要依赖于xml。最简洁的改进方案就是将它抽离出来,放到一个单独的包中。改进后的包图如下:

为何我将xml设计为infrastructure.xml包?这完全是因为依赖的关系。假设我们将每个依赖都设置其权值为1,则一个包被依赖得越多,其依赖权值就越大。假设这些权值就等同于重量,然后我们设想将这些包都丢到一湖秋水中,分次浮沉。显然,权值越多的包就会慢慢沉淀下去,而权值越少的包最后会浮在上面。我称这种现象为“依赖沉淀”。通过这种方式,可以在一定程度上帮助我们对软件系统的层进行有效划分。

依赖管理是开发者可能面临的地狱。如果还加上对各种版本包的依赖,这个地狱就可能处于十八层的水深火热中了。依赖管理是一个大课题,但如果模块间的职责分配合理,或许会是一个不错的开始。

Comments