《架构之美》摘录二
第2章 两个系统的故事:现代软件神话
软件系统就像一座由建筑和后面的路构成的城市——由公路和旅馆构成的错综复杂的网络。在繁忙的城市里发生着许多事情,控制流不断产生,它们的生命在城市中交织在一起,然后死亡。某些软件系统很幸运,创建时由有经验的架构师进行了深思熟虑的设计,在构建时体现出了优雅和平衡,有很好的地图,便于导航。
本章中的这两个系统特别有趣,因为它们有很大不同,尽管从表面上看非常相似:
<1>.它们具有相似的规模(大约500000行代码);
<2>.它们都是“嵌入式”消费音频设备;
<3>.每种软件的生态系统都是成熟的,已经经历了很多的产品版本;
<4>.两种解决方案都是基于Linux的;
<5>.编码采用C++语言;
<6>.它们都是由“有经验的”程序员开发的(在某些情况下,他们本应知道得更多);
<7>.程序员本身就是架构师。
2.1 混乱大都市
你们修筑、修筑,预备道路,将绊脚石从我百姓的路中除掉。——《以赛亚书》
第一个软件系统名为“混乱大都市”,既不是因为它好,也不是因为它让参与开发的人感到舒服,而是因为当第一次参与它的开发时,它交给的有价值的软件开发经验。工作起初并不顺利,但是你不能指望在加入一个新团队、面对新的代码集时会觉得很轻松。然而,日复一日(周复一周),情况却没有任何好转。
<1>.这些代码要花极长的时间来学习,没有显而易见的进入系统中的路径。这是个警告信号。
<2>.从微观的层面来说,也就是从每行程序、每个方法、每个组件来看,代码就是混乱而粗糙的叠在一起的。不存在一致性、不存在风格、也没有统一的概念能够将不同的部分组织在一起。这是另一个警告信号。
<3>.系统中的控制流让人觉得不舒服,无法预测。这又是一个警告的信号。
<4>.系统中有太多的“坏味道”,整个代码集散发着腐烂的气味,是在大热天里散发着刺激性气体的一个垃圾堆。这是一个清晰的警告信号。数据很少放在使用它的地方。经常是引入额外的巴罗克式缓存层,目的是试图让数据停留在更方便的地方。这就是一个警告信号。
<5>.当试图在大脑中建立“大都市”的全图时,没有人能解释它的结构:没有人知道它的所有层、它的藤蔓,以及那些黑暗、隔离的角落。实际上,没有人知道它究竟有多少部分是真正能工作的(它实际上靠的是运气和英雄式的维护程序员)。人们知道他们面对的那一小部分区域,但没有人了解整个系统。很自然,没有任何文档。这也是一个警告信号。
<6>.如果我们回顾创建“大都市”的公司的历史,它所处的状态是可以理解的(但是不可宽恕):这是一个初创的公司,快速提供许多新版本的压力很大。延期是不可容忍的——这会带来财务灾难。软件工程师被迫尽其极限,快速交付。所以代码是以一系列疯狂冲刺的方式叠在一起的。
(注意:不好的公司结构和不健康的开发过程将在糟糕的软件架构中得到反映。)
2.1.1 后果
<1>.不可理解
“大都市”的架构以及缺乏强制的结构,导致了一个很难理解的软件系统,实际上几乎不可能修改。新加入项目的团队成员会被复杂性惊呆,不能够搞清楚状况。坏的设计确实会招致在它上面叠加坏的设计(实际上它简直就是迫使你那样做),因为没有一种明智的方式可以扩展该设计。在没有能解决手上工作的方法之中,阻力最小的总会被采用,没有明显的办法来修复这些结构问题,所有只要能减少麻烦,就会扔进去新的功能。
(注意:重要的是要保持软件设计的品质。坏的架构设计会招致更坏的架构设计。)
<2>.缺乏内聚
系统的组件完全没有内聚性。每个组件本来都应该有一个定义良好的角色,但是它们却包含了一堆杂乱的、不一定相关的功能。这使我们很难确定组建存在的原因,也很难弄明白系统中已经实现了哪些具体的功能,严重影响了软件的品质和可靠性。 功能和数据都放在系统中错误的地方,“核心服务”却没有在系统的核心部分实现,而是由边远的模块来模拟实现。 进一步考察软件历史揭示原因:原来的团队中存在个人斗争,所以一些关键程序员开始创建他们自己的软件小帝国。他们把认为酷的功能放到他们的模块中,即使它不应该属于那里。为了做到这一点,他们又实现了更为巴罗克式的通信机制,把控制连回到正确的地方。
(注意:开发团队中健康的工作关系将直接有益于软件设计。不健康的关系和个性膨胀会导致不健康的软件。)
<3>.不必要的耦合
“大都市”没有清晰的分层。模块之间的依赖关系不是单向的,耦合常常是双向的。组件A会到达组件B的内部,目的是完成它的一项任务。在其他的地方,组件B又通过硬编码调用了组件A。系统没有最底层,也没有控制中心。它是整体式的一大块软件。 这意味这系统的各部分之间耦合非常紧密,在启动系统骨架就不得不创建所有的组件。单个组建的任何改变都会波及其他组件,需要修改许多依赖它的组件。这使得低层次的测试不能进行,代码层次间的集成测试也不能创建。
(注意:好的设计考虑到内部组件连接机制和连接数(以及连接性质)。系统的单个部分应该能够独立存在。紧耦合将导致不可测试的代码。)
<4>.代码问题
不良的顶层设计所带来的问题也影响到了代码层面。问题会引起其他问题(破窗理论)。因为没有通用的设计,也没有整体项目“风格”,所以也没有人关心共同的编码标准、使用共同的苦,或采用共同的惯例。组件、类和文件都没有命名惯例,甚至都没有共同的构建系统。 “大都市”最微妙而又严重的问题是重复。由于没有清晰的设计,也不清楚功能应该处于的位置,所以轮子在整个代码集中不断重复发明。一些简单的东西,如通用的算法和数据结构,在许多模块中重复出现,每种实现都带有自己的一些未知的缺陷和怪异的行为特征。 更多的软件历史考察揭示了原因:“大都市”开始是从一系列独立的原型拼接起来的,这些原型本该抛弃。“大都市”实际上是偶然形成的城市群。当代码组件缝合在一起时,组建之间匹配得不好。随着时间的退役,这种随意的缝合开始破裂,所以组件互相拉扯,导致代码集破碎,而不是和谐的协作。
(注意:松弛而模糊的架构将导致每个代码组件编写的不好,并且相互之间匹配得不好,它会导致重复的代码和工作。)
<5>.代码以外的问题
“大都市”内部的问题已经超越了代码集,在公司中其他的地方导致了混乱。不仅开发团队中有问题,而且架构的腐烂也影响到了支持和使用该产品的人。
a.开发团队
项目的新成员被复杂性惊呆了,不能够搞清楚状况。这很好的解释了为什么很少有新人能在公司待下来——员工流失率非常高。留下的人,非常努力工作,项目压力很大,规划新功能导致极大的恐惧。
b.缓慢的开发周期
由于维护“大都市”是一项恐怖的任务,所以即使是最简单的变更或“很小的”缺陷修复都不知道要花多少时间。管理软件开发周期非常难,客户只好等着实现重要的特征,管理层对开发团队不能满足业务目标感到越来越沮丧。
c.支持工程师
在支持这个极不寻常的产品时,产品支持工程师度过了可怕的时光,他们要设法弄明白很小的软件版本差异之间错综复杂的行为差异。
d.第三方支持
项目开发了一个外部支持协议,支持其他设备远程控制“大都市”。这只是 软件内部结构上面薄薄的一层,在“大都市”的架构上,这意味着难以理解的、容易偶尔出错的、不可能使用的。第三方工程师的生活也被“大都市”的可怕结构搞得一团糟。
e.公司内部政治
开发问题导致了公司内部不同“种族”的分裂。开发团队与营销团队之间关系紧张,每次新版本要推出时,制造部门总是要承受巨大的压力。经理们已经绝望了。
(注意:不良架构的影响不仅限于代码。它会进一步影响到人、团队、过程和时间表。)
<6>.清晰的需求
软件历史考察凸显了“混乱大都市”之所以混乱的一个重要原因:在项目开始之初,团队并不知道要构建的是什么。本来初创公司知道他们要占领哪个市场,但并不知道哪种产品能占领这个市场。所以要求一个可以做许多事情的软件平台。而不是创建一个把一件事情做好的架构,并能够在将来进行扩展,做更多的事情。
(注意:重要的是要在开始设计系统之前知道你打算设计什么。如果你不知道它是什么,也不知道它将做什么,暂时不要开始设计它。只涉及你知道需要设计的东西。)
2.1.2 现状
“大都市”的设计几乎完全是无药可救,曾尝试修复它。修复工作需要返工、重构、修改代码结构中的问题,这些已经成为不可能的任务。很难添加新的特性,所以人们只是忙着添加更多不完善的功能、救济补丁和编造谎言。没有人在面对代码时感到愉快,项目正盘旋着向下载。缺乏设计导致了不良的代码,从而又导致了不良的团队精神和不断变长的开发周期,这最终导致了公司严重的财务问题。最后,管理层宣布“混乱大都市”已经不盈利了,它被抛弃了。对于任何组织结构来说,这都是勇敢的异步,特别是这个公司一直眼高手低,同时又试图避免沉沦。
2.1.3 来自“大都市”的明信片
不良的架构会产生深远的影响和严重的反弹。在“混乱大都市”中缺少预见性和架构设计,导致了下面的问题:
<1>.低品质的软件和漫长的版本发布周期;
<2>.系统没有弹性,不能够适应变更或添加新的功能;
<3>.无处不在的代码问题;
<4>.员工问题(压力大、士气低、跳槽等);
<5>.大量混乱的公司内部政治;
<6>.公司不能成功;
<7>.许多痛苦和面对代码深夜加班。
2.2 设计之城
形式永远服从功能。——Louis Henry Sullivan
对于“设计之城”项目,使用有能力的开发者组成了一个全新的团队,从头开始构建这个产品。团队很小(开始4名程序员),团队的结构是扁平的。幸运的是项目中没有个人对抗,在团队中也没有任何争权夺利的事。在此之前,团队成员之间并不非常熟悉,不知道在一起可以配合的是不是很好,但是大家对这个项目都很热心,喜欢这项挑战。 从一开始,项目就有清晰定义的目标:具体的首个产品和将来功能的路线图,代码集必须能够支持这些功能。这将是一个通用目标的代码集,可以适用于多种产品配置。开发过程采用极限编程(XP),很多人相信这种开发过程避开了设计:直接开始编码,不要想太远。XP没有贬低设计,它贬低的是不必要的工作(即YAGNI原则,You Aren't Going To Need It)。它鼓励使用快速原型(所谓的“Spike”),快速展现并验证设计的有效性。这些都非常有用,对最终的软件设计产生了极大的影响。
2.2.1 设计之城的第一步
在设计过程的早期,我们确定了主要功能领域(这包括核心的音频通道、内容管理和用户控制/界面)。我们考虑了它们如何在系统中适配,推出了初步的架构,包括了实现性能需求所必需的核心线程模型。系统中各独立部分的相对位置关系体现为传统的分层结构。关于一些基本关注点的决定是这时候做出的,目的是确保代码能够容易而一致的增长,这些决定包括:
<1>.顶层文件结构;
<2>.我们如何对事物命名;
<3>.“内部”展示的风格;
<4>.共用的编码惯例;
<5>.选择单元测试框架;
<6>.支持基础设施(例如版本控制、适合的构建系统和持续继承)。
2.2.2 故事展开
在团队完成了初始设计之后,“设计之城”项目按照XP过程推进,设计和编码要么以结对的方式完成,要么经过仔细的复审,确保工作的正确性。产生的结果:
<1>.定位功能
由于从一开始我们就有系统结构的清晰总体视图,所以新的功能单元可以一致的添加到代码集的正确功能区域。代码应该属于哪一块从来就不是问题。在扩展功能或修复问题时,我们总是很容易找到已有功能的实现代码。所以,架构规划的存在有时候让开发者的功能变得更难一些。这些额外工作的回报就是今后的生活要容易很多,当我们维护或扩展系统时,不愉快的事情会很少。
(注意:架构有助于定位功能,添加功能、修改功能或修复缺陷。它为你提供了一个模块,让你将工作纳入到一张系统导航图中。)
<2>.一致性
整个系统是一致的。各个层次的所有决定都是在整个设计的背景下做出的。开发者从一开始就有意为之,这样得到的所有代码都完全符合系统设计,并与编写的所有其他代码相匹配。
(注意:清晰的架构设计将导致一致的系统,所有决定都应该在架构设计的背景下做出。)
顶层设计的好风格和优雅很自然会为较低层带来好处。即使在最底层,代码也是统一而整洁的。清晰定义的软件设计确保了没有重复,熟悉的设计模式到处使用,熟悉的接口惯例普遍采用,没有特殊的对象生命周期或奇怪的资源管理问题。代码是在城市规划背景之中写成的。
(注意:清晰的架构有助于减少功能重复。)
<3>.架构的增长
系统设计就像代码一样,被认为是可扩展、可重构的。开发团队的一项核心原则就是保持敏捷,没有什么是一成不变的,所以在需要时架构也可以修改。这促使我们让设计保持简单并易于修改。这样,代码就可以快速的增长,同时又保持好的内部结构。添加新的功能块不是问题。
(注意:软件架构不是一成不变的,需要时就改变它,要想做到可以修改,架构就必须保持简单,牺牲简单性的修改要抵制。)
<4>.延迟设计决定
一项XP原则对于“设计之城”的架构品质又提高,就是TAGNI(如果你不是马上需要,就不要去做)。这促使在早期只设计了重要的部分,将所有余下的决定推迟,知道我们对实际的需求有了更清晰的理解并指导如何放在系统中最好时,再做出这些决定。这是一种非常强大的设计方法,在很大程度上解放了我们的思想。
a.当你还不理解问题时就开始设计,这是一件糟糕的事。YAGIN迫使你等待,知道你真正的问题是什么,它应该怎样由设计来体现为止。这消除了猜测的工作,确保设计是正确的。
b.当你开始创建软件设计时就加入所有可能需要的东西是危险的。你的大部分设计工作会变成无用功,得到的只是额外的负担,你不得不在软件整个变更生命中支持这些设计。它一开始就增加了成本,而且在相貌的生命周期中不断增加成本。
(注意:延迟设计决定,知道你必须做出这些决定为止,不要在你还不知道需求的时候就做出架构决定,不要猜测。)
<5>.保持品质
从一开始,“设计之城”就准备好了一些品质控制过程:
a.结对编程;
b.对没有结对编程的工作进行代码/设计复审;
c.对每一段代码进行单元测试。
这些过程确保了系统中从未假如不正确的、不合适的变更。开发者坚信这个过程,这种信念凸显了一种重要的态度:开发者们相信设计,认为设计对项目相当主要。他们拥有设计,对设计负责。
(注意:必须保持架构品质,只有当开发者们相信它并对它负责时,才能做到这一点。)
<6>.管理技术债务
除了这些品质管理方法之外,“设计之城”的开发是相当注重实效的。随着最后期限的临近,一些不重要的功能被砍掉,让产品能够准时推出。小的代码“瑕疵”或涉及问题允许存在于代码集中,要么是为了功能快一点实现,要么是为了在接近发布时避免高风险的改动。但这些地方都被标记为技术债务,并安排在后续的版本中修正,知道将他们处理掉为止。同样,看到了开发者对涉及的品质负责。
<7>.单元测试打造设计
关于代码集的一项核心决定就是所有代码都要有单元测试(这也是XP开发中强制要求做到的)。单元测试有许多好处:
a.其中一点就是能够修改软件的一些部分,而不必担心在改进的过程中破坏其他的东西。在对“设计之城”内部结构的某些部分进行了相当激进的返工,单元测试给了信心,相信系统的其他部分没有被破坏。系统开发的很快,一叠带的方式进行,每一次迭代都改进了设计,直到它达到了相对稳定的状态。
(注意:你的系统应该有一组不错的自动化测试,它们让你在进行根本的架构变更时风险最小。这为你提供了工作的空间。)
b.另一个好处在于,它们在很大程度上定型了代码设计:它们实际上迫使我们设计好的结构。每个小的代码组件都被定型成定义良好的实体,可以独立存在,因为它必须能够在单元测试中构造出来,不需要围绕它构造系统的其它部分。编写单元测试确保了每个代码模块的内聚性,也确保了与系统其他部分之间的松耦合。单元测试迫使我们仔细考虑每个单元的接口,确保该单元的API是有意义的,内部是一致的。
(注意:对你的代码进行单元测试将带来更好的软件设计,所以设计时考虑可测试性。)
<8>.设计时间
“设计之城”成功的另一个原因是分配的开发时间段,它既不长也不短。项目需要一个有利的环境才能获得成功。如果时间太多,程序员常常会想创建他们的巨作(那种总是快要好了,但永远不会实现的东西)。有一点压力是好事,紧迫感有助于完成事情。但是,如果时间太少,就不可能得到任何有价值的设计,你只会得到半生不熟的解决方案。
(注意:好的项目计划将带来优质的设计,分配足够的时间来创建架构杰作,它们不会立即出现。)
<9>.与设计同行
尽管代码集很大,但它是一致而易于理解的。新的程序员可以比较容易的拿起代码并开始工作,不需要去理解不必要的复杂内部关系,也不需要面对奇怪的遗留代码。 由于代码中产生的问题比较少,工作起来有乐趣,所以团队人员的流失率很低。这是因为开发者们负责设计,并不断希望改进它。 “设计之城”的项目原则规定没有人“拥有”哪一部分设计,这意味着任何开发者都可以改动系统的所有地方。每个人都应该写出高品质的代码。 “设计之城”则是由密切合作的同时创建的一组干净、一致、密切合作的软件组件。在很大程度上,Conway法则反过来也生效,团队的组织方式就像软件的组织方式一样。
(注意:团队的组织方式必然对它产生的代码有影响,随着时间的推移,架构也会影响到团队协作的好坏。当团队瓦解时,戴梦得交互就很糟糕,当团队协作时,架构就集成的很好。)
2.2.3 现状
“设计之城”的架构图,与最初的设计非常相似,同时包含了一些值得注意的变更。此外,还包含了大量的经验,证明了这个设计是正确的,健康的开发过程,小的、更善于思考的开发团队,适当注意确保一致性,带来了极为简单、清晰、一致的设计。这种简单性为“设计之城”带来了好处,得到了可扩展的代码和快速开发的产品。 需要澄清的一点是:这些代码并不完美。有些地方存在着技术争论,但是它们在整洁的背景下显得特别突出,会在将来得到解决。没有什么是一成不变的,由于适应性强的架构和灵活的代码结构,这些问题都可以解决。几乎所有东西都各就各位,因为架构很好。
2.3 说明什么问题
等那完全的来到,这有限的必归于无有了。——《哥林多前书》
这个关于两个软件系统的简单故事当然不是软件架构的全面介绍,但已展示了架构如何对软件项目产生深远的影响,架构几乎会影响所有与之相关的人和事,它决定了代码集的健康,也决定了相关领域的健康。就像一个繁荣的城市会为当地带来成功和声望,好的软件架构将帮助项目获得发展,为依赖于它的人带来成功。 好的架构师很多因素的结果,包括以下方面(但不限于此):
<1>.确定进行有意为之的前端设计。(许多项目甚至还没有开始,就因为这一点而失败了。)
<2>.设计者的素质和经验。(以前犯过一些错误是有帮助的,这能在下一次为你指出正确方向!“大都市”项目肯定是教会一些东西的。)
<3>.在开发过程中,保持清晰的设计观点。
<4>.授予团队负责软件的整体设计,而团队也承担起这一责任。
<5>.不要害怕改变设计,没有什么是一成不变的。
<6>.让合适的人加入到团队中,包括设计者、程序呀un和经理,确保开发团队的规模合适。确保他们具有健康的工作关系,因为这些关系将不可避免的影响代码的结构。
<7>.在合适的时候做出设计决定,当你知道所有必要信息时再作出决定。延迟这些暂时不能做出的决定。
<8>.好的项目管理,以及合适的最后期限。
2.4 轮到你了
绝不要失去神圣的好奇心。 ——阿尔伯特,爱因斯坦
你正在读者本书是因为你对软件架构感兴趣,而且你对改进自己的软件感兴趣。所以这里就有一个极好的机会。对于你目前的软件经验,请考虑一下简单的问题: (1).什么是你看到过的最好的系统架构?
<1>.你怎么知道它是好的?
<2>.这个架构在代码集之内和之外带来了什么结果?
<3>.你从中学到了什么?
(2).什么是你看到过的最差的系统架构?
<1>.你怎么知道它是差的?
<2>.这个架构在代码集之内和之外带来了什么结果?
<3>.你从中学到了什么?