第十五章 架构是什么?
“架构”这个词唤起了人们对力量和神秘的愿景。这使我们想到重大的决定和深厚的技术实力。软件架构处于技术成就的顶峰。当我们想到一个软件架构师的时候,我们想到了一个有权力的人,并且尊敬的人。年轻有抱负的软件开发人员怎会不想有一天成为软件架构师呢?
但什么是软件架构?软件架构师做什么,什么时候做?
首先,软件架构师是程序员,并依然是程序员。别轻信传言说软件架构师从代码中撤回,专注于更高层次的问题。他们不是!软件架构师是最好的程序员,他们不断地承担编程任务,同时他们也有引导团队的其他成员进行最大化生产力的责任。软件架构师可能不会像其他程序员那样编写尽可能多的代码,但是他们继续参与编程任务。他们这样做是因为如果他们没有体会困扰其他程序员的问题,他们就不能很好地完成工作。
软件系统的架构是构建它的系统赋予该系统的形状。这个形状的形式是将该系统划分为组件,以及这些组件的布局以及这些组件之间的通信方式。
这个形状的目的是为了便于软件系统的开发,部署,操作和维护。
这一便利化的策略是尽可能多地开放尽可能多的选项。
也许这句话让你感到吃惊。也许你认为软件架构的目标是使系统正常工作。当然,我们希望系统能够正常工作,系统的架构当然必须支持这个目标作为最高优先级之一。
但是,系统的体系结构对该系统是否正常运行影响不大。比如有很多系统,虽然架构糟透了,但运行得很好。他们的烦恼不在于他们的操作;相反,麻烦发生在其部署,维护和持续开发中。
这并不是说架构对支持系统的正确行为没有任何作用。当然有作用,而且这个角色是至关重要的。但是这个角色是被动和锦上添花的,而不是主动的或者是必要的。如果有的话,系统的架构可以保持开放的行为选项很少。1
架构的主要目的是支持系统的生命周期。良好的架构使系统易于理解,易于开发,维护简单,易于部署。最终目标是最大限度地减少系统的生命周期成本,并最大限度地提高程序员的生产力。
开发
一个很难开发的软件系统是不太可能有个长寿而健康的生命周期的。所以系统架构应该时这个开发系统的团队容易去开发。
不同团队结构意味着不同架构的决策。一方面,一个五人开发者的团队可以十分有效率地协作开发一个月周期的系统,而不需要精心定义组件和接口。事实上,这样的团队很可能会在早期的开发中,在架构结构层面发现一些障碍。这就是为什么许多的系统缺少良好架构的原因:他们从无开始,因为这个团队很小,不需要上层建筑的障碍。
另一方面,一个系统同时由五个团队开发,每个团队包含七名开发者,除非把系统拆分成定义良好的可靠接口的组件,不然进度堪忧。如果没有考虑这些因素,系统的架构很可能变成每个团队各自开发的五个组件。
每个团队各自开发的五个组件的架构不太可能有最好的系统的部署、运行、维护的架构。尽管如此,这也是一种开发架构,每个团队各自有独立地开发进度计划,多个团队共同开发。
部署
为了有效率,一个软件系统一定是可部署的,部署代价越高,系统就越难用。软件架构的一个目标是使得系统可以容易部署,一键部署(with a single action)!
不幸的是,在初始开发阶段,很少考虑部署策略。这导致了一个架构可能容易开发,但极难部署。
比如,在早期系统开发,开发者决定用“微服务架构”,他们觉得这种方式让系统变得容易开发,因为组件边界是确定的,接口也相对稳定。但是,到了部署系统的时候,他们发现微服务的数量变得有点可怕了:配置微服务之间的连接,以及它们启动的时间,也可能变成一个巨大的错误来源。
如果架构师尽早考虑部署问题,他们可能会决定减少服务,混合使用服务和在线处理的组件,并采用更为集成的方式来管理互连。2
运行
架构对系统运行的影响比对开发、部署、维护的影响要小,几乎所有运行的问题都可以靠增加硬件解决而不影响到软件架构。
确实,我们多次看到这样的情况了。架构不够有效的系统软件常常可以通过怎家存储,增加服务器资源来提高效能。硬件是廉价的而人力是昂贵的意味着架构对处理运行的障碍的代价并不会大于对开发、部署、维护的障碍的代价。
这并不是说良好的架构对系统的运行不重要。重要的,但只是会在开发、部署、维护话更多代价。
讲到这里,架构在系统运行打的另一个作用是:一个好的软件架构可以传达系统的运行需求。
也许更好的说法是,对于开发者来说系统的体系架构使得系统运行是显而易见的。 架构应该揭示运行。 系统的体系架构应该将系统的用例,功能特性和所需的行为提升到开发人员可视的标记的第一类实体。 3这简化了对系统的理解,因此大大有助于开发和维护。
维护
软件系统的所有方面里,维护的成本最大。新功能的不断迭代和必然的缺陷修正消耗了大量的人力资源。
维护的主要成本是在探索(spelunking)和风险。 探索是挖掘现有软件的成本,试图确定添加新功能或修复缺陷的最佳位置和最佳策略。 但在做出这样的改变的同时,造成不经意的缺陷的可能性总是存在,增加了风险成本。
经过仔细思考的架构大大减轻了这些成本。 通过将系统分成组件,并通过稳定的接口隔离这些组件,可以为未来的功能特性打好基础,并大大降低无意中破坏的风险。
保持选项开放
如我们在之前章节的描述,软件有两类值:它的行为值和它的结构值。第二个值相较而言更重要,因为它使得软件“软”。
软件的发明是因为我们需要一种快速方便地改变机器行为的方法。但是,这种灵活性关键取决于系统的形状,组件的布置以及这些组件的相互连接方式。
软件保持软性的方法是尽可能多地打开选项。我们需要开放哪些选项?他们是不重要的细节。
所有的软件系统都可以分解为两大部分:策略和细节。策略要素体现了所有的业务规则和过程。该策略是系统的真正价值所在。细节是使人类,其他系统和程序员能够与策略沟通,但不影响策略行为的必要条件。它们包括IO设备,数据库,Web系统,服务器,框架,通信协议等等。
架构师的目标是为系统创建一个形状,将策略视为系统中最重要的元素,同时使细节与该策略无关。这使得关于这些细节的决定一直被延后。
例如:
- 在开发初期不需要选择数据库系统,因为高层策略不应该关心使用哪种数据库。事实上,如果架构师非常小心,那么高层的策略就不会在乎数据库是关系型的,分布式的,分层的还是纯粹的文本文件。
- 没有必要在开发早期选择Web服务器,因为高层策略不应该知道它是否通过Web传送的。如果高层策略不敏感HTML,AJAX,JSP,JSF,或者其他任何网站开发的一堆像字母表开头的技术,那么可能直到项目的后期,你都不需要决定使用哪个Web系统。事实上,你甚至不需要决定系统是否通过网络传送。
- 在开发初期就没有必要采用REST,因为高层策略应该与外界的接口不可知。也没有必要采用微服务框架或SOA框架。再次,高层政策不应该关心这些事情。
- 在开发早期不需要采用依赖注入框架,因为高层策略不应该关心依赖关系是如何解决的。
我认为你说对了。如果您可以制定高层策略而不承诺相关的细节,则可以推迟并延迟有关这些细节的决定。等待做出这些决定的时间越长,你就有越多信息去做出正确的决定。
这也让你选择尝试不同的实验。如果你拥有高层策略的一部分,并且对数据库不敏感,则可以尝试将其连接到多个不同的数据库以检查适用性和性能。 Web系统,Web框架甚至Web本身也是如此。
你选择的时间放得越长,你可以进行的实验越多,你可以尝试的事情就越多,当你达到决定不再延期的时候,你决定时获得的信息也越多。
如果这些决定是由其他人做出的呢?如果贵公司已经对某个数据库,某个Web服务器或某个框架作出限制,该怎么办?一位优秀的架构师假装还没有这些限制,并且形成了这个系统,使得这些决定仍然可以被推迟或者尽可能地改变。
一个好的架构师可以最大化不做决定的数量。4
设备独立性
作为这种想法的一个例子,让我们回到20世纪60年代,当时计算机还是是“青少年”,大多数程序员是数学家或其他学科的工程师(三分之一以上是女性)。
那时候我们犯了很多错误。当然,我们当时并不知道他们是错误的。 我们怎么了呢?
其中一个错误是将我们的代码直接绑定到IO设备。如果我们需要在打印机上打印某些东西,我们编写了使用控制打印机的IO指令的代码。我们的代码依赖于设备。
例如,当我编写打印在电传打印机上的PDP-8程序时,我使用了一组如下所示的机器指令:
PRTCHR, 0
TSF
JMP .-1
TLS
JMP I PRTCHR
PRTCHR
是一个在电传打印机打印字符的子例程。起始零被用作返回地址的存储器。(不深究。)如果电传打印机准备打印字符,则TSF
指令跳过下一条指令。如果电传打字机忙,那么TSF
就会直接跳到JMP.-1
指令,这个指令又回到TSF
指令。如果电传打字机准备就绪,则TSF
将跳到TLS
指令,该指令将A寄存器中的字符发送到电传打印机。然后JMP I PRTCHR
指令返回给调用者。
起初这个策略运作良好。如果我们需要从读卡器读取卡片,我们使用直接与读卡器交谈的代码。如果我们需要打卡,我们编写了直接操纵打卡的代码。程序运作完美。这是有什么问题呢?
因为大批量的卡片难以管理。他们可能会丢失,残缺,纺织,洗牌或丢弃。个别卡可能会丢失,而且可能插入额外的卡。所以数据完整性成为一个重要的问题。
磁带是解决方案。我们可以将卡片承载的数据转移到磁带上。如果卸掉了磁带,记录不会被打乱。你不会意外丢失记录,只需简单处理磁带就可以插入空白记录。磁带更安全。读取和写入速度也更快,制作备份副本也非常简单。
不幸的是,我们所有的软件都是为了操作读卡器和打卡而编写的。这些程序必须改写为使用磁带。这是一个很大的工作。
到了二十世纪六十年代后期,我们已经吸取了教训——我们发明了设备独立性。当时的操作系统将IO设备抽象成软件功能,处理看起来像卡片的单元记录。程序将调用处理抽象单元记录设备的操作系统服务。操作员可以告诉操作系统这些抽象服务是否应该连接到读卡器,磁带或任何其他单位记录设备。
现在相同的程序可以读写卡片,或者读写磁带,而不需要改变。开闭原则诞生了(但尚未命名)。
垃圾邮件
在20世纪60年代后期,我曾为一家为客户打印垃圾邮件的公司工作。客户会给我们发送带有客户姓名和地址的单位记录的磁带,我们会写出打印好个性化广告的程序。
你知道那种:
马丁先生,
祝贺你!
我们选择住在Witchwood Lane的所有其他人来参加我们的新奇妙的一次性活动...
客户会寄给我们大量的信息,除了名字和地址,以及他们想要我们来打印的任何其他元素。我们编写了从磁带上提取名称,地址和其他元素的程序,并将这些元素正好印在表格上需要显示的地方。
这些堆卷纸重500磅,包含数以千计的信件。客户会送我们数百磅卷纸。我们会分别打印每一个。
起初,我们有一台IBM 360在其唯一的行式打印机上进行打印。我们每班可以打印几千封信。不幸的是,这在很长一段时间里这是一台非常昂贵的机器。在那些日子里,IBM 360每月租用数万美元。
所以我们告诉操作系统使用磁带而不是行式打印机。我们的程序并不在意,因为操作系统执行写入操作用的是IO抽象。
这台360可以在10分钟左右抽出一个完整的磁带——足以打印几卷字母。磁带被拿到电脑室外面,安装在连接到离线打印机的磁带机上。我们有五个,我们每周七天,每天24小时运行这五台打印机,每周打印数十万封垃圾邮件。
设备独立性的价值是巨大的!我们可以编写我们的程序,而不需要知道或关心使用哪个设备。我们可以使用连接到计算机的本地行式打印机来测试这些程序。然后我们可以告诉操作系统“打印”到磁带上,并运行成千上万的表单。
我们的程序有一个形状。这形成了与细节脱节的策略。该策略是格式化的名称和地址记录。细节是设备。我们推迟了我们将使用哪种设备的决定。
物理地址
在二十世纪七十年代初期,我为当地的一个卡车工会做一个大型的会计系统。我们有一个25MB的磁盘驱动器,我们存储Agents
,Employers
和 Members
记录。不同的记录具有不同的大小,所以我们格式化磁盘的前几个磁盘,以便每个扇区只是Agent
记录的大小。接下来的几个柱面被格式化以便有足够的扇区符合Employer
记录。最后几个柱面被格式化以符合Member
记录。
我们编写软件来了解磁盘的详细结构。已知磁盘有200个圆柱和10个磁头,每个磁头每个磁头有几十个扇区。已知哪些柱面拥有Agents
, Employers
和Members
。所有这些都被硬编码到代码中。
我们在磁盘上保留了一个索引,使我们能够查找每个Agents
, Employers
和Members
。这个索引是在磁盘上的又一个特别格式化的一组柱面。Agents
索引由包含其ID的记录以其记录的柱面号,磁头号和扇区号组成。Employers
和Members
有类似的索引。Members
也被保存在磁盘上的双向链接列表中。每个Members
记录保存了下一个会员记录的柱面号,磁头号和扇区号,以及前一个的Members
记录。
如果我们需要升级到一个新的磁盘驱动器,一个更多的磁头,一个更多的磁道,或者每个磁道更多的扇区,会发生什么?我们必须编写一个特殊的程序来读取旧磁盘中的旧数据,然后将它写出到新磁盘中,将所有的柱面/磁头/扇区号翻译出来。我们也必须改变我们的代码中的所有硬关联,而且硬关联无处不在!所有的业务规则都详细地用到了柱面/磁头/扇区的模式。
有一天,一个更有经验的程序员加入我们的行列。当他看到我们所做的事情时,脸色胀红,他瞪着我们,好像我们是外星人一样。然后,他有礼貌地建议我们改变我们的寻址方案,使用相对地址。
我们聪明的同事建议,我们应当把磁盘当做一个巨大的线性数组,每个扇区都可以通过一个连续的整数进行寻址。然后,我们可以写一个知道磁盘物理结构的小转换例程,并且可以将相对地址转换为柱面/磁头/扇区号。
幸运的是,我们接受了他的建议。我们改变了系统的高层策略,对磁盘的物理结构是不可知的。这使我们能够将关于磁盘驱动器结构的决定从应用程序中分离出来。
小结
本章中的两个故事就是架构师在大范围内采用的一个小原则的例子。优秀的架构师将细节与策略细分,然后将策略与细节彻底脱钩,以至于策略不了解细节,也不以细节为依据。 优秀的架构师设计策略,以便有关细节的决定可以推迟得尽可能长。
1. There are few, if any, behavioral options that the architecture of a system can leave open. ↩
2. Had the architects considered deployment issues early on, they might have decided on fewer services, a hybrid of services and in-process components, and a more integrated means of managing the interconnections. ↩
3. Architecture should reveal operation. The architecture of the system should elevate the use cases, the features, and the required behaviors of the system to first-class entities that are visible landmarks for the developers. ↩
4. A good architect maximizes the number of decisions not made. ↩