秦孝公在位期间致力于恢复秦国的霸业,他因此颁布著名的求贤令, 商鞅3次游说秦孝公,第一次讲的是尧、舜、禹、汤的 帝道 。上古时期,百姓安居乐业。秦孝公听着听着睡着了。商鞅离开之后,秦孝公向景监发火,说商鞅自大。景监把这个反馈给商鞅,商鞅没有气馁,请求再给他一次机会。5天后景监给他安排第二次朝见。这一次商鞅讲的是周文王、周武王的 王道 ,讲的是礼治天下。秦孝公有点兴趣,觉得商鞅可以一起谈论,但没有打算重用商鞅。景监把秦孝公的意思回复给商鞅后,商鞅说:"我已经知道怎么游说他了,请再给我一次机会"。第三次商鞅讲的是春秋五霸的 霸道 。秦孝公这次听得津津有味,并几天几夜促膝长谈,不断向商鞅请教,并重用商鞅,才有了后来的商鞅变法,秦国一统天下。
在软件和编程领域有很多书冠之以‘某某之道’,‘某某之美’,‘某某之禅’。软件领域也有自己的一套哲学思想。我今天想聊一聊我对软件架构的哲学思考。这是一个超大的命题,即使我在软件领域摸爬滚打了这么多年,也不敢说我已经窥其一二,今日胡说一通,希望得到大家的指正。
‘道’ 暨道路或者方向,是自然规律,是原则和目的,道路和方向是错的,无论你怎么努力,都很难成功。
‘术’ 是方法,手段和技巧,好的方法和工具可以让你在成功的路上事半功倍,前提是你走在正确的道路上。
‘道’ 是价值观,是主观的东西,也就是你认为什么是正确的事, 不同的人对什么是正确的事有着不同的认识。商鞅的霸道带领秦国统一六国,但是注定不能长久,秦国也未能如始皇的愿,千秋万代。
‘术’ 是方法论,相对比较客观,一个好的方法基本上是可以推广的。
我个人认为,软件架构之道最核心的问题是解决复杂性的问题。如果说‘道’是方向,那么软件架构之路应该带领码农走向简单。这里的简单应该包含:
那么我们想一想,软件发展到今天,我们上面所说的这些有变简单了么?似乎没有。那么是不是软件架构在背道而驰,变得越来越复杂了呢?我不这么认为。为了理解这个问题,我们来看看软件中的复杂性的元凶,是什么带来了软件的复杂性。
首先,软件的复杂性来自于 功能 。我们之前提到,软件架构的中心问题是满足功能要求。随着人们希望软件能够提供越来越多的功能,软件架构的设计必然会随之变得复杂。而这个复杂度的增加和功能之间的关系并不是线性的,而是几何级数甚至更高。因为软件开发是一个动态过程,新加入的功能必然会和已有的功能产生互动,有些是依赖,有些是制约,有些是干扰。例如系统最初有一个功能A,运作良好;当加入功能B的时候,A功能会制约B功能,所以在设计B功能的时候,除了要完成B本身的功能外,还要设计如何和A功能互动或者如何屏蔽A功能对B的限制。当C功能到来的时候,A和B同时要影响C,甚至可能要考虑ABC的联动,这样系统就会越来越复杂了。所以复杂的功能是当前软件复杂性的主要原因。
其次,软件的复杂性的另一个元凶是 人为 的。也就是由于开发软件的人和组织因为能力不足,或者因为懒惰,贪婪,傲慢等原罪,人为的使软件变得复杂。
我认为软件发展的大势仍然是向着简单性的方向在前进。因为对功能的要求越来越多,我们现在看到的复杂的软件架构实际支撑了更为复杂的诸多功能,所以从这个角度来看,软件架构实际上是向着简单的趋势发展。
我们所能做的是把复杂性封装在更低的层次。例如操作系统封装了对计算资源(内存,CPU,进程,文件系统)的使用,AWS云在基础设置infrastructure层次对网络,主机的使用进行封装,而Kubernetes利用容器集群,封装了应用的部署。操作系统,云和Kubernetes都是很复杂的,但是通过良好的封装和简单的接口,他们给使用者带来了便利。做为架构设计的一个原则,应该尽量把复杂性封装在更低的层次。
通常在分层架构中,越高的层次一般意味着更多的代码和更多的使用。当一个问题发生的时候,一般总是表现在最高层处(UI或者应用层),然而对这个问题的处理,可以在各个层次解决。例如为了提高访问效率,我们可以在UI或者数据层加入缓存,数据层的缓存可以支持所有的UI用户,而UI层的缓存只能针对使用该类型的UI的用户,例如web端和移动端的不用UI各自要实现自己的缓存。当然实际中,所有的层次都可以利用自己的缓存来解决问题,但是从解决问题的效率来看,在低层次上解决问题的效率要高于更高的层次。所以我们应该尽可能的把复杂问题的解决放在更低的层次上。
但是我们之前也提到过人是软件架构的一个基本点之一,分层的架构往往也意味着分层的组织。当UI的团队试图解决性能问题的时候,他们往往希望在组织可控的范围内解决问题。这并不是因为他们不明白在数据层解决问题的优势,而是因为要和另一个组织,数据库部门去沟通,带来的额外成本比自己加一个缓存可能还要高。做正确的事是有代价的。(参见 康威定律 )
爱因斯坦说: “Simple,but not simpler”。
建筑大师路德维希·密斯·凡德罗说:“Less is more。”
奥卡姆说:“如无必要,勿增实体”。
老子说:“大道至简”。
软件架构之道在于找到设计的平衡点,使得架构足够简单,但是能够满足需要。 (关于奥卡姆剃刀,请大家参考我之前的动图,用可视化来讲故事)
除了简单性作为道之根本,软件架构设计还有一些常见的通用原则,我认为这些原则都是和降低系统复杂度一致的:
Keep it simple and stupid 是我们之前的简单性原则的一种说法。我想说的是简单未必愚蠢,很多时候大智若愚。
光总是走最短路径,人也一样。程序猿的惰性与生俱来,我们总是选择最容易行走的路径。这也是为什么我们应该尽可能在一开始的时候,作出正确的选择,因为一旦这个架构设计出现,后面的人很有可能不愿意为了更好的架构而改进,而是遵循已有的设计。这个和简单性原则一致,如果我们不能在一开始作出正确的选择,因为最小代价原则,系统必然会走向复杂。
以前读过一本讲UX设计的书《点石成金》,英文名叫《Dont make me think》。和这个原则一致,UX设计应该自然,符合用户的常识和使用习惯。如果用户需要通过思考才能理解如何交互,那么一定是设计出了问题。同样的,架构设计也一样,好的设计应该避免意外,遵守通用的规范和习惯,代码也是。这些意外其实是软件架构和设计中的复杂因素。最小意外也就意味着尽可能的简单。
重复是软件的原罪之一,“Dont repeat yourself” 告诉我们应该尽可能的消灭重复和冗余。重复使的软件的阅读,修改,测试变得复杂,消灭重复,是使软件变得简单的手段之一。
我们再来聊聊软件架构的‘术’。在软件和软件架构设计领域,有很多方法和工具,我们来看看其中最常见的一些。我们可以把他们归为“术”。这些和软件架构都有着直接或者间接的关系。
数据结构和算法是软件编程领域里最重要的方法。数据结构,是抽象的表示数据的方式;算法,则是计算的一系列有效、通用的步骤。数据结构是以某种形式将数据组织在一起的集合,它不仅存储数据,还支持访问和处理数据的操作。算法是为求解一个问题需要遵循的、被清楚指定的简单指令的集合。 算法与数据结构是程序设计中相辅相成的两个方面,是计算机学科的重要基石。运用数据结构和算法,可以有效的解决编程中遇到的一些常见的复杂问题。
每一个程序猿从一开始学习编程就接触数据结构和算法。有人说,程序等于数据结构加算法。这有一定的道理,但是显然不够全面。数据结构和算法作为编程的基础方法,是各大软件厂商招聘考核的标杆,不管你是程序猿还是架构师,都需要投入精力于此,数据结构和算法是软件和架构设计的基础。
仅有数据结构和算法还不足以应对复杂的软件开发的需要。面向对象的设计成为了软件开发领域里最为流行的思想。面向对象是一种对现实世界理解和抽象的方法,利用的是隐喻(metaphor)的手段。隐喻其实是我们在软件设计中常用的一种手段,为了便于理解,我们把现实世界中的概念用软件中的概念的来模拟,这样做的好处是便于我们去思考,因为我们的设计是基于我们对现实世界的理解,所以可以重用我们现现实世界积累的成功经验。这样做的缺点是,软件世界有自己的特点,完全套用现实世界的偏见可能并非最为有效。现在虽然对面向对象的程序设计仍处于统治地位,但是它的声音已经渐渐变弱。与之对应的有面向过程,函数式编程,面向切面(Aspect Oriented)等思想。
设计模式(Design pattern)起源于《设计模式:可复用面向对象软件的基础》一书,提出和总结了对于一些常见软件设计问题的标准解决方案,称为软件设计模式。该书作者后以“四人帮”著称。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
软件设计模式和面向对象的设计都基于一些常见的原则:
以上的五个原则构成了著名的 SOLID ,除此之外,还有一个著名的 高内聚低耦合原则, 是指具有相似功能的代码应该放在同一个代码组件里。一个代码片段(代码块,函数,类等)应该最小化它对其它代码的依赖。这个目标通过尽可能少的使用共享变量来实现。“低耦合是一个计算机系统结构合理、设计优秀的标志,把它与高聚合特征联合起来,会对可读性和可维护性等重要目标的实现具有重要的意义。”该原则和我们之前提到的把复杂性封装在更低的层次上是一致的,对复杂性的封装就是高内聚。
模块化和分层是架构设计里最基本的方法。
如上图的一个典型的软件架构图中,不同的模块位于不同的层级,某些模块会跨一些层级。模块化和分层利用的是分而治之的方法,把复杂的软件系统划分为可控制,可管理的小单元。也即是说,它仍然是为了解决复杂性的问题。分层和模块化不是总能降低复杂度,过多的层次和冗余的模块会使系统变得更为复杂。
随着越来越多的软件应用走向云端,云原生的设计越来越多的被提起。微服务随之大行其道。Martin Fowler对微服务的定义是“微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间相互协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务和服务之间采用轻量级的通信机制相互沟通(通常是基于HTTP的Restful API).每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外,应尽量避免统一的、集中的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构"
和之前的模块化和分层相比较,分层更多是关注在垂直方向上的责任划分,而微服务增加了水平方向的扩张,并实现去中心化的网络架构。关于去中心化的网络架构推荐阅读凯文·凯利的 《失控》
我们看到,微服务在某种程度上提高了系统的复杂度,天下没有免费的午餐,作为一个架构师,需要理解微服务带来的好处是否能够抵消复杂性提升的代价。
当今的微服务大多基于容器设计。(关于微服务中容器设计的原则,请参考我的博客 基于容器应用设计的原则,模式和反模式 )
Serverless 架构是指大量依赖第三方服务(也叫做后端即服务,即“BaaS”)或暂存容器中运行的自定义代码(函数即服务,即“FaaS”)的应用程序,函数是无服务器架构中抽象语言运行时的最小单位。在这种架构中,我们并不看重运行一个函数需要多少 CPU 或 RAM 或任何其他资源,而是更看重运行函数所需的时间,我们也只为这些函数的运行时间付费。
无服务的设计带来的优点有:
当然无服务设计也有一些限制,它更适用于大量,频繁而简单的,无状态的操作,如果服务之间的关系比较复杂,状态比较多,调用时间比较长,无服务设计并不是非常适合。
软件架构设计还有很多方法,这里不可能一一讨论,例如领域驱动的设计(DDD),重构技术,敏捷(更多是软件工程,但是和架构也会有密切地关系),希望以后是时间再和大家分享,讨论。
如果软件行业是个江湖,那么我们程序猿就是行走于江湖的武林人士。每个人要做的就是找寻“道”,研习“术”,磨练“器”。
“道”是价值观,是你的江湖理想,是你的追求。就像《神雕侠侣》时期守襄阳,信奉“侠之大者,为国为民”的郭靖,。
“术”是你的内功心法和拳艺招数。数据结构和算法就像是内功,帮助你提高对战的效率。而各种其它的方法就像是招数。你可以像是学了独孤九剑的令狐冲,仅凭招数就可以杀敌无数,但是要成为武林盟主,内功修养也是不可或缺。
“器”是工具,各种语言,IDE,可以划分到“器”,“工欲善其事,必先利其器”。各位大侠行走江湖,免不了要选几样趁手的兵器,有人喜欢剑走偏锋,有人喜欢暴力砍杀。什么,你说你喜欢九齿钉耙?我只能赞你一声神仙威武!(参考 如果编程语言是一种武器 )
江湖苦修非一日之功,希望各位大侠在软件江湖早日得道,成为武林至尊或者一方豪杰。
刚哥谈架构 (二) 我眼中的架构师
刚哥谈架构 (一) 软件架构的定义