许式伟谈架构3
六、软件工程篇
68 | 软件工程的宏观视角
我理解的架构师的职责其实是从软件工程出发的。也许大家都学过软件工程,但如果我们把软件工程这门课重新看待,这门学科到底谈的是什么?是软件项目管理的方法论?无论如何,软件工程是一门最年轻的学科,相比其他动辄跨世纪的自然科学而言,软件工程只有 50 年的历史。这门学科的实践太少了,任何一门学科的实践时间短的话,都很难沉淀出真正高效的经验总结,因为这些总结通常都是需要很多代人共同推动来完成的。
为什么说它只有 50 年时间呢?
我们先来看看 C 语言,一般意义上来说,我们可能认为它是现代语言的开始。C 语言诞生于 1970 年,到现在是 49 年。再看 Fortran,它被认定为是第一个高级语言,诞生于 1954 年,那时候主要面向的领域是科学计算。Fortran 的程序代码量普遍都还不大,量不大的时候谈不上工程的概念。这也是我为什么说软件工程这门学科很年轻,它只有 50 岁。对于这样一个年轻的学科,我们对它的认知肯定还是非常肤浅的。
我在这个架构课的序言 “开篇词 | 怎样成长为优秀的软件架构师?” 一上来就做了软件工程和建筑工程的对比。通过对比我们可以发现,二者有非常大的区别,具体在于两点:
其一,不确定性。为什么软件工程有很大的不确定性?大部分大型的软件系统都有几千甚至几万人的规模,而这几千几万人中,却没有两个人的工作是重复的。虽然大家都在编程,但是编程的内容是不一样的。每个人昨天和今天的工作也是不一样的,没有人会写一模一样的代码,我们总是不停地写新的东西,做新的工作。这些东西是非常不同的,软件工程从事的是创造性的工作。
大家都知道创造是很难的,创造意味着会有大量的试错,因为我们没有做过。大部分软件的形成都是一项极其复杂的工程,它们远比传统的工程复杂得多,无论是涉及的人力、时间还是业务的变数都要多很多。这些都会导致软件工程有非常大的不确定性。
其二,快速变化。建筑工程在完工以后就结束了,基本上很少会进行变更。但在软件工程里,软件生产出来只是开始。只要软件还在服务客户中,程序员们的创造过程就不会停止,软件系统仍然持续迭代更新,以便形成更好的市场竞争力。
这些都与传统建筑工程的模式大相径庭。一幢建筑自它完成之后,所有的变化便主要集中在一些软装的细节上,很少会再发生剧烈的变动,更不会持续地发生变动。但软件却不是这样,它从诞生之初到其生命周期结束,自始至终都在迭代变化,从未停止。
以上这两点都会导致软件工程区别于传统意义上的所有工程,有非常强的管理难度。过去那么多年,工业界有非常多的工程实践,但是所有的工程实践对软件工程来说都是不适用的,因为二者有很大的不一样。今天如果我们站在管理的视角再看软件工程的话,我们知道管理学谈的是确定性。管理学本身的目的之一就是要抑制不确定性,产生确定性。
比如,开发工期、时间成本是否能确定。比如,人力成本、研发成本以及后期运维的成本是否能确定。
所以,软件项目的管理又期望达到确定性。但软件工程本身是快速变化的,是不确定的。这就是软件工程本身的矛盾。我们的目标是在大量的不确定性中找到确定性,这其实就是软件工程最核心的点。
架构师的职责
如果用 “瀑布模型” 的方式来表达,现代软件工程的全过程大体如下:
\t从开始的需求与历史版本缺陷,到新版本的产品设计,到架构设计,到编码与测试,到最终的产品发布,到线上服务的持续维护。贯穿整个工程始终的,还有不变的团队分工与协同,以及不变的质量管理。
更为重要的是,这个过程并不是只发生一遍,而是终其生命周期过程中,反复迭代演进。
它是一个生命周期往往以数年甚至数十年计的工程。对于传统工程,我们往往也把一个工程称为项目,项目工程。但软件工程不同,虽然我们平常也有项目的概念,但软件工程并不是一个项目,而是无数个项目。每个项目只是软件工程的一个里程碑(Milestone)。
所以,光靠把控软件工程师的水平,依赖他们自觉保障工程质量,是远远不够的。软件工程是一项非常复杂的系统工程,它需要依赖一个能够掌控整个工程全局的团队,来规划和引导整个系统的演变过程。这个团队就是架构师团队。
软件架构师的职责,并不单单是我们通常理解的,对软件系统进行边界划分和模块规格的定义。从根本目标来说,软件架构师要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。因此,虽然架构师的确是一个技术岗,但是架构师干的事情,并不是那么纯技术。
首先是用户需求的解读。怎么提升需求分析能力,尤其是需求演进的预判能力?它无关技术,关键是心态,心里得装着用户。除了需要 “在心里对需求反复推敲” 的严谨态度外,对用户反馈的尊重之心也至关重要。
其次是产品设计。产品边界的确立过程虽然是产品经理主导,但是架构师理应深度参与其中。原因在于,产品功能的开放性设计不是一个纯粹的用户需求问题,它通常涉及技术方案的探讨。因此,产品边界的确立不是一个纯需求,也不是一个纯技术,而是两者合而为一的过程。
以上两点,是架构本身的专业性带来的,在前面五章中已经谈过很多,我们这里不再展开。在本章中,我们更多是从工程本身出发。这些话题是因软件工程的工程性而来,属于工程管理的范畴,但它们却又通常和架构师的工作密不可分。
这里面最为突出但也非常基础的,是贯穿软件工程始终的 “团队分工与协同” 问题、“软件的质量管理” 问题。从 “团队分工与协同” 来说,话题可以是团队的目标共识,也可以是做事方式的默契,各类规范的制定。从 “软件的质量管理” 来说,话题可能涉及软件的版本发布,质量保障的过程体系等等。
从更宏观的视角看,我们还涉及人力资源规划的问题。什么东西应该外包出去,包给谁?软件版本的计划是什么样的,哪些功能先做,哪些功能后做?看起来,这些似乎和架构师的 “本职工作” 不那么直接相关。但是如果你认同架构师的职责是 “对软件工程的执行结果负责”,那么就能够理解为什么你需要去关注这些内容。
结语
软件工程本身是一个非常新兴、非常复杂的话题。可能需要再花费 50 年这样漫长的时间才能形成更清晰的认知(例如,我们第四章 “服务治理篇” 专门探讨了现代软件工程全过程最后一个环节 “线上服务管理” 这个话题)。
作为架构课的一部分,这一章我们将主要精选部分与架构师的工作关系密切的话题来进行讨论,主要包括:
- 团队的共识管理;
- 如何阅读别人的代码;
- 怎么写设计文档;
- 发布单元与版本管理;
- 软件质量管理:单元测试、持续构建与发布;
- 开源、云服务与外包管理;
- 软件版本迭代的规划;
- 软件工程的未来。
69 | 团队的共识管理
软件工程是一项团体活动,大家有分工更有协同。不同的个体因为能力差别,可以形成十倍以上的生产力差距。而不同团体更是如此,他们的差距往往可以用天壤之别来形容。这差距背后的原因,关乎的是协同的科学。
团队共识
有的团体像一盘散沙,充其量可以叫团伙。有的团体则有极强的凝聚力,整个团队上下同心,拧成一股绳,这种团体才是高效率组织,是真正意义上的团队。团队靠什么上下同心?靠的是共识。那么,什么是团队的共识?
团队的共识分很多层次。其一,团队是不是有共同的目标。其二,团队是不是有共同的行事做人的准则。其三,对产品与市场的要与不要,以及为什么要或为什么不要,是否已达成一致。其四,对执行路径有没有共同的认知。其五,有没有团队默契,是否日常沟通交流很多地方不必赘述,沟通上一点即透。
一个团体如果缺乏共同的目标,那么它最多能够算得上是一个团伙,而不能称之为团队。团队的目标也分很多层次。为什么很多企业都会谈他们的使命和愿景,是因为它是这个企业作为一个团队存在的意义,是企业所有人共同的长远目标。
人是愿景型动物,需要看到未来。越高级的人才越在乎团队存在的意义。所以高科技公司的人才通常只能去影响,而不是像一些人心中理解的那样,认为管理是去控制。愿景是一种心力。人有很强的主观能动性。一旦人相信企业的使命与愿景,员工就变得有很强烈的使命感,有强烈的原动力。员工的行为方式也就会潜移默化发生变化。
不过,有共同的远景目标的团队仍然有可能走向分裂。中国有句古话说得好:“道不同,不相为谋”。团队有没有相同的价值观,有没有相同的行事做人的准则,这些更根本性的基础共识,极有可能会成为压垮团队的稻草。
共识大于能力。如果一个人有很强的个人能力,但是却和团队没有共同的愿景,或者没有共同的价值观,那么能力越大产生的破坏性也就越大。
怎么达成共识?
团队有了共同的使命、愿景与价值观,就有了共同努力把一件事情干成的最大基础。然而,这并不代表这个团队就不会遇到共识问题。团队仅有远期的目标是不够的,还要有中短期的目标。企业的使命和愿景需要由一个个的战略行动来落地。我们的产品定位怎么样,选择哪些细分市场去切入,这些同样需要团队达成共识。
怎么去达成共识?越 “聪明” 的团队负责人,往往越容易忽视达成共识的难度。他们通常会召开会议,然后把自己的想法说给大家听。半个小时后,兄弟们迷茫地回去了。
在团队还小的时候,这种简单共识的方式很可能是可以奏效的,尤其是当团队负责人还能够一一去检查每个人的工作内容时,所有的理解偏差都能够得到比较及时的纠正。但是团队规模稍微变大一些,这种简单共识突然就失效了。“我明明已经告诉他们要做什么了。” 负责人有时候困惑于团队成员为什么并没有理解他的话。
这是因为他还并不理解真正的共识意味着什么。也没有对达成共识的难度有足够的认知。让更多人参与到决策形成的过程现场,是更好的共识达成的方式。通过同步足够充分的信息,通过共创而非传达决策的方式让结论自然产生。
这个共创过程不必团队所有人都参与,但要确保所有影响落地的关键角色都在,并确保参与这个过程的人都能够产生思想的碰撞,而非做个吃西瓜群众。
契约与共识效率
目标与执行路径达成了共识,这还不够。我们还需要把共识表达出来,形成文字。为什么这很重要?
因为共识之所以为共识,是因为它不是空中楼阁,不是口号,而是指导我们做战略选择的依据,指导我们平常行为的依据。
所以,共识就是团队协作的契约。契约的表达越是精确而无歧义,团队协作中主观能动性就越高,执行的效率也就越高。
对于架构过程同样如此。架构过程实际上是团队共识形成与确认的过程。架构设计需要回答两个基本的问题:
- 系统要做成什么样?
- 怎么做?
架构设计为什么叫架构设计,是因为架构师的工作中除了架构,还有设计。设计其实谈的就是 “系统要做成什么样”:设计高于架构。
设计强调规格,架构强调实现。规格设计是架构过程的最高共识。所以,规格高于实现。我们用架构的全局性和系统性思维去做设计。
一些架构师乐衷于画架构图,把它当作是架构师最重要的工作内容。但架构图在共识的表达上并不太好。因为共识是需要精确的、无歧义的。而架构图显然并不精确。
对于一个工程团队来说,没有精确的共识很可怕。它可能导致不同模块的工作牛头不对马嘴,完全无法连接起来,但是这个风险没有被暴露,直到最后一刻里程碑时间要到了,要出版本了,大家才匆匆忙忙联调,临时解决因为架构不到位产生的 “锅”。这时候人们的动作通常会走形。追求的不再是架构设计的好坏,而是打补丁,怎么把里程碑的目标实现了,别影响了团队绩效。
我们作个类比,这种不精确的架构,就好比建筑工程中,设计师画了一个效果图,没有任何尺寸和关键细节的确认,然后大家就分头开工了。最后放在一起拼接(联调),发现彼此完全没法对上,只能临时修修改改,拼接得上就谢天谢地了。是不是能够和当初效果图匹配?让老天爷决定吧。
更精确描述架构的方法是定义每个模块的接口。接口可以用代码表达,这种表达是精确的、无歧义的。架构图则只是辅助模块接口,用于说明模块接口之间的关联。尊重契约,尊重共识精确的、无歧义的表达,非常非常重要。
绝大部分哪怕是非常优秀的架构师,在系统设计(也叫概要设计)阶段通常也只会形成系统的概貌,把子系统的划分谈清楚,把子系统的接口规格谈清楚。但实际上概要设计阶段最好的状态并不是只有设计文档。为了降低风险,系统设计阶段也应该有代码产出。
这样做有两个方面的目的。其一,系统的初始框架代码。也就是说,系统的大体架子已经搭建起来了。其二,原型性的代码来验证。一些核心子系统在这个阶段提供了 mock 的系统。
这样做的好处是,一上来我们就关注了全局系统性风险的消除,并且给了每个子系统或模块的负责人一个更具象且确定性的认知。代码即文档。代码是理解一致性更强的文档。
结语
这一讲我们谈的是协同的科学。为什么有的团队效率极高,有的团队却进展缓慢,从背后的协同效率来说,共识管理是根因中的根因。
共识有非常多的层次。不同层次的共识处于完全不同的维度。它们都极其重要,且相互不可替代。当某个层次的共识出问题的时候,我们需要在相应的层次去解决它。如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “怎么写设计文档”。原计划我们下一讲是 “如何阅读别人的代码”,但是我想先顺着共识这个话题谈问题谈清楚。
70 | 怎么写设计文档?
我们用最基本的 “瀑布模型” 来描述现代软件工程的全过程,在这个过程中,有两个阶段非常关键:一个是 “产品设计”,一个是 “架构设计”。产品设计由产品经理主导,关注的是 “如何以产品特性来系统化地满足用户需求”。架构设计由架构师主导,关注的是 “业务系统如何系统化地进行分解与交付”。
“设计” 一词非常精妙。无论是 “产品设计”,还是 “架构设计”,其实谈的都是 “需求如何被满足” 这件事情的共识。无论是 “产品文档”,还是 “架构文档”,它们都是设计文档的一种,都有团队内及团队间的协同价值。
一讲 “69 | 团队的共识管理” 我们已经从团队的协同角度,谈了共识的重要性。本质上,我们也是在谈 “设计” 的重要性。换个角度来说,一个企业的使命、愿景与价值观,何尝不是这个企业最高维度的 “设计” 呢?
产品经理与架构师是一体两面,对人的能力要求的确会比较像,但是分工不同,关注的维度不同。产品经理关注的维度,其关键词是:用户需求、技术赋能、商业成功。而架构师关注的维度,其关键词是:用户需求、技术实现、业务迭代。
今天我们谈的 “设计文档”,重点聊的是 “架构设计文档” 怎么写,但是本质上所有 “设计文档” 的内容组织逻辑,都应该是相通的。它们的内容大体如下:
-
现状 :我们在哪里,现状是什么样的?
-
需求:我们的问题或诉求是什么,要做何改进?
-
需求满足方式:
-
要做成什么样,交付物规格,或者说使用界面(接口)是什么?
-
怎么做到?交付物的实现原理。
关于设计文档内容组织的详细说明,我们在前面 “45 | 架构:怎么做详细设计?” 中已经进行过交代。概括来说,这些设计文档要素的关键在于以下几点。
现状:不要长篇累牍。现状更多的是陈述与我们要做的改变相关的重要事实,侧重于强调这些事实的存在性和重要性。
需求:同样不需要长篇累牍。痛点只要够痛,大家都知道,所以需求陈述是对痛点和改进方向的一次共识确认。
需求满足方式:要详写,把我们的设计方案谈清楚。具体来说,它包括 “交付物规格” 和 “实现原理” 两个方面。
交付物规格,或者说使用界面,体现的是别人要怎么使用我。对于 “产品设计”,交付物规格可能是 “产品原型”。对于 “架构设计”,交付物规格可能是 “网络 API 协议” 或者 “包(package)导出的公开类或函数”。
实现原理,谈的是我们是怎么做到的。对于 “产品设计”,它谈的是用户需求对应的 UserStory 设计,也就是业务流具体是怎么完成的。而对于 “架构设计”,它谈的是 UserStory 具体如何被我们的程序逻辑所实现。以下这个公式大家都耳熟能详了:程序 = 数据结构 + 算法
它是一个很好的指导思想。当我们谈程序实现逻辑时,我们总是从数据结构和算法两个维度去描述它。其中,“数据结构” 可以是内存数据结构,也可以是外存数据结构,还可以是数据库的 “表结构”。“算法” 基于 “数据结构”,它描述的是 UserStory 的具体实现,它可以是 UML 时序图(Sequence Diagram),也可以是伪代码(Pseudo Code)。
多个设计方案的对比
在现实中,一篇设计文档有时候不是只有一个设计方案,而是有多个可能的需求实现方式。在这个时候,通常我们会概要地描述清楚两个设计方案的本质差别,并且从如下这些维度进行对比:
- 方案的易实施性与可维护性。
- 方案的时间复杂度与空间复杂度。
不同的业务系统倾向性不太一样。对于绝大部分业务,我们最关心的是工程效率,所以方案的易实施性与可维护性为先;但是对于部分对成本与性能非常敏感的业务,则通常在保证方案的时间复杂度与空间复杂度达到业务预期的前提下,再考虑工程效率。
在确定了设计方案的倾向性后,我们就不会就我们放弃的设计方案做过多的展开,整个设计文档还是以描述一种设计方案为主。
如果我们非要写两套设计方案,这时应该把设计文档分为两篇独立的设计文档,而不是揉在一起。你可能觉得没有人会这么不怕麻烦,居然写两套设计方案。但是如果两套设计方案的比较优势没有那么显著时,现实中写两套设计方案确实是存在的,并且应该被鼓励。
为什么这么说?这是因为 “设计” 是软件工程中的头等大事,我们应该在这里 “多浪费点时间”,这样的 “浪费” 最终会得到十倍甚至百倍以上的回报。
使用界面(接口)
在描述交付物的规格上,系统的概要设计,与模块的详细设计很不一样。对于 “模块的详细设计” 来说,规格描述相对简单。因为我们关注的面只是模块本身,而非模块之间的关系。对于模块本身,我们核心关注点是以下两点:一是接口是否足够简单,是否自然体现业务需求。二是尽可能避免进行接口变更,接口要向前兼容。
关于接口变更,后面有机会我们还会进行详细的讨论,这一讲先略过。但对于 “系统的概要设计” 来说,我们第一关心的是模块关系,第二关心的才是各个模块的核心接口。这些接口能够把系统的关键 UserStory 都串起来。
表达模块关系在某种程度来说的确非常重要,这可能是许多人喜欢画架构图的原因。但描述模块间的关系的确是一件比较复杂的事情。我们在 “32 | 架构:系统的概要设计” 这一讲中实际上先回避了这个问题。
一种思路是我们不整体描述模块关系,直接基于一个个 UserStory 把模块之间的调用关系画出来。比如对于对象存储系统,我们上传一个文件的业务流程图看起来是这样的:
这类图相信大家见过不少。但它从模块关系表达上并不是好的选择,因为根本并没有对模块关系进行抽象。这类图更多被用在面向客户介绍 API SDK 的背后的实现原理时采用,而非出现在设计文档。如果只是对于 UserStory 业务流程的表达来说,UML 时序图通常是更好的表达方式。
但是,怎么表达模块关系呢?一个方法是对模块的调用接口进行分类。通过 “62 | 重新认识开闭原则 (OCP)” 这一讲我们知道,一个模块对外提供的访问接口无非是:
- 常规 DOM API,即正常的模块功能调用;
- 事件(Event)的发送与监听;
- 插件(Plugin)的注册。
这些不同类型的访问接口,分别代表了模块间不同的依赖关系。我们回忆一下 MVC 的框架图,如下:
在图中,View 监听 Model 层的数据变更事件。View 转发用户交互事件给 Controller。Controller 则负责将用户交互事件转为 Model 层的 DOM API 调用。另一个表达模块关系的视角,是从架构分解看,我们把系统看作 “一个最小化的核心系统 + 多个彼此正交分解的周边系统”。例如,我们实战案例 — 画图程序的模块关系图如下:
需要清楚的是,模块关系图的表达是非常粗糙的,虽然它有助于我们理解系统分解的逻辑。为了共识的精确,我们仍然需要将各个模块核心的使用界面(接口)表达出来。
实现原理
谈清楚了交付物的规格,我们就开始谈实现。对于 “系统的概要设计” 与 “模块的详细设计”,两者实现上的表达有所不同。对于模块的详细设计来说,需要先交代清楚 “数据结构” 是什么样的,然后再将一个个 UserStory 的业务流程讲清楚。
对于系统的概要设计来说,核心是交代清楚不同模块的配合关系,所以无需交代数据结构,只需要把一个个 UserStory 的业务流程讲清楚。无论是否要画 UML 时序图,在表达上伪代码(Pseudo Code)的设计都是必需的。
伪代码的表达方式及语义需要在团队内形成默契。这种伪代码的语义表达必须是精确的。比如,对于网络请求相关的伪代码,我们可以基于类似 qiniu httptest 的语法,如下:
类似地,对于 MongoDB,我们可以直接用 MongoDB 的 JavaScript 脚本文法。对于 MySQL,则可以直接基于 SQL 语法。等等。
总结
前面在 “45 | 架构:怎么做详细设计?” 我们实际上已经大体介绍了模块级的设计文档怎么写。所以这一讲我们主要较为全面地补充了各类设计文档,包括产品设计、系统的概要设计等在细节上与模块设计文档的异同。
74 | 开源、云服务与外包管理
在这一讲之前,我们涉及到分工这个话题,基本上都局限于企业内部,且大多数情况下主要在同一个团队内部。但今天我们聊的是更大的分工:跨组织的分工与协作。
外包及其理想模型
在软件工程中,我们第一个接触的外部分工毫无疑问是外包。所谓外包,就是将我们软件的全部或部分模块的实现职能交给外部团队来做。但是,软件工程项目的外包实际上成功率非常低。这背后有其必然性,它主要表现在以下这些方面。
其一,任务表达的模糊,双方容易扯皮。期望需求方能够把需求边界说清楚,把产品原型画清楚,把业务流程讲清楚,这非常难。有这样专业的需求表达能力的,通常软件工程水平不低,遇到这样的需求方,绝对应该谢天谢地。这种专业型的甲方,它大部分情况下只发生在项目交付型外包,而非产品功能外包。更多的产品外包,一般是甲方不太懂技术,需要有团队替自己把事情干了,他好拿着产品去运营。
其二,交付的代码质量低下,长期维护的代价高。软件工程不是项目,它都需要长长久久地运行下去。但是接包方的选择相当重要。因为接包方的质量相当参差不齐,遇上搬砖的概率远高于设计能力优良的团队。事实上,有良好设计能力的团队,多数情况下也不甘于长期做外包。
其三,项目交接困难,知识传承效率很低。软件工程并非普通的工程,就算交付的结果理想,项目交接也非常困难。所以外包项目第一期结束后,如果运营得好,往往项目还继续会有第二期、第三期。这里的原因是你只能找同一拨人做,如果换一波人接着做,考虑到知识传承效率低下,往往需要很长的一个交接周期。
那么,外包的理想模型是什么?上面我们已经说到,外包在通常情况下,专业的甲方需要说清楚需求,这样双方就没有分歧。但是,更好的做法其实不是外包需求,而是外包实现。也就是说,作为专业的甲方,我自己做好需求分析,做好系统的概要设计。进一步,我们把每个模块的业务范畴与接口细化下来。我们以此作为外包边界。假设分了 N 个模块,我们可以把它们平均分给若干个接包方。
这种方式的外包,甲方相当于只留了架构师团队,实现完全交给了别人。但是它与普通的外包完全不同,因为根本不担心知识传承的问题。每个模块的接包方对甲方来说就真的只是干活的。接包方拿到的是模块的规格说明书。他要做的是模块的详细设计的实现部分,其中最为核心的是数据结构设计。对于服务端,甲方可以规定所采用的数据库是什么,但是把表结构的设计交出去。进一步,如果模块的外包说明书中还规定了单元测试的案例需要包含哪些,那么这个模块发生设计偏离的可能性就很低。
外包的验收需要包含模块的实现设计文档,里面描述了数据结构 + 算法。另外,单元测试部分,每个测试场景,也填上对应的测试函数的名称。实际会有人这样去外包么?我不确定。但我们可以把它看作一种分工的假想实验。这个假想实验可以充分说明架构师团队的重要性。有了一个好的架构师团队,他们设计合适的系统架构,对每个模块的规格都做了相应的定义,他们验收模块的实现。
这样,项目就可以有条不紊地展开。甚至,研发进度可以自如控制。嫌项目进展太慢?找一倍的接包方,就可以让工程加速一倍。所以,这个外包假想实验也说明了一点:我们的平常项目之所以进度无法达到预期,无他,团队缺乏优秀的架构师而已。让我们把软件工程看作一门科学。我们以工程师思维的严谨态度来看它。我们减少项目中的随意性,把架构设计的核心,模块规格,也就是接口,牢牢把控住。这样,项目的执行风险就完全消除了。
哦不,还有一个最大的执行风险没有消除。我怎么证明这个系统架构的分解是对的?不会出现每个模块做好了,但是最终却拼不起来?我们前面在 “架构:系统的概要设计” 这一讲中实际上已经谈过这事的解决方法:系统设计的产出要有源代码,它是项目的原型。关键模块有 mock 的实现,业务系统的关键 UserStory 都串了一遍,确保系统设计的正确性。
这个假想实验是有趣的,它可以让你想明白很多事情。甚至可以把它看作理解这个专栏的架构思维核心思想的钥匙。我希望,它不只是一个假想实验。
开源与众包
我们把话题拉回到跨组织的分工。除了传统的外包外,在软件工程中出现的第二类外包是众包,它以开源这样一个形态出现。从分工角度,开源的核心思想是让全社会的程序员共同来完成一个业务系统。
开源的优势非常明显。对于一个热门的开源项目,它的迭代进度是非常惊人的,因为它撬动的资源太大了。但不是开源了就能够获得这样的好处。虽然成功的开源项目风风火火,但是我们也应该意识到,对于那些并没有得到关注的开源项目,它们的迭代速度完全无法保障。最终,你可能还是只能靠自己的团队来完成它的演进。
从这个意义上看,开源是一种商业选择。你得持续经营它。没有经营的开源项目不会成功。你需要宣传它,你自己也得持续迭代它,你还要为它拉客户。有客户的开源项目自然就有了生命力。另外,开源这种形态,注定了它只能做大众市场。如果一个业务系统它的受众很少,就比较难通过开源获得足够的外部支持。
所以绝大部分成功的开源项目,都属于基础设施性质的业务系统,有极其广泛适用的场景。例如,语言、操作系统、基础库、编程框架、浏览器、应用网关、各类中间件等等。我们这个架构课重点介绍的内容,大部分都有相应的开源实现。开源对信息科技的影响极其巨大,它极大地加速了信息科技前进的进程,是全球共同精诚协作的典范。
没有参与过开源的程序员是需要心有遗憾的。开源沉淀下来的协同方法与工作流,今天被无数公司所借鉴。没有开源,我们无法想象这件事情:那么多形形色色的企业,今天其中绝大部分,它们的软件工程协同方法与业务流竟然如此相似。这是开源带来的另一种无形资产。
如果大家没有忘记的话,可能能够回忆起来,在谈完软件工程的宏观视角之后,我首先聊的是 “团队的共识管理”。为什么这很重要?因为它是团队协作效率的最大基础。如果连对协作的工作流都没有共识,那团队真的是一盘散沙了。今天我们几乎不会遇到工作方式上的问题,不是别的原因,是开源给予我们的礼物。它让全球的程序员、全球的科技企业,都养成了一模一样的工程习惯。
云计算与服务外包
云服务是新的跨组织分工的形态。无论是传统的外包,还是开源的众包,它们都属于源代码外包。这类外包的共同特点是,它们不对结果负责。对于传统外包,项目验收结束,双方一手交钱一手交货,至于用得好不好,那是甲方自己的事情。
对于开源软件来说,那更是完全免责,你爱用不用,用了有什么问题责任自负。当然有很多公司会购买开源软件的商业支持,这不难理解,除了有人能够帮助我一起完成项目上线外,最重要的是要有人能够给我分担出问题的责任。互联网为跨组织协同带来了新的机会。我可以 24 小时为另一个组织服务,而无需跑到对方的办公室,和他们团队物理上处在一起。
这就是云计算。云计算从跨组织协同的角度来看,不过是一种新的交付方式。我们不再是源代码交付,而是服务交付。所以,你也可以把云计算看着一种外包,我们称之为服务外包。大部分的基础设施,都可以以服务外包的方式进行交付。这中间释放的生产力是惊人的。一方面,云计算与传统外包不同,它对结果负责,有服务 SLA 承诺。一旦出问题,问题也可以由云服务提供方自己解决,而无需业务方介入,这极大降低了双方的耦合,大家各司其职。
另一方面,它简化了业务方的业务系统,让它得以能专注自己真正的核心竞争力的构建。站在生产效率角度看,不难理解为什么我们会坚信云服务是未来必然的方向。
外包方式的选择
任何企业都存在于社会生态之中,我们无法避开组织外部的分工协同问题。怎么选择跨组织的协同方式?在七牛,自成立以来我们就一直有一句话谈我们对跨组织协同的看法:我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。
这句话有它一定的道理,但也有它模糊的地方。首先是关于 “核心竞争力相关”。我们并没有太清晰地去定义什么样的东西是我们核心竞争力相关,什么不相关。一些程序员对此理解可能会比较 “技术化”,认为业务系统的核心模块就是核心竞争力。与它相关的东西就是核心竞争力相关。
但更合理的视角不是技术视角,而是业务视角。我们每一家企业都是因为服务客户而存在。所以,与服务客户的业务流越相关,越不能外包,而是要自己迭代优化,建立服务质量与效率的竞争优势。另外,外包的选择需要非常谨慎。很多开发人员都有随意引用开源项目的习惯,这一定程度上给项目带来了不确定的风险。我一直认为,开源项目的引入需要严格把关。严谨来说,开源项目引入大部分情况下是属于我说的 “基础架构” 选择的范畴,这同样是架构师团队需要承担的重要职责,一定要有正规的评估流程。
结语
今天我们聊的话题是跨组织的分工与协同。在形态上,我们可以分为:传统外包、开源与云服务。当然还有就是我们今天没有讨论的使用外部商业软件。从形态来说,商业软件很接近传统外包,但是从它的边界来说,因为商业软件往往有明确的业务边界,所以在品质上会远高于外包。当然定制过于严重的商业软件例外,它在某种程度上来说退化为了传统外包。
在外包方式的选择上,我们的建议是:我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。
七、结束语| 放下技术人的身段,用极限思维提升架构能力
从工程角度来说,架构师的存在几乎是一种必然。传统项目工程也有架构师的角色,只不过软件工程有其特殊性,它快速变化,充满了不确定性,所以架构师的重要性的比重会被进一步放大。但是如何才能成为优秀的软件工程架构师?
传统的架构图书往往从架构思维开始。但是,我认为它们错了。这里面最关键的问题在于:架构并不是 “知识点”。
架构思维的确非常非常重要。但是,熟读架构思维并不足以让人成为一名优秀的架构师。关于这一点,我经常拿中国传统的武学文化做类比。武功招式可以精确传授,是 “知识点”,掌握了就是掌握了,理论上可以做到分毫不差。但是,架构不是武功招式。它更像内功,它不是 0 和 1,没有清晰的掌握和没有掌握这样泾渭分明的区别。
在架构能力上,没有最好,只有更好。
这是为什么我们的架构课并不是从架构思维开始,而是采用双线结构。它基本上围绕着以下两个脉络主线来展开内容:
- 如何从零开始一步步构建出整个信息世界;
- 在整个信息世界的构建过程中,都用了哪些重要的架构思维范式,以及这些范式如何去运用于你平常的工程实践中。
这两大脉络相辅相成。首先,我们通过还原信息世界的构建过程,剥离出了整个信息世界的核心骨架,这也是最真实、最宏大的架构实践案例。
其次,我们结合这个宏大的架构实践来谈架构思维,避免因对架构思维的阐述过于理论化而让人难以理解。
最后,架构就是对业务系统的正交分解。因此,整个信息科技的演化过程,自然而然形成了分层:基础架构 + 业务架构。
基础架构的产生是对业务架构不断深入理解的过程。越来越多的共性需求从业务架构抽离出来,成为信息科技的基础设施。
作为架构师,我们需要坚持对业务进行正交分解的信念,要坚持不断地探索各类需求的架构分解方法。这样的思考多了,我们就逐步形成了各种各样的架构范式。
这些架构范式,并不仅仅是一些架构思维,而是 “一个个业务只读、接口稳定、易于组合的模块 + 组合的方法论”,它们才是架构师真正的武器库。这个武器库包含哪些内容?
首先,它应该包括信息科技形成的基础架构。努力把前辈们的心血,变成我们自己真正的积累。光会用还不够,以深刻理解它们背后的架构逻辑,确保自己与基础架构最大程度上的 “同频共振”。
只有让基础架构完全融入自己的思维体系,同频共振,我们才有可能在架构设计需要的时候 “想到它们”。这一点很有趣。有些人看起来博学多才,头头是道,但是真做架构时完全想不到他的 “博学”。
从体系结构来说,这个基础架构包含哪些内容?
其一,基础平台。包括:冯·诺依曼体系、编程语言、操作系统。
其二,桌面开发平台。包括:窗口系统、GDI 系统、浏览器与小程序。当然我们也要理解桌面开发背后的架构逻辑,MVC 架构。
其三,服务端开发平台。包括:负载均衡、各类存储中间件。服务端业务开发的业务逻辑比桌面要简单得多。服务端难在如何形成有效的基础架构,其中大部分是存储中间件。
其四,服务治理平台。主要是以容器技术为核心的 DCOS(数据中心操作系统),以及围绕它形成的整个服务治理生态。这一块还在高速发展过程中,最终它将让服务端开发变得极其简单。
理解了这些基础架构,再加上你自己所处行业的领域知识,设计出一个优秀业务系统对你来说就只是轻车熟路而已。
这也是为什么这个架构课的内容结构是目前这个样子组织的。因为消化基础架构成为架构师自身的本领,远比消化架构设计原则,架构思维逻辑要难得多。消化基础架构的过程,同时也是消化架构思维的过程。
把虚的事情往实里做,才有可能真正做好。
当然提升架构能力,不完全与成为架构师这件事情等同。架构能力其实是一种属性,并不是只有架构师需要架构能力。软件开发工程师、SRE、甚至包括产品经理,都需要具备架构能力。
而架构师这个特殊的岗位,则是因为软件工程的需要而产生的。它从更全局的视角来把控工程的演进方向,以确保整个业务系统经历几年甚至几十年的迭代,仍然可以快速适应变化,而不至于老化。成为架构师并不是一件纯技能的事情。
架构师需要放下技术人的身段,学会 “共情”。与用户共情,理解用户的所思所想。与开发人员共情,理解技术人的所思所想。与公司共情,理解公司的发展诉求。
架构师需要学会 “认同他人,反思迭代自己”。不要在不了解背景的情况下,随意推翻别人写的代码,而理由可能仅仅是不符合你的个人风格。当然反过来完全看不到项目的问题同样要不得,但这往往是受限于个人能力。要提升自己的架构水平,需要在实践中不断反思,不断在自我否定中成长。
不过我们今天把话题的重心收敛到架构能力上。怎么才有意识地通过训练来提升自己的架构能力?实践对架构能力不可或缺。
在现实中,不少技术人员连函数规格都想不清楚。他们关心你是怎么 “实现” 的,但是却不关心 “接口规格” 是什么样的,接口规格是否符合函数的 “业务语义”。
要提升架构能力,首先得做到规格为先,而不是实现为先。不要动不动问怎么实现的。要首先谈这个规格合不合理,是否存在多余的依赖。进一步来说,要多去谈这个函数(或软件实体)的业务范畴合不合理,是否应该换一个切分的姿势。
其实 review 自己的代码也是一种极佳的架构能力的提升手段。对自己刚刚写完的代码,去 review 它,从中找出问题。如此反复训练,就能实现自我能力的提升。这其实是最高效的自我提升的方式。如果团队其他成员 code review 发现了你的问题,你得反思一下为什么自己发现不了。
很多人追逐实现新的业务系统,通过做新系统来找到满足感。但是实际上对架构师来说,恰恰是反复打磨既有系统是更加锻炼人的。如果你一年前实现的系统今天仍然很满意,那就需要警醒,因为这一年你在原地踏步。
在架构能力上,没有最好,只有更好。
这里我想分享一段我自己 review 自己代码的特殊经历。事情发生在我大学期间,当时的电脑相对我们大部分学生的购买力来说,还是非常昂贵。所以我和另外 4 个同学花了 7500 元合买了一台电脑。结果就是,我们 5 个人轮流使用这台电脑。这意味着,我一周平均只能用一天多一点时间。再刨除上课时间,我真正能够上机的时间并不多。
而当时的我对编程非常着迷,所以我绝大部分的上机时间都花在编程上。作为物理系的学生,正常来说我学的编程语言是 Fortran。但我很快就把 Fortran 课程自学完了,并从老师口中和 Fortran 课程的附录中了解到了 C 语言。于是我找物理系高年级的同学搞到了 Turbo C 2.0,开始翻遍学校图书馆的图书自学 C 语言。
为了能够高效利用一周只有一天多的上机时间,我尝试把程序写到纸上,并且提前进行 code review,确保尽可能多地发现程序中的错误,以减少上机过程中的调试时间。在一次数学建模竞赛里,我和另外两位同学(廖唯棨和程胜峰)一组,其中用到了 Dijkstra 的最短路径算法。看完算法逻辑的介绍后,我直接一遍写成最终的代码,没有经过任何调试过程。这让在旁边看着的同学廖唯棨觉得很神奇,问我是不是之前实现过 Dijkstra 算法。但其实于我而言,这不过是长期养成自我 review 代码习惯的结果而已。
这个习惯持续了三年之久。这三年里,我开始的时候都是先把代码写到纸上并完成 review,然后再到电脑上。但是到后期这个习惯就变了,我不再需要把所有细节都提前写到纸上,而是只需要提前准备好骨架:整个程序串起来的思路是什么。
我大学期间写过很多高代码量的程序。其实第一个 C 程序就不短,是一个仿 DOSKEY 的程序。后来也做过汇编语言的 IDE。这是因为学汇编的时候,发现没有好的汇编语言集成环境,于是就自己做了一个。至于为什么学汇编?是因为我想写一个 C++ 编译器,感受一下语言实现者的体验。另外,我也尝试在 DOS 操作系统下实现了一个图形界面库,并用它做了图片查看器和 MP3 播放器。
在代码量非常大的时候,人的脑容量就完全无法把这个实现装到头脑中。这时 “规格重于实现” 背后的意义就完全体现出来了。通过规格串起整个业务系统,以此把业务系统装到脑子里,这就是很朴素的架构 “骨架” 思维。
这不是一个假想实验。它是我的亲身经历。这段经历启发我意识到极限思维对架构能力提升的重要性。架构没有最好,只有更好。在极有限的上机时间里,在没有电脑的情况下,我们只能选择把更多的逻辑装进脑子里。
这个过程还可以更进一步。我们不断训练自己对不同业务领域的架构范式的理解。直至最终,我们头脑中可以装得下整个信息科技的骨架。到那时,单就架构能力而言,你就是最顶级的架构师了。
