UML软件工程组织

通往弹性软件架构之路
雅各布森中国

决定软件系统质量的最重要因素是软件架构。一个好的架构要确保不同类型的关注点(concern)的分离,也就是说,当其中一个发生变化时不会影响到系统的其它部分。可以通过识别系统中的关键用例来构建架构。通过分析这些关键用例,可以构建一个弹性架构,也就是说,各种不同类型的关注点保持分离,而系统中的一部分发生变化时对其余部分的影响是最小的。同时,架构的设计也必须满足诸如性能、可靠性等系统级关注点。架构将在系统早期的一个关键的版本中得以展现,这是一个可执行的版本——即称为架构基线的版本。在建立架构基线之前可能需要花费几个迭代,但完成后,将证实你的假设以及系统开发的方法,因此也就能够有效地降低风险。基于该架构,其它部分的开发速度必将大大加快。

什么是架构?

架构无疑是非常重要的。什么是架构?如果你问五个不同的人,可能会得到五种不同的答案。架构,和许多其它词汇一样,是无法真实触及的东西,其感觉就像过程、用例、项目、组件一样。但是,这些术语可以通过描述的形式来使之具体化。我们可以理解过程描述、组件描述、用例规约、项目计划以及架构描述等。当谈及架构时,我们将讨论如何理解架构描述。因此架构是架构描述的语义,它包含了关于系统的主要决策,例如:系统元素是如何组织的?系统如何实现所需的功能?系统如何满足预期的性能、可靠性和其它质量特性?系统需要什么技术(例如,Web客户端、胖客户端,特定的消息中间件)?系统内部组织的结构是否能够弹性地响应功能、技术、平台等变化?是否有标准能够确保系统开发始终保持一致?例如,使用了什么设计模式?使用什么异常处理原则?

根据项目的实际来考量系统的决策是很重要的。例如,你可能需要连接特定的遗留系统的接口,或者系统可能需要是可配置的、并且需要有定义系统参数的途径,或者系统还需要通过远程安装和管理,系统还可能需要处理特定业务领域的复杂性。这个列表还可以不断列下去。但架构不是系统的全部,它只是整个系统中最重要的20%的事情。

什么是好的架构?

什么是一个好的架构呢?无疑,一个好的架构必须满足诸如性能、可靠性等系统级的关注点。它必须是易于理解的,以便能够很容易地跟踪出架构中的哪一部分实现了哪些需求或用例。每一个类(稍后将组成包)都明确地扮演已定义的角色,履行该角色的全部的且仅仅是属于该角色的职责集。

一个好的架构必须使每个关注点相互分离,也就是说系统中的一部分发生了改变,不会影响其它部分。即使需要改变,也能够清晰地识别出哪些部分需要改变。如果需要对架构进行扩展,影响也将会最小化,已经可以工作的各个部分仍将继续工作。使用面向方面技术来构建系统,可以有效地保持关注点的分离。

分离功能需求。一般来说,我们希望保持功能需求之间是分离的,不管它们是以特性(feature)、用例或其它术语来表示。毕竟,它们都表明了不同的最终用户的关注点,并相互独立发展。你不希望某一个功能的变化会影响到其它的功能。功能需求通常是站在问题域的(例如,酒店管理、物流、银行、保险等)高度来表述的,很自然地,我们希望系统的特定功能从领域中分离出来。这样,易于把系统适配到类似的领域中。另外,一些功能需求会以其它功能需求的扩展形式定义,你也必须使它们相互独立。

从功能需求中分离出非功能需求。非功能需求通常用来标识所期望的系统质量属性,安全、性能、可靠性等等。这将通过一些基础结构(infrastructure)机制来实现。例如,你需要一些授权、验证以及加密机制来实现安全性;需要缓存、负载均衡机制来满足性能需求等。通常,这些基础结构机制需要在许多类中添加一小部分行为(方法)。这就意味着基础结构机制实现的一点变动会对整个系统产生巨大的影响,因此要使功能需求和非功能需求之间保持分离。

分离平台特性。现在的系统需要运行在多种技术之上。这些技术经常是与平台或厂商相关的。当一个厂商将技术升级到一个新的、更好的版本时,如果你的系统是紧密地依赖于该技术的前一个版本,那么要进行相应的升级并不容易。你当然不希望捆绑在某个特定的技术上。因此,你要使平台特性与系统保持独立。

将测试从被测单元中分离出来。作为完成一项测试的一部分工作,你必须采用一些控制措施和工具(例如调试、跟踪、做日志等)。这些控制措施是为了使系统的运行流程符合测试要求的规程。工具是为了(在系统执行过程中)提取信息,来确认系统确实按照预期的测试规程执行。这些控制措施和工具,经常需要在测试过程中向系统内插入一些与系统一起运行的代码。这些措施和工具所插入的代码在测试完毕后就会被删去。因此,你会希望能将测试的实现从被测试的系统中分离出来。

建立架构基线的步骤

必须尽可能早地建立一个良好的架构。即使在理论上,都很难通过采用重构等增量技术将一个糟糕的架构改良为一个好的架构,在实践中,那就更难了。这不是说重构是没有用的,而只是说明从一开始就建立一个良好的架构会更好。否则,重构的成本将很高昂,以致超出了功利的管理者所能接受的程度,那么他/她就会自然而然的选择简单修改而不是重构。所以,一个良好的架构需要在成本很小或者甚至零成本的时候就建立起来。优先构建架构会获得良好的投资回报的,它减少了项目执行过程中的重新设计和大量的返工。如果已经建立了一个良好的初始结构,就可以进行持续地重新评估架构,并做一些必要的完善和重构。

架构基线。架构是最终系统的一个早期版本,也称为架构基线。架构基线是整个系统的子集,我们称之为骨架系统(skinny system)。这个骨架系统包含了项目结束时的“丰满(full-fledged)”系统所具有的模型的一个版本。它包含了相同的子系统、组件和节点的骨架(skeleton),但是并非所有的“肌肉(musculature)”都已齐全。

不管怎么说,骨架系统确实具有行为,并且是可执行代码。骨架系统要进化到丰满系统(full-fledged system),可能只需要对结构和状态做出细微的改动。改动之所以是微小的,是因为在细化(elaboration)阶段或架构迭代结束时,我们已经得到了一个稳定的架构。否则,细化阶段必须继续,直到架构稳定为止,当然有一种系统化的方法来实现这一点。

虽然骨架系统(架构基线)经常只包括了5~15%的最终代码,但是它已经足够可以验证所做的关键设计了。更为重要的是,你必须确认骨架系统能够成长为一个完整的系统。伴随着骨架系统,会有一个书面的架构描述文档。

用例驱动架构基线。架构基线是由系统中的关键用例子集来驱动而建立的。我们称这个子集为架构重要用例(architecturally significant use case)。在识别出这些架构重要用例之前,你必须尽可能多地收集已有的信息来识别出系统的所有用例。注意,识别用例和描述用例并不是一回事。识别是指对系统需要做的事情进行界定、探索和发现。描述用例则是指对用例中的流程和步骤进行细化。描述用例贯穿了项目的整个生命周期,而识别用例则必须尽可能早地完成。

从这些识别出来的用例中,确定出其中哪些是重要的——“重要”意味着把它们组合在一起可以覆盖所有需要做出的关键决策:

· 演练了系统的关键功能和特性。

· 涵盖了大部分的功能性、基础结构、平台特性等方面的风险。

· 突出了系统中的一些具有高复杂度或高风险性的部分。

· 是系统其他部分的基础。

架构重要用例包含了不同类型的用例。毕竟,每个用例捕获了一组不同的涉众的关注点,并且需要做出不同的决策。因此,架构重要用例包含了应用用例和基础结构用例的组合。你可能会发现系统中的某些用例具有技术相似性和交互模式相似性。在这种情况下,只需要选择一个用例作为代表就可以了。这样,只要解决了其中的一个用例,就可以解决其他用例了。例如在酒店管理系统中,“登记入住”和“结帐离店”用例具有相似性,所以可以选择其中一个作为架构重要用例。

当你挑出了架构重要用例,就可以分析它们中的关键场景了。分析用例场景时,就能更好地理解系统需要做什么和系统中的元素是如何交互的。通过对系统的理解,就可以定义和评估架构。这个过程将不断地迭代直到形成一个稳定的架构。稳定就意味着系统的关键风险已经得到解决,并且所作的决定可以基本满足着手开发系统剩余部分的需要。这个架构不仅受架构重要用例影响,还受使用的平台、必须集成的遗留系统、标准和方针、分布性需求、所使用的中间件和框架等等的影响。即便如此,用例对于评估架构还是非常有用的。在所选平台、中间件、分布式结构等环境中分析每个用例,可以评估所做的选择是否足够,并且发现哪些地方还需要完善。

迭代地建立架构基线。对于一个复杂系统,在最终建立一个稳定的架构之前需要经过几个迭代。由于这些迭代聚焦于建立架构,因此也被称为架构迭代。在统一过程术语中,这些迭代叫做细化迭代(elaboration iterations)。

每次架构迭代中必须处理所有的架构关注点。你可能无法在每次架构迭代中成功地解决所有的关注点,但是需要全面地考虑它们。每次架构迭代过程产生一个增量,解决了部分的架构关注点。

迭代将持续进行直到所有架构关注点都已经得到解决。架构迭代结束时,将得到系统的一个早期可执行版本(骨架系统),并是通过测试和执行结果来证实的,因此,它是已验证和已确认的。

此时的系统版本就成为了架构基线。因此,架构就是系统的一个早期版本,它展示了架构的关注点是如何解决的。由于系统包含了一系列的模型,架构基线也是由这些模型的一个版本来表述的。伴随着架构基线存在一个架构描述文档,它的内容就是从这些模型中摘录出来的。

架构描述作为开发小组在系统的整个生命周期中的指南。架构描述是开发者在项目后续迭代中必须遵循的依据。

架构描述还将经过涉众的评审,以确定架构是否确实合理。应在架构描述文档中附上一张修订历史表来说明系统的演变,它同时也说明了重要的决策。

在架构迭代中,由于你需要做出决策,所以进展一般比较缓慢。一旦完成了架构迭代,生产率就会显著提高,因此投入到架构迭代中的时间是相当值得。

从平台无关的结构开始

如何结构化系统的方法是一个重要的架构决策,通过这个方法将使系统关注点保持相互分离。首先,从平台无关的角度进行结构化,然后根据平台特征来完善系统。平台无关的结构化是功能需求驱动的(如用例建模)。

用来实现弹性结构的工具有:类和用例。类有助于确保系统中的单元互相独立,用例则可以保持每个单元的任务互相独立。因此,系统中就有了两个正交的结构――元素结构和用例结构。

元素结构揭示了系统的元素在元素命名空间中的位置。一般通过层(layer)、包、类以及类中的特性来构建层次化的元素结构。

用例结构定义了在元素结构上实现功能的方法。它包括了切片(slice)――用例切片(use-case slice)和非用例特定切片(non-use-case-specific slice),这些切片把实际的类和类特性增加到元素结构上。

如果想让元素结构和用例结构都保持弹性,这意味着如果发生需求变更,造成的影响应该局限到元素结构中少量的包和类中;同时该影响也应该局限到少数的用例切片。局限意味着少量的改变,并且这些改变不会在那些需要变化的包和用例切片之外传播。

元素结构

一个模型的元素结构是基于包和类的分层结构,它唯一标识了每个元素。由于目标是实现弹性结构,自然会将相同用途的类放在一起。

层(Layer)。一般在模型中用层来作为第一个层次的划分方法。层用来对抽象出来的同一级软件元素进行分组。将更抽象及可重用的元素放在下面的层中,而把更具体的或者是很少重用的元素放在上面的层中。通常,使用两个高级别的层就足够来分析系统的功能性需求:

应用层。应用层包含了这些元素,它们实现了用例中支持系统的主要参与者的工作流。应用层中的元素通常用领域层中的元素来实现用例。你可以按照以下准则来组织应用层中的包:

· 支持一个或者多个特定参与者的类。

· 涉及一个或者多个特定用例的类。

· 涉及系统某些功能区域的类。

领域层。领域层包含了描述重要领域概念的元素。它们捕获了系统中需要维护、跟踪或者控制的信息以及产生这些信息的相关行为。这些元素通常在用例实现中被共享,它们更经常被重用,因此处于比应用层更低的层中。尽管如此,由于它们被用例实现(use-case realization)共享,因此用例实现经常横切这些领域元素。

用例结构

如上所述,元素结构只是简单地识别命名空间中的元素,是用例结构中的切片为每个元素加上了实际内容。共有两种切片:用例切片和非用例特定切片。

按照惯例,一般采用纵向来表示元素结构(包含的层,包和类),这样在顶端有应用特定层和包,在底端有应用无关的层和包。为了强调和用例结构的正交性,我们采用横向结构来表示用例结构,左边是非用例特定切片,右边是用例特定切片。

叠加平台特性

在每天结束的时候,你正在构建的系统必须能在某个目标平台上运行,还需要加入一些用户接口。如果需要提供高处理能力,必须将处理分布到所有的处理节点中。分布性就是平台相关的。必须为系统所管理的信息提供固定的存储空间,可能还需要和遗留系统整合。因此,你会发现平台特性贯穿于整个用例实现中,不管这是应用用例还是基础结构用例。

选择平台

系统的平台特性基于架构师选定的部署结构(deployment structure)和进程结构(process structure)。

保持平台特性独立

即便选定了部署和进程结构,仍然还有很多平台具体的实现技术需要选择。必须明确的是,不要被束缚在特定的执行平台上或特定的供应商上。平台特定技术总是不断发展,不断推陈出新。如果更改设计只是为了跟上这些技术上的变化,那是非常有害的。因此,必需使平台特性保持独立。如果从用例设计中把这些平台特性剥离出来,剩下的就是最小化用例设计(minimal use-case design)了。这个最小化用例设计有以下特征:

· 它是可执行的,并且采用一种默认的编程语言实现,如Java。

· 它在通过程序接口来激活的。一个单独的程序来触发该最小化用例。通过这种方式,所有用户接口、信息的表示,和数据的输入机制都从最小化用例的设计中分离出来了。

· 分布性、内部进程的通信和平台相关消息通信等关注点都与最小化用例设计保持分离。所以,虽然事实上最小化用例设计在先前说明的所选平台上运行,但是它看起来是运行在单一节点、单一进程和单一线程上。

· 它所需要的所有信息都在内存中。通过这个方式,所有与留存机制(持久性)相关的关注点都不在该最小化设计中。同样,参与者实例的每个活动都是原子性活动。

其它的所有东西(用户界面,分布等)都被认为是平台特性相关的,并且被单独设计,叠加在最小化用例设计的上层。

把平台特定部分从最小用例设计中分离出来有很多好处。首先,最小化用例设计变得更加简洁。任何一个懂得指定编程语言的人都可以开发它,而不需要知道所有的平台特性。最小用例设计容易设计和开发,让你很容易生产出一个可执行产品。更加容易测试,因为它不需要具有平台特性的测试环境。

总结和强调

在项目的早期建立一个弹性架构是至关重要的,其目标是让系统健壮并且减少需求变化所带来的冲击和系统大量的修改。它同时也让系统变得容易理解。从面向方面的观点看,一个弹性系统让你更容易定义pointcut,因为需要扩展的所有类和职责都被局部化了。

建立描述系统的模型结构的过程是迭代的。首先从一些平台无关的结构开始,然后开始逐个分析架构重要的用例。这么做的时候,会逐渐补充和完善已经存在的结构,并且将平台相关的元素融合到结构中。当遍历了所有的架构重要用例,就可以建立一个相当有弹性的架构。


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