敏捷与结构性模块化
记者 火龙果

1 简介

敏捷开发方法论日益流行,然而大多数“敏捷”专家和分析师都在孤立地讨论敏捷,也就是说忽视了系统“结构”(Kirk Knoernschild是一个例外,他编写了一本名为《Java Application Architecture》的图书阐述这一理念)。考虑到“敏捷”是基础实体的一个重要特性或属性,那么,这种疏忽令人感到很惊讶。一个实体要具有“敏捷”的特性,它必须具有高度的结构性模块化(structural modularity)特征(参见Scott Page的《Diversity & Complexity》)。

也许正因为这种疏忽,许多组织在敏捷开发流程方面进行投入但忽略了应用程序的结构。除了“如何实现一个敏捷的系统?”这个问题以外, 有人肯定还会问, “如何构建一个在结构上具备高度模块化的系统?”

这个系列的文章将从探讨结构性模块化和敏捷之间的关系开始。

2 结构、模块化与敏捷

业务主管和应用开发人员经常面临相同的挑战。无论是商业领域还是服务于商业的软件,它都必须在成本范围内构建和维护。如果要保持实体的持续运营,就必须能够以低成本的方式快速响应难以预料的变化。

如果我们希望高效地管理一个系统,就必须先理解该系统。只有理解了系统,可控制的变更和定向升级才能成为可能。

当然,我们并不需要理解系统的每个组成部分的详细情况和特性,只需要理解所负责的系统的相关参数以及相应等级层次的行为即可。

隐藏服务实现

从外部角度来看,我们仅仅关心系统暴露的行为、提供的服务类型以及该服务的属性。例如,服务可靠吗?与替代方案相比有竞争力吗?

作为服务的使用者,我们并不关心服务的特性是如何实现的,我们只关心所提供的功能(Capability)是否可以满足我们的需求(Requirement)。

理解结构以便管理

不同于使用者,服务的实现方式对于服务的提供者而言,是极为重要的。为了更好的理解,我们为负责提供服务的系统建立概念模型,这是通过将系统分解成一组更小的相互关联的单元实现的。如果这个实体是某个公司,这张组件图就代表了“组织结构图(Organization Chart’)”;如果这个实体是软件应用程序,那么这张图就是模块间依赖关系的映射图。

尝试理解抽象系统的第一步如下图所示。

从上图的例子,我们可以马上知道:

1、系统由15个组件组成。

2、每个组件的名称。

3、这些模块间的依赖,尽管我们无法知道这些依赖为何存在。

4、尽管我们并不知道每个组件各自所承担的责任,从关联性出发,我们依然可以推断出“Tom”模块在系统中所占据的地位很有可能比“Dick”模块更为重要。

需要注意的是,这些组件可能并不是我们所创建的,我们也不必理解这些组件之间的内在结构。就像作为服务的使用者,只需关心服务所提供的功能,我们作为组件的使用者,只是需要它们的功能。

需求和功能(Requirements & Capabilities)

到目前为止,我们仅仅知道组件之间存在依赖性,并不知道为什么会存在这些依赖性。另外目前的状态是与时间无关的。如果随着时间的推移,发生了变化又该怎样呢?

最初我们可能会借助于实体的名字,再加上版本号(version)或者版本范围(version range),结构的变化由版本的变化体现出来。然而,如图3所示,版本名称(versioned name)尽管表明了系统的改变,但却无法解释为什么Susan 2.0不能像Susan 1.0那样与Tom 2.1一起协同工作。

这是为什么呢?

图3:如何跟踪结构随时间的变化?系统以前能够正确运行,后来由于一个组件的升级导致整个系统出错。为什么呢?

只有当我们仔细研究系统的功能和需求后,才能了解问题的原因。Tom 2.1需要管理者(Manager)的功能,这个功能在Susan 1.0中提供。然而稍后的Susan 2.0,由于她的职业规划,决定进行再培训,这时的Susan 2.0被赋予了新的Plumber 1.0功能,也就意味着其不再拥有管理者的功能了。

这个简单的例子向我们展示了模块间的依赖关系需要由需求和功能来表达,而不是它们的名字(Apache Maven项目最近正在讨论为制件的名字采用版本范围。尽管这是一个进步,但依然有缺陷,因为依赖还是用实体的名字来进行描述。)。这些描述应当能显示模块的本质,即模块应当能自我描述(需求、功能以及依赖应该进行文档化,但是随着时间的推移,这些描述会变得过时,如最初的文档制定之后,系统又发生了变化,而文档并没有得到更新。)。

图4:组织化结构:按照功能、需求的术语以及语义化的版本来进行定义

 

如图所示,我们完全可以不引用具体实体的名字,而直接使用需求和功能来描述一个系统

系统演化和语义化版本的角色

目前,功能与需求是我们了解系统结构的主要途径。然而,要理解时间推移所带来的变化,我们依然还会遇到问题。

在组织结构图中,如果某个员工晋升了,那么原有的关联性是否依旧有效(功能增强)?
在一组互相关联的软件组件中,如果我们重构了其中的一个模块(可能改变其公开接口),原有的依赖是否依然有效?
通过简单的版本化,我们可以观察到系统所发生的变化,却无法了解这些变化所带来的影响。然而,如果采用语义化版本命名方式(见 http://www.osgi.org/wiki/uploads/Links/SemanticVersioning.pdf),我们就能够传达系统变化而带来的潜在影响。

这可以通过以下的方式实现:

1、将功能根据major.minor.micro 的版本模式来进行版本化。同时我们达成共识,minor 或 micro这两个版本域代表非破坏性的变化(non-breaking change)。例如,2.7.1?2.8.7。相反,major版本域的变化,例如,2.7.1?3.0.0表示有破坏性的变化(breaking change),组件的改变可能影响到它的使用者。

2、需求则使用可接受的功能的版本范围来表示。方括号“[”和“]”表示包含此值,而圆括号“(”和“)”表示不含此值。因此,范围[2.7.1,3.0.0) 表示任何版本高于或等于2.7.1并且低于3.0.0(不含3.0.0)的功能都是可接受的
使用这种方式,我们可以看到如果Helen代替了Joe,Tom的需求依然会得到满足。然而,同样有管理者功能的Harry却因为其功能仍是1.7版本,不在Tom的[2,3)需求范围内,所以无法进行替换。

通过使用语义化版本的命名方式可以表达系统变更所带来的影响。再加上需求和功能,我们具备了足够信息,能够保证在满足系统各部分依赖的前提下,进行模块的替换。

我们的工作到此告一段落,这样简单的系统是敏捷且易维护的!

敏捷——从上至下贯穿各层

最后的挑战与复杂性息息相关。试想如果下列情况出现时会发生什么:1)系统的规模和难度不断增长?2)系统的模块数量大幅增加,并且模块间的互相依赖性也大幅增加?有些读者在前面的例子中,可能已经注意到出现了某种程度的自相似性(self-similarity),你们或许已经从中猜到了答案。

服务的使用者选择我们的服务是因为服务所宣称的功能符合它们的需求(见图1)。而提供服务的系统的实现方式,对服务的使用者而言是不可见的。向下的每一层都沿用这一模式。系统的结构自然而然地根据各组件的功能和需求进行描述。(见图4)。这时组件的内部结构对于系统而言是不可见的。如图5所示,许多逻辑层都可能使用这种模式。

图5:敏捷的结构:每层只暴露必要的信息。每层都是由组件间的依赖所组成的,这些依赖通过需求和功能来进行表述。

 

所有真正敏捷的系统都是以分层的层级结构建立起来的。在每个结构化的层次中,各组件的自描述都遵循以下的规则:只描述当前层次的有关信息,关于更低层次的不必要细节是不会描述的。

这种模式不断出现于自然系统和人造系统中。自然生态系统中所构建的大量结构都是由嵌套的模块化组件所组成的。例如:

1.生物体

2.器官

3.组织

4.细胞

无独有偶,商业组织也有类似结构:

1.组织

2.部门

3.团队

4.个人

因此,我们也期望复杂的敏捷软件系统也参照这些最优的解决方案:

1.商业服务

2.粗粒度的业务组件

3.细粒度的子服务

4.代码级别模块

这项进程起源于20世纪90年代中后期,许多公司开始采用包括面向服务架构(Service Oriented Architecture,SOA)和企业服务总线(Enterprise Service Buses ,ESB’s)为代表的粗粒度模块化技术。商业程序通过已知的服务接口或消息传递的方式,较为宽松地联系在一起。SOA提倡更加“敏捷”的IT环境,即商业系统应该更加易于升级或替换。

然而在许多实际场景中,核心的应用程序一直没有更改。很多现有的程序只是简单地将接口暴露为SOA服务。从这一角度来看,SOA实质上并不能如其保证的那样节省开支和使商业快速化:http://apsblog.burtongroup.com/2009/01/soa-is-dead-long-live-services.html。

这是因为内部缺乏模块化,加入SOA之后的程序和没加之前一样难以修改

变得敏捷?

我们将在此章节,对目前为止的观点进行小结。

为了“敏捷”,系统必须符合以下的特性:

1.层级化的结构(Hierarchical Structure):系统必须层级化,每一层由更低一层的组件构成。

2.隔离性(Isolation):对于每个结构化的层级,高度的隔离性确保参与运行的组件的内部结构将是不可见的。

3.抽象化(Abstraction):对于每一层,参与运行的组件的行为通过需求和功能加以表达。

4.自描述(Self-Describing):在每层之中,参与运行的模块间的关系都必须是自描述的。也就是说,依赖性定义将通过需求和功能进行表达。

5.变化的影响(Impact of Change):通过语义化的版本命名,变化对依赖的影响可以进行表述。

系统按上述原则建立,将是:

1.易于理解的(Understandable):基于层级化的结构,系统在每一层的结构都易于理解。

2.高适应性的(Adaptable):在每一层中,结构性模块化保证了变更的影响可以局限在那些相关的模块内部,高度模块化所建立起来的边界能够保护系统的其他部分不受影响。

3.可演化的(Evolvable):每层中的组件都可以被代替。因此,系统可以支持多样化(diversity)并且是可演化的。

系统可以通过结构性模块化来实现敏捷。

在这个系列的下一部分,我们将会讨论OSGi?——Java?的模块化框架——如何满足结构性模块化的需求,从而为流行的敏捷方法学奠定基础,最终形成敏捷的企业。

CIO之家 www.ciozj.com 公众号:imciow
关联的文档
也许您喜欢