13. 逻辑架构

有没有一种方法在大产品和小团队之间的缺口上架起一座桥梁呢?答案是肯定的,有!那就是架构。架构最重要的一点,就是它能把难以处理的大问题分解成便于管理的小问题。 -- Eric Brechner,《代码之道》

一流是每个程序设计人员向往并为之奋斗却又无法具体说出的、难以达到的境界,一流的软件非常简明。它灵活而清晰,能通过创造性的机制解决复杂的问题,这些机制语义丰富,可应用于其他可能完全无关的问题,一流意味着寻求恰当的抽象,意味着通过新的途径合理利用有限的资源。 -- Grady Booch,《面向对象项目的解决方案》

划分子系统、定义接口......,这些典型工作都是属于逻辑架构设计的范畴。

接下来,我们主要说说5视图方法中逻辑架构视图的设计:

先从划分子系统的3种必用手段讲起
随后,纠正“我的接口我做主”这种错误认识,代之以“协作决定接口”的正确理解
而且,接下来将解析逻辑架构设计的整体思维套路,解决架构师郁闷已久的“多视图方法只讲做什么、不讲怎么做”的问题
最后,总结逻辑架构设计的10条经验要点。

13.1. 划分子系统的3种必用策略

架构师最缺的不是理论,也不是技术,而是位于理论和技术之间的“实践策略”和“实践套路”。

就划分子系统这个架构师必须做的工作而言,其实实践策略可以归纳为3种:

  • 分层的细化
  • 分区的引入
  • 机制的提取

13.1.1. 分层(Layer)的细化

分层是最常用的架构模式:在架构设计初期,100%的系统都可以用分层架构,就算随着设计的深入而采用了其他架构模式也未必和分层架构矛盾。

于是,架构师在划分子系统时常受到初期分层方式的影响--实际上,很多架构师最熟悉、最自然的划分子系统的方式就是:分层的细化。

3层架构或4层架构的“倩影”经常出现在投标时,或者市场彩页中,于是有人戏称之为“市场架构”。的确,直接用3层架构或4层架构来支持团队的并行开发是远远不够的。所以,“分层的细化”是划分子系统的必用策略之一,架构师们不要忘记。

13.1.2. 分区(Partition)的引入

序幕才刚刚拉开,划分子系统的工作还远远没有结束。

迭代式开发挺盛行,但所有真正意义上的迭代开发,都必须解决这样一个“困扰”:如果架构设计中只有“层”的概念,以“深度优先”的方式完成一个个具体功能就不可能的!

所以,工程师们就经常遇到这样的烦恼

例如《代码之道》一书中就论及了这一点:

为了得到客户经常性的反馈,快速迭代有个基本前提:开发应该是“深度优先”,而不是“广度优先”。

广度优先极端情况下意味着每一个功能进行定义,然后对每个功能进行设计,接着对每个功能进行编码,最后才能对所有功能一起测试。而深度优先极端情况下意味着每个功能完整的进行定义、设计、编码和测试,而只有当这个功能完成之后,你才能做下一个功能。当然,两个极端都是不好的,但深度优先要好的多。对于大部分团队来说,应该做一个高级广度设计,然后马上转到深度优先的底层设计和实现上去。

为了支持迭代开发,逻辑架构设计中必须(注意是必须)引入分区。分区是一种单元,它位于某个层的内部,其粒度比层 要小。一旦架构师针对每个层进行了分区设计,“深度优先”式的迭代开发就非常自然。

架构是迭代开发的基础。架构师若要在“支持迭代”方面不辱使命,必须注重“分区的引入”--这也是划分子系统的必用策略之一。

13.1.3. 机制的提取

Grady Booch在他的著作中指出:

机制才是设计的灵魂所在......否则我们就不得不面对一群无法相互协作的对象,它们相互推搡着做自己的事情而不关心其他对象。

机制之于设计是如此的重要。那么,什么是机制呢?

那“机制”的定义是:软件系统中的机制,是指预先定义好的、能够完成预期目标的、基于抽象角色的协作方式。机制不仅仅包含协作关系,同时也包含了协作流程。

对于面向对象方法而言,“协作”可以定义为“多个对象为完成某种目标而进行的交互”,而“协作”和“机制”的区别可以概况为:

基于接口(和抽象类)的协作是机制,基于具体类的协作则算不上机制。

对于编程实现而言,在没有提取机制的情况下,机制是一种隐式的重复代码--虽然语句直接比较并不相同,但是很多语句只是引用的变量不同,更重要的是大段的语句块结构完全相同。如果提取了机制,它在编程层面体现为“基于抽象角色(OO中就是接口)编程的那部分程序”。

对于逻辑架构设计而言,机制是一种特殊的子系统--架构师在划分子系统时不要忘记这一点。最容易理解的子系统,是通过“直接组装”粒度更小的单元来是吸纳软件的”最终功能“。在实现不同的最终功能时,可以重用同一个机制,避免重复进行繁琐的”组装“工作。例如,网络管理软件中拓扑显示和告警通知都可利用消息机制。

13.1.4. 总结:回顾”三维思维“

至此,我们讨论了划分子系统的3种手段:分层的细化分区的引入机制的提取 。通过这3种手段的综合运用,就可更理性、更专业的展开逻辑架构的设计。

如何通过关注点分离来达到“系统中的一部分发生了改变,不会影响其他部分”的目标呢?

首先,可以通过职责划分来分离关注点,面向对象的关键所在,就是职责的识别和分配。每个功能的完成,都是通过一系列职责组成的“协作链条”完成的;当不同职责被合理分离之后,为了实现新的功能只需构造新的“协作链条”,而需求变更也往往只会影响到少数职责的定义和实现......

其次,可以利用软件系统各部分的通用性不同进行关注点分离。不同的通用程度意味着变化的可能性不同,将通用性不同的部分分离有利于通用部分的重用,也便于专用部分修改......

另外,还可以先考虑大粒度的子系统,而暂时忽略子系统是如何通过更小粒度的模块和类组成的......

总结了上述的架构设计关注点分离原理。可以说,根据职责分离关注点,根据通用性分离关注点,根据不同粒度级别分离关注点是三种位于不同“维度“的思维方式,所以在实际工作中必须综合运用这些手段。

于是,不难理解分层的细化分区的引入机制的提取 这3种划分子系统手段之间的关系:它们处在思维的3个维度上

首先,分层和机制位于不同的维度:职责维度和通用维度。

另外,是否引入分区,设计所“覆盖”的Scope是完全相同的。原因是层的粒度较大,而是层内部引入的分区的粒度更小,便于组合出一个个功能(支持迭代开发)。这是第三维:粒度。

看来,分层的细化、分区的引入、机制的提取这3个手段不是相互替代的关系,而是相辅相成的关系。

13.1.5. 探究:划分子系统的4个重要原则

重要的内容就值得多讲几遍。

下面的分层的细化、分区的引入、机制的提取这3种策略背后的4个通用设计原则:

  • 职责不同 的单元规划不同子系统
  • 通用性不同 的单元划归不同子系统
  • 需要不同开发技能 的单元划归不同子系统
  • 兼顾工作量的相对均衡 ,进一步切分到多个原则综合作用的结果

13.2. 接口设计的事实与缪误

世界是复杂的,很多东西难以直接获取。

例如直接最强幸福,是永远追不到的(《乐在工作》一书中说幸福是副产品)。

殊不知,合理的接口设计也不是“直接”得到的

由于面向对象非常强调“自治”,许多人不知不觉的形成了一种错误认知:面向对象推崇“我的接口我做主”。很遗憾,“自治”正确。但“我的接口我做主”这个推断是错误的。

软件世界中本无模块。1968年,Dijkstra发表了第1篇关于层次结构的论文《The Structure of THE-multiprogramming System》(原文PDF)。1972年,Parnas发表论文《On the Criteria To Be Used in Decomposing Systems into Modules》论及了模块化和信息隐藏的话题......这是架构学科开始萌芽的标志。

那么,为什么要对软件进行模块化设计呢?是为了解决复杂性更高的大问题。于是,我们突然领悟:对问题进行分解,分别解决小问题,其实这只是手段。每个架构师应该牢记:

“分”是手段,“合”是目的 。不能“合”在一起支持更高层次功能的模块,又有何用呢?

因此,我们必须把模块放在协作的上下文之中进行考虑。架构师设计接口时,要考虑的重点是“为了实现软件系统的一系列功能,这个软件单元要和其他哪些单元如何协作”,总结成一句话就是:

协作决定接口

相反,直接设计接口,是很多“面向接口的”架构依然拙劣的原因之一。类似“我的接口我做主”的观点是错误的,每个模块或子系统(甚至类)无视协作需要而进行的接口定义很难顺畅的被其他模块或子系统使用。

13.3. 逻辑架构设计的整体思维套路

13.3.1. 整体思路:质疑驱动的逻辑架构设计

要点如下:

  • 质疑驱动
  • 结构设计和行为设计相分离

模板不是一天建成的。需求对架构设计的“驱动”作用,是伴随着架构师“不断设计中间成果 -> 不断质疑中间成果 -> 不断调整完善细化中间成果 ”的过程渐进展开的。打个比方,需求就像“缓释胶囊”,功能、质量、约束这3类“药物成分”的药力并不是一股脑释放的,而是缓缓释放的--“缓释”的控制者必然是人,是架构师

“药力释放机理” -- 逻辑架构设计的整体思维套路

先考虑结构方面的切分。手段是上面所讲的分层的细分、分区的引入、机制的提取。

然后,让切分出的职责协作起来,验证能否完成功能。这个工作,可以借助时序图进行。

此时,结构和行为方面各进行了一定的设计,就应开始质疑自己的设计。架构师要从两个角度质疑:

  • 功能方面,特殊的功能支持吗?
  • 质量方面,耦合性、重用性、性能等怎么样?

如此循环思维,不断将设计推向深入......其间,会涉及接口的定义,建议用“包-接口图”作为从结构到行为过度的桥梁,从而识别接口。至于接口的明确定义(接口包含的方法为何),则要进一步考虑基于职责的具体交互过程。

13.3.2. 过程串联:给初学者

第1步,根据当前理解切分。质疑驱动的逻辑架构设计整体思路,是从运用分层的细化、分区的引入、机制的提取进行子系统划分开始。

第2步,找到某功能的参与单元。弱找不到明显缺单元,就可以直接返回到第1步,以补充遗漏的职责单元。

第3步,让它们协作完成功能。研究第2步找到的参与单元之间的协作关系,看看能否完成预期功能,完成的怎么样?

第4步,质疑并推进设计的深入。通过质疑“对不对”和“好不好”,可以发现新职责,或者调整协作方式。这意味着,第1步的子系统切分方案被调整、被优化......如此循环。

13.3.3. 案例示范:自己设计MyZip

MyZip的概念架构设计,它将和需求一起,影响MyZip的细化架构设计。

下面主要是演示如何以质疑驱动的思路,设计MyZip的逻辑架构视图。

首先,考虑结构方面的切分,3种划分子系统的手段都运用上。

  • 分层的细分。压缩实现层从原来的压缩控制层中分离出来。回忆一下之前的“子系统划分策略背后的4大原则”。无论是从职责不同的角度,还是从所需技能的角度考虑,两者都应该分离成为单独的“子系统”。
  • 分区的引入。界面交互层必须进一步分区,例如:支持右键菜单的“Windows外壳扩展”部分被独立。
  • 机制的提取。例子是智能缓冲机制,它应该成为一个通用性的基础子系统。同时,为了使它可重用,缓冲区不负责“缓冲区已满”时的具体处理而是毁掉外部单元进行。再者,为了提高使用友好性,缓冲区具有一定“智能”,它会自动保存溢出的部分,从而简化使用缓冲区的接口。

然后,让切分出来的职责协作起来,验证能否完成功能。我们来回答“切分之后的结构能支持压缩的协作吗?”的问题。回忆一下之前提到的增量建模技巧--不要急于“一口吃个胖子”。

如此循环,早晚要定义子系统的接口。下面是包-接口图 ,帮助架构师明确需要哪些接口(还没有到接口内方法定义一级)。

再次从结构设计跳到行为设计。现在在该更明确考虑压缩了。接下来,我们要演示ZipOneFile的设计。同样,遵循“先大局,后局部”的设计原则。具体设计决策是,让“控制”担当ZipOneFile的职责,而不是让“压缩实现”来担负--原因是希望“压缩实现”不须感知File的概念而能够更大程度上的被重用(例如对数据包而非文件进行压缩)。

我们来进一步的明确接口的方法定义......

13.4. 更多经验总结

13.4.1. 逻辑架构设计的10条经验要点

我们归纳了逻辑架构设计的10条经验要点,其中,如何划分子系统,如何定义接口,如何运用质疑驱动的事物套路等已经介绍,其他几点再继续简单介绍。

  1. 划分子系统:分层的细化
  2. 划分子系统:分区的引入
  3. 划分子系统:机制的提取
  4. 接口的定义:协作决定接口
  5. 选用序列图:杜绝协作图
  6. 包-接口图:从结构到行为的桥
  7. 灰盒包图:描述关键子系统
  8. 循序渐进的螺旋思维
  9. 设计模型:包内结构
  10. 设计模式:包间协作

13.4.2. 简述:逻辑架构设计中设计模式应用

设计模式是Class Level的设计,它如何用于架构一级的设计呢?

基本观点是:让ClassSubSystem搭上关系。不难理解,设计模式用于架构设计主要有两种方式:

  • 明确子系统内的结构
  • 明确包间的协作关系

如何做呢?答案是灰盒包图。下图中说明了灰盒包图的意义,它打破了“子系统黑盒”,关系子系统中的关键类,从而可以更到位的说明子系统之间的协作关系,并成为设计模式应用的基础。

例如,我们对比一下黑盒包图和灰盒包图(背景是项目关系系统甘特图展示的问题)。后者明确了子系统之间的交互机制,还显式的说明了Adpter设计模式的应用--这就是灰盒包图的价值。

13.4.3. 简述:逻辑架构设计的建模支持

工欲善其事,必先利其器 。在实践中必须选择最适合的模型,甚至做一些改造工作让UML更适合特定的实践目的。例如:灰盒包图就是一种“专门说明重要子系统设计”的UML图的应用。

另外,包-接口图是类图的一种特定形式,它包含“包(package)”和“接口(interface)”两种主要元素。这种图的作用很专一:说明包之间的协作需要哪些接口。逻辑架构设计中,包-接口图是从结构设计到行为设计的思维桥梁。

最后,“逻辑架构设计的整体思维套路”时已亮明了观点:逻辑架构的设计,应该使结构设计和行为设计相分离。这样才利于更有效的思维。不信?请看下面所示的“设计图”(这是很多设计者习惯的思维方式)。思维清楚吗?思维混乱的原因:将结构和行为过多的混在了一起

推荐用时序图(它较为专注于行为设计)辅助逻辑架构设计,尽量不要用协作图(虽然在UML 1.4中,它和时序图等价,但从形式上它的“结构气”太重)。

13.5. 贯穿案例

我们来继续贯穿案例:PASS系统的架构设计。首先应注意两点:

  • 第一,细化架构的重要“输入”之一是概念架构设计,不应忽视,比较细化架构设计是整个架构设计过程中的一个阶段。例如,之前进行的“基于鲁棒图的初步设计”,以及“高层分割考虑”。
  • 第二,5视图方法的运用,总体而言是5个视图的设计穿插进行的,对复杂系统而言,根本不可能将逻辑视图设计完全之后再考虑其他视图。而本案例的PASS系统具有很强的分布特点,所以必然较早的考虑到物理视图对逻辑设计的影响。例如,PASS服务器作为一个物理架构元素的“节点(Node)”,它之上“跑”的逻辑架构“逻辑层(Layer)”有哪些呢?

进入细化架构阶段的逻辑架构设计,常以初步设计为基础,借助分层细化、分层引入、机制提取等手段。对PASS服务器软件进行逻辑架构的结构设计。

从结构设计跳到行为设计,常用手段是画时序图。它处于逻辑架构设计的“螺旋式”整体思维讨论的起始循环,是进一步深入设计的基础。

有了不同职责单元之间具体的协作关系,就可以展开细致的“质疑”了--别忘了,架构设计是质疑驱动的。

  • 处方检查服务能被医生工作站访问到吗?毕竟前者位于PASS服务器上......于是,设计中要进一步明确“远程调用机制”。
  • 这样一个分布式的系统,访问服务之前要经过什么样的验证呢......于是,进一步考虑安全性的支持。
  • 不同的医生不停的开处方,处方检查功能会不会很慢?常用药的用药规则应该常驻内存,这样才能提升性能......于是,设计要进一步明确Cache等提升性能的机制。
  • ......

于是,自然而然地,沿着逻辑架构设计的“螺旋式”整体思维套路思考,我们就能意识到“结构设计”要继续完善和细化。基于对远程调用、安全性、高性能的质疑,改进的“结构设计”后就得到下面的逻辑架构图。

下一节:我认识一些架构师,他们的生活都是失控的。因为架构天性范围宽广,涉及人、工作量都非常多。一些架构师把他们的时间整天整天的花在跟“项目干系人”开会上,然后夜以继日,再搭上周末去实际的架构工作。 -- Eric Brechner, 《代码之道》

多重软件架构视图之所以必不可少,是因为各类涉众(用户、客户、开发人员、测试人员、维护人员、内部操作人员、其他人员)需要从各自角度理解和使用架构。 -- Barry Boechm

架构要涵盖的内容和决策太多了,超过了人脑“一蹴而就”的能力范围,因此采用“分而治之”的办法从不同视角分别设计;同时,这样也为对软件架构的理解、交流和归档提供了方便。

接下来会介绍物理架构、运行架构、开发架构作为软件架构的不同视图,它们分别关注不同的方面、针对不同的目标和用途。