根据业界的统计数据显示,在一个软件系统从开始研发到最终消亡的整个生命周期过 程中,前期的架构、设计、编码、测试所付出的成本及代价只占所有系统相关工作的 20%~40%。绝大部分的工作重点,还需要花费到系统投入生产的后续维护和在以往系统上 进行的升级工作上。
与此形成鲜明对比的是,系统开发行业,尤其是系统架构领域将绝大部分的研究精力 集中在了前期架构、设计、编码、测试等如何髙质量幵展的方向上,却忽视了现实中存在 着大量的老系统和大量的现存代码需要进行维护。同时,在老系统和代码的基础上需要增 加新功能也占有很大比例的工作量。但是计算机科学发展至今,在很长的时间内,对系统 后续维护工作方面的系统化方法论的研究还没有引起行业内的足够重视。
提及系统的后续维护的话题,我们还是先来看看现实工作中一个笔者与客户进行的关 于架构和设计方面协作的典型实例。
一个系统架构恢复和重构的主要目的,首先是将现有系统已经含糊的架构和设计进行 梳理并恢复到一定清晰的程度。然后进行架构重构和优化,以便于日后的扩展和修改等维 护工作。通常,架构恢复和重构可以从下述几个阶段来依次展开,我们将其称之为架构恢 复与重构的 AR&RM (Architecture Recovery & Refactory Method)方法:
• Stage I:确立反向工程与正向工程的概念
• Stage II:架构和设计恢复
• Stage III:架构和设计重构
• Stage IV:系统代码重构
业界的最佳实践证明了如图7-2所示的四个主要阶段的工作是延缓一个系统产品不可 抗拒的衰老趋势所必不可缺的动作:首先,要在团队中正确的建立反向和正向工程的理论
概念,这样才能很好地帮助目标的界定和实现;其次,架构和设计恢复工作的进行,有效 地保证了我们的团队是工作在一个淸晰并认知一致的架构基线上:接着,为了改变现有架 构中存在的那些不能满足维护要求的设计问题,进行架构和设计的调整就显现得非常有必 要;自然,原先的代码是不允许完全抛弃的,我们可以通过代码重构来合理地重用那些有 用的代码。
7_1反向工程和正向工程
为了延缓系统的衰变,我们将采用业界的最佳实践来进行系统和架构的恢复和重构。 首先需要澄淸一些概念和行动步骤,以便于后续工作的开展和目标的制定和实现。这些重 要的概念包括:系统重组(Reengineering)、反向工程(Reverse-Engineering)、正向工 程(Forward-Engineering)、架构和设计恢复(Architecture & Design Recovery)、架构和 设计重构(Architecture & Design Refactory)、代码重构(Code Refactory)»
那么’先来看看什么是系统重组(Reengineering),这是架构恢复和重构的核心概念, 也是最复杂的一个总体概念。
从图7-3中,我们可以看到,一次完整的系统重组被划分成了两个主要阶段:反方向 工程阶段和正向工程阶段。其中,反向工程(Reverse-Engineering)阶段主要包括以下活动:
系统分析和架构恢复:该活动的主要目的是能够使我们全面掌握当前系统的架构基 线’即主要回答这样一个问题:我们面对的是一个什么系统?
SWOT分析:针对当前所面对的系统,从架构的角度来分析其优点、弱点、改进机 会、面临威胁等。
进行抉择:前两个活动分析的结果,会成为我们进行抉择的输入信息。现在,该 是我们抉择系统中哪些部分该保留,哪些部分该修改,而哪些部分可以彻底抛弃 的时候。
反向工程任务完成之后,我们已经完全明确了当前系统代码的功能及关系、系统架构 和设计的结构、满足的主线业务情节等重要信息。然后,我们会进入到正向工程(Forward-Engineering)阶段 „
建立新的架构远景:为崭新的系统确立架构远景,其中包含了以往可重用的架构组 成部分及计划添加、修改、替换的部分。
建立新的架构基线:遵循“将可重用的或新的架构部分逐渐组装进系统架构当中, 然后立即进行重构”这样的原则,采取逐渐提炼和优化的风格,进行架构和设计的 重构,并最终逐步形成崭新的架构基线。
进行代码重构:遵循“将可重用的或新的代码部分逐漸组装进系统代码当中,然后 立即进行重构”这样的原则,采取逐渐提炼和优化的风格,进行代码的重构,并最 终逐步形成崭新的系统代码基线。
从Reengineering中的反向工程和正向工程的活动中,我们或许产生了这样一个感觉: “Reengineering”的概念和我们所熟知的由Martin Fowler引入的“重构”(Refactory)的概念似乎非常相像。澄淸这两个概念的异同,对于我们即将进行的架构恢复和重构的工作非常重要。
我们先来看看由 Frank Buschmann 和 Michael StaJ 对“系统重组(Reengineering)” 所下的个定乂: Reengineering is the examination and alteration of a system to reconstitute it in a new form and the subsequent implementation of the new form”。
接着,再来看看由Martin Fowler给“重构(Refactoring)”所下的定义:-Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure”。
乍看这两个定义,似乎真的非常相像。但是,如果我们仔细分析一下两者的真实含义, 就不难发现它们还是有着明显的不同和各自的侧重。
涉及范围:Reengineering是对系统全局范围内的一个动作;而重构一般只会影响系统某些局部子系统、构件或模块。
•运作过程:Reengineering在执行过程中,通常遵循了先分解、继而进行重构、最 后再组装的原则。这样可以确定卸装出来的部分是否可重用以及需要修改和优化的 程度,从而最终在组装时最大程度地顺利集成为完整的一体;重构基本遵循了只改 变系统的部分结构,而以往的系统功能和行为保持不变。
•最终结果:Reengineering的最终结果往往会导致一个全新系统的诞生,这个新系 统无论从结构上、功能上和行为上都可能与先前的系统有着明显的区别;传统的重 构往往只是优化了系统内部的结构,而其功能上没有任何变化,一个重构过的系统 对于该系统的使用者而言,将会感知不到系统其实已经发生过变化。
看来传统意义上的“重构”,可以应用到一个初创系统时期来帮助进行系统结构的优 化,同时重构技术也可以应用在-个系统的老化时期来帮助Reengineering中的正向工程去优化一个系统中的部分结构。Reengineering和重构其实并没有什么矛盾的地方。相反, 重构技术是Reengineering的一个有力补充。更具体地讲,Reengineering中正向工程使 用的最主要技术就是重构技术。我们进行Reengineering的主要目的之一’也同样是想最 大程度地复用并优化以往的系统结构,但要保持系统的功能不变!
7.2架构和设计恢复
Elliot J. Chikofsky曾经对一个系统的架构和设计恢复给过这样一个经典的定义“The process of analyzing a subject system to identify the system’s components and their inter-relationships and to create representations of the system in another form or at a higher level of abstraction\从这个定义中我们可以看出,架构和设计恢复就是利用现有的资源(现 存的代码、文档、测试用例等)来抽取关键的架构信息,从而恢复一个系统原有的风貌。只 有在这样的基础之上,维护人员才可能完全理解一个系统的当前状况,为未来计划中的系统修改做好铺垫工作。从反向工程的概念来看,架构和设计恢复就是反向工程的核心任务。
可是,如果我们负责系统架构和设计的恢复工作,单从这样一个经典定义中,我们还 是不知道到底如何计划、管理和执行架构恢复工作。虽然我们知道并能够熟练运用很多系 统研发的流程,但是,业界似乎并没有一个可以参照的、类似于Waterfall或RUP这样经 典的架构恢复流程。
早在20世纪80年代末期,业界就已经认识到了同样的问题:系统研发范围内的研究, 仅仅局限在一个系统正向研发时的各种方法论。而忽视了一个系统进入维护或运营阶段所 经常出现的架构恢复的要求。从20世纪90年代初期开始,一些研究探索和实践就围绕着 架构恢复这个问题逐渐展开。到目前为止,这些努力和尝试取得了相当的成果。十几年的 探索过程基本上集中在下述方面:
•对底层结构的恢复(例如系统数据流的恢复)
对高层系统概念的恢复
•对程序部分结构组成的恢复
•程序代码综合结构的恢复
系统结构的恢复
基于领域知识和模型的架构恢复
基于设计模式的架构恢复
如果我们仔细分析一下上述领域的研究和实践,就会发现这些研究和实践基本上集中 在如下四个层面上:“系统概念和需求恢复”、“系统功能恢复”、“系统架构和设计恢复”以 及“系统编码实现恢复”。很明显,这四个层面间的次序,是一种逐渐深入、逐渐细化的层 次关系。这也就意味着,在不同的层面上,可以进行不同程度的系统恢复工作。
在我们进一步探讨如何进行一次架构和设计恢复前,需要先将这四个层面进行一下说明。
系统概念和需求恢复:系统概念可以从两种视角来衡量。如果假定我们是一个系统 的用户,那么一个系统到底能帮助我完成什么工作,这就是系统用户建立起来的对 一个系统的概念。如果我们从系统架构、设计,甚至是编码人员的视角去看一个系 统,系统概念就会成为一个个主要的功能点。同时,这些功能点是怎样协作来完成 要求的任务也是需要建立起来的系统概念之一。系统需求,则代表了用户指定的系 统必须完成的任务以及必须满足的质量方面的要求。
系统功能恢复:是遵照客户所指定的需求,经过设计和实施后系统所具备的能力。.
•系统架构和设计恢复:为了满足所要求的功能而构建出的系统组成结构及关系,这 时可以利用的材料主要是原先系统架构设计中的文档、代码、模板等。
•系统编码实现恢复:是一个系统设计经过编程语言最终实现的过程。这时可以利用 的材料主要是原先系统编码实现时产生的源代码文件、开发文件目录、培训材料、 用户手册等。
如果我们进行一次系统架构和设计的重构,通常会需要将上述四个层面的核心内容完 全恢复出来。如果只是完成了其中一两个层面的恢复工作,仍然无法勾勒出一个系统的全 貌9没有这样的全貌,就根本无法满足我们后续系统维护的要求。所以,我们需要一个更 加系统化的架构恢复方法。
基于前期架构恢复实践和系统未来维护的要求,较理想的架构恢复过程和最理想的结 果如图74所示。
从图7-4可以看出,我们其实最需要的系统架构恢复流程,是一个覆盖了从“系统概 念和需求”到“系统编码实现”四个层面,并且经过了恢复信息的汇总、对应和交叉验证 的过程之后,最终恢复成为一个完整的系统架构的系统化过程。
现在,架构恢复的总目标和大致恢复过程的轮廓己经确立。那么,为了实现这样一个 理想中的系统架构恢复,我们应该怎样进行实际操作呢?从流程的角度上来讲,可以将架 构恢复划分为这样五个主要流程阶段:系统概念和需求恢复阶段、系统功能恢复阶段、系 统架构和设计恢复阶段、系统编码恢复阶段、汇总/对应和验证阶段。
1.系统概念和需求恢复
“系统概念和需求恢复”工作是系统架构恢复的第一个阶段。该阶段的主要目的,是以 当前可以搜集到的那些系统相关的文档(例如:系统的用户使用手册等)为出发点,最终 帮助我们建立起对一个系统的概念和认知,包括系统服务的商业业务领域、系统提供的服 务内容、系统的行为的轮廓、用户在该系统初创时需求方面的要求等,从而在一个架构恢 复正式开始前,明确地界定此次架构恢复的目标。
从图7-5可以看出,该阶段的工作基本上由四个主要的活动顺序组成:搜集系统当前 保留的信息、进行初步系统信息分析、建立系统概念和系统需求、与管理层确立架构恢复 目标。
A.搜集系统信息
一个系统从开始创建,经过了数年的连续维护,必然会有一些重要的文档或信息保留 了下来。作为后续架构恢复的基础,这些记录过去历史的可视性信息将有助于回答诸如“这 个系统到底是什么”之类的问题。
系统信息搜集,就是要通过各种渠道,去搜集和汇总那些与系统相关的信息,例如系 统源代码、架构和设计文档、需求说明书、测试计划、集成和系统测试结果、测试用例等。 除了研发期间的文档,那些系统维护期间的文档也需要进行汇总,例如系统维护记录、系 统维护手册等。从客户使用的视角来看,用户使用手册等信息也非常有助于我们理解该系统的商业功能。如果该组织的知识管理做得非常好,那么这些信息的搜集还是非常容易的。
为了快速建立起对该系统更加深入的认知,与系统的Stakeholders及研发人员的沟通 也是一种非常有效和直接的方式。如果条件允许的话,我们还可以利用各种方式,与当年 的系统研发人员取得联系,然后进行简短的沟通,这无疑对我们目前的工作会有极大的帮助。
在大多数情况下,我们无法找到当年的研发人员。这时,从事系统后续维护工作的架 构和设计人员、编码和测试人员等,就是我们可以重点走访的对象。从他们那里,我们肯 定可以听到许多更加深入和直观的有关当前系统的论述(有时甚至是抱怨、系统所保留的历史信息、维护人员的访谈信息,都成为下一步我们分析和理解的基础。
B.系统信息分析
经常遇到的尴尬情况是,我们手中能够掌握的有关系统的信息,大多数是有关于如何 完成某些特定要求或功能的文档,例如:系统设计最终应用了一个怎样的算法来解决系统 资源的调度问题。虽然这类信息在一定程度上回答了一些系统相关片段的“How”问题。 但是,当前阶段我们的首要任务是要明确“整体是一个怎样的系统”,即我们需要知道的是 整体系统的“What”问题。
所以,我们需要以这些可能不完整的系统信息文档为出发点,通过逐渐分析、汇总及 拼凑,最终需要回答“这是一个什么系统”的问题。这样分析的结果,在一定层面上帮助 我们建立起该系统一个比较宏观但完整的概念:例如该系统能够提供的主要商业服务或商 业功能(我们目前并不关心这些服务在系统中是如何实现的这样的问题)、系统运行时的主 要行为、系统的主要组成元素(子系统、主要服务或构件等)、系统物理的分布、系统组成 元素的职能和相互关联、系统与商业组织运作的结合等。
经过这样的分析,一个完整的系统概念已经在我们的脑海中建立了起来,我们已经在 很大程度'上清楚所面对的是一个什么系统。
C.系统概念建立
通过上面分析的结果,我们已经明确了系统的整体轮廓。现在,需要将这样的系统概 念以适当的方式描述出来。毕竟,头脑中建立起来的概念可能并不是一个可视的、系统化 的描述。我们需要以文字、图表来叙述该系统存在的商业背景、主要功能及商业目的、系 统主要输入和输出、系统提供的用户界面等。
这种书面的方式,比较适于今后工作中的交流,并且该过程本身也是一个不断完善的 系统化工作。.
D.确定目标
在具体展开一次系统架构恢复前,还需要根据目前所构建出的系统概念,与管理层确 定架构恢复的目标和范围。业界的实践告诉我们,如果没有这样一次范围限定的活动,势 必会造成恢复工作持续时间长、目标和范围模糊、恢复效率低下等问题。
对一个大规模复杂系统的反向工程,尤其需要进行时间和范围的限定,否则会导致一
次反向工程中的产出物甚至比你要进行架构恢复的系统所拥有的文档数量还多,这显然适 得其反。
进行目标和范围限定时,一定要以目标为导向,结合前期构建的系统“big picture”来
界定此次架构恢复的范围。例如:此次架构恢复是要将该系统的功能进行全面的恢复,从
而明确该系统所涵盖的各种功能以及相应功能的应用主线场景和各个可能的分支;如果是
对系统中某个子系统的架构和设计进行恢复,我们主要就会利用该子系统的源代码或设计 文档来进行进一步的工作。
2.系统功能恢复
系统架构恢复的第二个阶段,就是要完整地恢复一个系统所覆盖的各种商业业务功能。 系统功能的全貌,有助于我们理解一个系统为什么会进行不同的架构和设计抉择。同时, 系统功能的理解也帮助我们对当时系统构建时客户所提出的需求进行分析和汇总。这样,在很大程度上,我们将一个系统的用户需求和系统功能进行了完整的映射,如图7-6所示。
系统功能恢复与系统概念恢复阶段相比’是一个更加深入的分析过程。通过文档、源 代码、系统测试用例、系统运行界面的输入输出、系统的数据流、系统控制流、相关人员 的访谈等各种方式的结合,试图进一步细化并完整汇总系统所支持的用例(use Case); 同时继续细化前期获得的部分系统需求,使用户需求更加丰满。
仅仅是系统用例’还不能够复原系统的全貌,需要逐步完善系统所支持的各种业务和 系统场景(Business and System Scenario)。只有将系统的功能细化到支持场景的程度上, 才能真正在细节这样的层面上完善系统功能的原貌。
3.系统架构和设计恢复
基于我们已经建立起来的系统概念、搜集汇总的用户需求、细化和分析的系统功能这 三类主要信息,再结合当前遗留的架构设计文档,我们可以开始进入到系统架构和设计恢 复阶段的工作了。
系统架构和设计的恢复是一个滚动添加和修改的过程。之所以用“滚动”来描述架构 和设计的恢复,就是因为架构和设计的恢复其实是一个猜想、验证、修改的循序渐进的过 程。但是,作为“滚动”的第一步,一个合乎逻辑的系统架构和设计猜想是走向成功的基 础,如图7-7所示。
架构和设计恢复的核心工作,就是一个架构猜想的建立和验证的过程。
首先,通过与维护工作人员中涉及架构和设计的技术人员的沟通,结合系统设计文档 的进一步深入分析,已经在很大程度上具备了做出一个系统架构和设计猜想的条件。这时 我们再结合一些计算机辅助软件工程(Computer Assistant Software Engineering, CASE) 工具,就能从系统源代码中抽取那些与架构和设计相关的结构方面的信息。例如:系统的 接口信息、构件间的调用关系或构件执行顺序、系统的数据流、系统的控制流、代码执行 顺序等。这样就逐步形成了一个完整的、符合商业需求及当前收集的系统功能要求的系统 架构和设计猜想。
4.系统编码实现恢复
系统编码实现方面的恢复是最低层面的系统恢复工作。根据前期建立的系统概念、汇 总的系统功能、系统架构和设计的猜想,我们可以开始重现当前系统代码级的原貌,如图 7-8所示。
系统代码级的恢复,主要是将系统代码的结构和关系进行恢复
举一个经常出现的例子,例如一个经过多年维护的系统,系统代码经常会出现代码目 录结构混乱(应用程序的代码包经常和中间件或编程框架的代码混杂在一起)的问题,以至于维护人员为了编译一个小小的修改,需要将很多无关的代码添加进来,才能最终编译 成功。
通常,系统实现代码间的结构及相互之间的关系,可以从下面几个方面得到有用的线索。
开发人员的文档:在开发人员的详细设计文档和源代码中,经常有很多有用的解释信息,这些信息有助于我们理解代码间的组成关系和协作行为联系。
•代码调试:核心代码的调试会有效地帮助我们理解系统实现代码间的执行顺序和代码的各种异常分支,这也是搞清代码结构关系的重要方式。
•系统参看物:可视的用户界面变化(例如页面前后次序)、系统榆出的报表、系统与外部交互的过程等’都是我们对代码执行期间的动态关系进行推敲的工具。
•辅助工具:有些反向工程工具可以帮助我们解析系统代码,抽取一些静态信息(例I 如:Attributes及功能方法),或者形成构件结构视图和依赖关系视图,这在很大程| 度上也有助于我们的分析。
、在系统编码实现级完成了恢复工作后,一个完整的代码级系统静态视图将会展现在我 们面前。这样的视图无疑在后续系统代码维护时期成为我们重要的参照依据之一。
5.汇总/对应和验证恢复
一旦上述四个层面的恢复工作各自完成到一定的深度后,就需要进入一个汇总、对应 和验证的过程中来。这是因为,我们需要将相对割裂的四层的工作结果进行关联。例如: 系统功能的描述,需要对应到那些相应的实现代码上。而那些实现代码,需要对应到架构 和设计的某个模块或构件上。同时,这样的对应往往可以帮助我们更正系统架构和设计猜 想中那些与实现代码完全脱节的错误或遗漏。
作为系统恢复的最后一个动作,还必须将恢复过程中的各种文档进行汇总和整理,并 且形成系统化的互联关系,从而通过“系统恢复文档集”的方式,将一致的、相互联系的、 真实的系统原貌展现给系统维护方。
系统架构和设计恢复是一个不断递增的、系统化的复原过程。作为一个完整的流程, 我们可以参考如图7-9所示的动态表现方式的系统架构恢复流程,再根据自身的要求,来 进行范围明确的架构恢复。
作为一个完整的架构恢复流程,从图7-9中我们可以看到,除了需要构建一个架构的静态 结构描述外,架构的动态描述也是非常重要的辅助信息。构建架构的动态描述,非常有利于我们创建一些测试用例。这样做的目的,就是使用当前的运行系统来检测我们系统恢复的质量。
虽然架构和设计恢复是系统反向工程中的核心任务。但是,除了进行架构恢复外,通 吊我们还需要在恢复系统原貌的基础上,进一步分析当前系统结构上的优缺点及改进机会。 例如:哪些系统组成部分已经不能继续使用、哪些系统组成部分需要进行修改、哪些接口 需要重新制定、哪些是坏死部分并已经完全没有重用价值等。只有这样一个系统反向工程 的结果’才真正能够帮助系统维护管理层进行未来系统改进时的抉择。
7.3架构和设计重构
前期反向工程中的架构和设计恢复,已经将-个系统的原始风貌完全还原了出来。这 样的-个系统架构和代码基线,将成为下-步我们进行正向工程的良好起点。换句话讲, 反向工程虽然做了很多分析工作,但是不会对既有系统做任何改动。从正向工程开始,就 是我们大胆进行系统变革的时刻。
正向工程中的核心任务主要由两个方面的重构动作组成:系统架构和设计的重构与系 统代码的重构。当然,除了这两个主要的重构动作外,正向工程中还包括其他方面的重构 工作(由于本书的侧重点,我们不去详细讨论下述重构问题)。
•文档重构:如何重新组织系统相关文档和图标的结构,使这些可视信息更加便于阅 读和理解。
^流程重构:如何对系统研发流程进行修改,以满足维护工作的需要。
测试重构:如何使测试流程、测试计划适应由于系统架构和系统代码修改所带来的 变化。
数据库重构:如何使底层数据库能够适应变化了的系统架构、设计和编码实现。
组织结构重构:如何有效地进行研发团队组织结构的优化。
“重构”这个概念,相信很多人都对Martin Fowler所引入定义的“重构”非常熟悉。 但是,Martin Fowler的“重构”只是业界研究系统代码重构的著名代表(非常遗憾的是, 业界大多数的从业人员和研究人员,依然把重构的重点放在了系统代码上,而忽视了架构 和设计重构的重要性)。除了代码重构外,当前业界对系统架构和设计重构的研究和实践, 也引起了越来越多的关注。Joshua Kerievsky的研究结果,就是这方面的一个成功典范。
谈到系统架构和设计重构,有一点与Martin Fowler的系统代码重构相似:如果我们纵 览一遍老系统的代码,直觉上就能够嗅到很多系统代码的“坏味”(Bad Smells in Code) (备注:代码的“坏味”是Martin Fowler的一个著名概念)》类似地,当我们纵览一遍恢复 出来的系统架构和设计时,同样也能察觉到很多系统架构和设计的“坏味”,例如:
架构设计严重违反了 DRY (Don't Repeat Yourself)原則。
系统元素命名混乱,无法分辨其在系统内的职责和角色。
大量使用中央集中处理的架构机制。
系统中各部分相似的问题却使用不同的设计方式来处理。
无法顺利使用子系统内的部分功能
子系统或构件的结构不断蟛胀。
直接暴露子系统或构件的内部功能。
•分散的系统配置,造成了手工逐点配置的大量工作。
«系统资源访问方式不佳,严重影响系统性能。
«系统内同步和异步通信方式使用混乱。
»系统元素间没有形成双向联系,无法互相调用。
•系统接口的Contract没有严格定义,造成调用子系统和构件的行为没有严格按照接 口 的 Contract 执行。
•子系统间功能划分不合理,造成子系统间緊耦合。
•系统结构逻辑没有严格按照Layer的架构风格设计。
架构和设计重构的核心任务,其实就是按照Brian Foote曾经说过的“不断地去否定 以往的系统架构和设计”的原则’力图找到架构和设计中的“坏味”,然后在不改变系统行 为的基础上,调整系统的内部结构,最终使其达到优化的目的。
如果让我们进行一次架构和设计重构,基本的操作思路就是按照下述的步骤贯穿整个 系统的各个子系统、构件和服务。
问题及背景识别:首先需要识别系统架构和设计中有哪些是可以优化和改进的部' 分,并澄清存在的问题、问題背景或原因。
改进方案:设想如果自己就是该系统的架构或设计人员,面对这样的问题时将会采 取怎样的设计方案。同时,参照业界经典的问题解决方案(架构重构模式)也是极 其有效的一种手段。
风格沿袭:将系统中存在类似问题的其他部分,用类似统一的方案来解决。这样做 的结果,会使系统架构和设计非常匀称和平衡。
上述的架构重构步骤,非常像是在使用一系列的设计模式来解决系统设计问题。其实, 要想掌握架构和设计重构技术,就是要经过多次实践,总结经验后,最终能够提炼出一些 “重构模式(Refactory Pattern)”的过程。即掌握问题的现象,并寻求问题的最佳解决方式。
针对系统架构和设计中的“坏味”,我们可以分别来看看总结出的那些“重构模式”到 底是如何将这些设计坏味去掉的。
重构横式1:实体重新命名
背景和动因:进行系统架构与设计时,系统中各个组成元素的名称对淸晰地表述系统
内部结构有着重要的意义。
问题:进行架构和设计时所界定出来的系统组成元素(子系统、构件、模块等)名称
使用混乱,不能很好地表达该系统元素的用途或语义,使系统结构难以理解。
解决方案:
在系统全局范围内,使用事先定义的命名规则和编码规则。
尽量使用容易使stakeholders理解的含有明显语义的名称来给系统元素命名。
•改变系统元素名称时,需要考虑该元素与其他系统元素间的引用关系,使元素名称 包含引用的概念。
举例:重构时,我们可以使用适当的名称来表达两个构件或服务间的关系,如图7-10 所示。
重构模式2:转移重复元素
背景和动因:根据面向对象的DRY (Don’t Repeat Yourself)基本原则,一些共用的
系统元素可以考虑封装起来,这样方便系统其他部分对其的重用。同时,这样的架构和设 计将使系统结构描述更加简捷直接。
问题:系统架构和设计中,同样的功能或系统元素屡次出现在很多其他系统元素中。 这样会降低系统的可理解性,并使这些相同功能的代码散布在整个系统设计当中,无论从设计或编码的角度上,都无疑增加了理解和维护的复杂性。
解决方案:
识别这些散布在系统结构当中的公共功能或公共任务。
汇总有哪些系统元素会利用这些公共部分。
尝试将这些共用部分从各个系统元素中转移到一个封装起来的系统元素中。
举例:我们可以使用一个新的构件来封装共享的功能,并移除原先那些构件中相同的 设计逻辑;取而代之的是以调用的方式来满足各个构件的功能引用,如图7-11所示。
重构模式3:利用抽象的层次结构
背景和动因:根据 Barbar Liskov 著名的 LSP (Liskov Substitution Principle)原则: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it",为了保持系统架构和设计中相似概念的一致性,从而
避免出现违反LSP原则的调用发生,应该考虑使用数据抽象的方式来使Object之间形成
自然的层次结构关系。
问题:架构重构期间,我们会发现有些设计单元所实现的功能非常相近。这些重复出 现的相似设计单元以及后继的相似功能的代码,会严重干扰系统架构一些概念的一致性, 从而降低了架构描述的简捷和可读性。如果从OO (Object Oritented)的视角来看,打乱 了 Open-Closed的基本原则。因为违反LSP原则的设计,会导致系统调用时需要和各种 Object变种进行紧耦合,从而形成难以应对的设计结构。
解决方案:
利用LSP为指导原則,引入一个“通用抽象”来封装那些相似通用的功能。
定义一个以“通用抽象”为基础的层次结构。
将其他具体变种的架构设计单元,按照层次结构依次进行衍生。
举例:按照LSP原则,组成以Windows为基础的抽象层次结构,以便衍生其他特定 的具体元素,如图7-12所示。
重构模式4:以适配(Adaption)方式替代中介方式(Mediation)
背景和动因:在系统运行中,一些对等的系统实体需要一个中央处理单元来协调它们 之间的运行先后顺序。这样集中管理的方式就是通常我们会用到的一种中介方式的系统 设计。
问题:架构重构期间,我们会发现一些过分使用这种集中式的设计。当对等系统实体 之间的交互顺序比较复杂的时候,适当地使用一些中介协调器(Mediator)不失为一种好 的选择。但是,这样集中管理的架构方式,对系统的可扩展能力、可修改能力有着明显的 抑制作用。因为每增加一个新的系统实体,我们就必须修改一系列的中介协调器 (Mediator),这明显非常不利于系统的维护。
解决方案:
在系统架构中增加一个“系统集成层”,系统实体(例如:构件或服务)完全是以 插件的方式集成在这个“集成层”中。
将这些对等实体的调用逻辑转移到该集成层之外的其他系统代码位置当中,系统通 过“系统集成层”来调用相应的构件或服务。
我们所熟悉的ESB ( Enterprise Service Bus )就是以这样方式来管理系统中的对等实体。
举例:将中介方式的架构设计转换为适配器方式,明显增强了系统的可扩展能力,如 图7-13所示。
重构模式5:合并子系统
背景和动因:一个大规模系统中存在一些子系统,这些子系统间或多或少存在着相互 耦合的关系。
问题:架构重构时,我们经常会见到一个系统内的各个子系统之间存在着紧耦合关系。 实践经验告诉我们,一个系统中的各个子系统之间,在架构时应该尽量保持相对的松耦合 关系;同时,一个子系统内的各个构件之间可以保持紧耦合关系。如果子系统间的耦合关 系太高,对系统整体架构而言,系统进行维护和修改的代价就会非常高,并且重用一个子 系统也会变得非常困难。这完全是由于紧耦合造成了子系统间复杂关系的结果。
解决方案:
两个子系统间的这种紧耦合关系,其实就意味着这两者是为了同样的功能目的而 :协作。这时可以考虑将这两个子系统进行合并。
举例:与其将紧耦合分成两个子系统,倒不如将其合并。这符合“子系统间松耦合, 子系统内紧耦合”的设计原则,如图7-14所示。
重构模式6:强化层间调用
背景和动因:目前系统架构经常会采用Layer的架构风格,这样分层的结构非常适于 系统功能的切分,使系统结构淸晰,从而方便了未来的维护。
问题:如果我们重构系统,经常会发现层间调用会出现跨层的现象。这种设计的出现, 完全打破了 Layer架构风格的优势,很容易造成架构的混乱。
解决方案:
重新构建层间关系,使系统保持严格逐层调用的架构风范。
举例:解决越层调用并不是一件非常容易的事,有时甚至需要进行商业逻辑重构才能解决,如图7-15所示。
重构模式7:以Message通信替代RPC
背景和动因:分布式系统中异步通信方式是一种非常有利的通信机制。
问题:重构系统时,经吊会发现大量使用远程方法调用RPC( Remote Procedure Call)
方式进行逋信,这无疑是一种最常见的通信实现方式。即便是在前端应用层可能需要的是
重构模式9:避免构件Interface的膨胀
背景和动因:有这样一个事实,那就是构件的接口(Interface)是会膨胀的。
问题:一个经历了数年维护的系统,随着新功能的增加,系统构件的接口也会随之变 化,这就导致了构件接口的膨胀。
现实中,一个软件应用,经常会出现利用接口这样的抽象方式,来尽可能地使该接口 的功能抽象满足各种各样的应用要求。如果我们熟悉“Swiss Army Knife Anti-Pattern”(瑞
士军刀有很多种刀刃,试图满足各种各样的刀具使用环境的需要)这种不好的设计模式, 就知道这种试图满足各种要求的接口设计,会导致系统的可用性、可维护性、可管理性明 显降低。最严重的情况是,这样膨胀的接口可能会导致系统代码的混乱。
解决方案:
可以考虑使用“Extension Interface”设计模式来重构系统的接口,即引入一个接口的层次结构关系,再提供一个公共的接口导航方式,以便客户端代码可以在一个层次结构的接口关系中寻找相应的功能实现。
举例:将一个膨胀的构件接口重构为层次化的结构,并提供寻找具体功能实现的导航 机制,如图7-18所示。
重构横式10:使用配置子系统/构件
背景和动因:一个系统在运行前,可能在各个层面(例如:各个子系统、构件、显示 层、数据库层等)都需要进行配置。
问题:重构前的系统,经常会发现有许多方面需要进行配置或参数调整。如果是手工 进行逐步配置,一方面需要配置的点很多,工作非常枯燥并且容易出现错误。另一方面, 如果配置项之间还存在着关联关系,就会增加出错的可能。如果从架构和设计上适当简化 系统配置要求,并部分实现自动化配置,就显得非常有价值。
解决方案:
在重构系统架构和设计时,尽量使每个子系统和需要配置的构件都具有一个可配置 的接口。
不再使用人工进行逐点的配置工作,以一个配里子系统或配置构件(可以参考 “Component Configurator”设计模式)来取代人工的配置工作。这个配里子系统/ 构件是以读取配置文件等方式来配置各个系统可选项。
•当一个事件发生或一个状态变化时,配置子系统/构件就会选择需要配置的点,通过 该点提供的接口进行肊置动作。
举例:通过事件的驱动,利用Configuration Subsystem/Component自动完成对各个
需要配置的子系统的配置工作,如图7-19所示。
117.4系统代码重构
Bertrand Meyer曾经说过“Code is all you need,,。虽然经过了系统架构和设计的重
构’系统的结构已经得到了很大程度的改善。但是,最终我们还需要进行一个更低层面但 绝对重要的重构工作:系统代码重构。
Martin Fowler在为我们引入代码重构概念的同时,也总结了很多经常能够遇到的所谓 系统代码的“坏味' 即我们在浏览一个系统代码后,通过经验及直觉就能发现的一些问题。
代码的Method过大。
•系统中重复的代码过多。
类的子类中存在大量相同Method。
代码中过多的注释。
•参數列表太长。
•……(注:在Martin Fowler的著作中还有很多“坏味”)
既然我们能够嗅到不同的代码“坏味”,那么一般我们会选择怎样的时机去解决这些问 题呢?通常,有如下四种时刻最合适进行代码重构。反之,如果随意选择代码重构的时刻, 则会使系统代码更加混乱。
当我们试图将新的功能代码集成进系统代码前。
•当调试系统代码发现Bug时。
•当我们进行常规代码评审时。
•系统架构和设计重构结束后。
需要强调一点,在我们真正动手前,明确代码重构通常所遵循的原则也有着极为重要 的意义。
•必须创建相应的大量测试用例和Test Suite。
代码重构要遵循小步调的工作规模(小计划、小构想、小改动、小测试)。
先拿问题最严重或最危险的部分开刀。
一定要利用测试进行验证。如果测试失敗,就需要重复进行上述动作。
执行代码重构,不是简单地依靠自己的直觉进行代码修改。往往还需要参考业界在代 码重构实践经验的基础上,提炼出来的那些类似于设计模式的“代码重构模式”来进行工 作。我们可以以典型的几个代码重构模式作为示例,来看看前人总结的经典经验。
代码重构模式1:方法抽取
背景和动因:代码结构需要简捷明了,否则混杂在一起的代码难以理解。
问题:浏览一个Class的代码,经常会发现该Class不同的方法代码中,会重复出现 很多完全一致的代码。这些重复的代码与各个方法内的功能代码混杂在一起,非常难以理 解该Class的功能结构。
解决方案:
将这些重复出现的代码抽取出来,汇总在该Class适当的新方法中。原先调用的位置,只留下对新方法的调用代码。
举例(如图7-21所示):
代码重构横式2:方法上移
背景和动因:面向对象编码应该尽量使用继承的特性,避免功能代码散布在整个继承 结构中。
问題:浏览一些类及其子类的代码,经常会发现这些子类中都存在着功能完全相同的 方法的代码。这明显不能满足OOP (Object Oritented Programming)的原则,势必造成
无论是架构和设计重构,还是系统代码重构,都是相互依赖和相互联系的。如果我们 进行架构和设计重构时,没有系统代码作为支撑,就不能完全掌握系统内部结构的关系。 同时,一次增量式的小规模设计重构,非常有可能成为触发后续系统代码相应部分的重构 动作。例如:我们对系统内一些元素的名称进行了修改,那么我们自然对相应的代码(包 括名称、类名称、方法名称等)进行修改。所以,在进行架构和设计重构时,如果能够详 细记录设计变化所涉及的代码,将对后续的代码重构非常重要。
重构,经常是牵一发而动全身的一个改造动作。所以,在实际操作过程中,应该尽量 采取小批量递增的方式,逐步进行系统设计和代码的改善。力图一次就完成一个翻天覆地 的变化,一定会出现适得其反的结果。
完成了正向工程中系统架构和设计重构、代码重构、文档重构、流程重构等这样的工 作后,正向工程的主要任务就已经圆满完成了。
免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删