mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 06:33:48 +08:00
del
This commit is contained in:
88
极客时间专栏/geek/许式伟的架构课/软件工程篇/68 | 软件工程的宏观视角.md
Normal file
88
极客时间专栏/geek/许式伟的架构课/软件工程篇/68 | 软件工程的宏观视角.md
Normal file
@@ -0,0 +1,88 @@
|
||||
<audio id="audio" title="68 | 软件工程的宏观视角" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/99/41/99d1904cef5c342132fe1ba2e2c8ce41.mp3"></audio>
|
||||
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
## 软件工程
|
||||
|
||||
今天开始,我们进入第六章,谈谈软件工程。
|
||||
|
||||
我理解的架构师的职责其实是从软件工程出发的。也许大家都学过软件工程,但如果我们把软件工程这门课重新看待,这门学科到底谈的是什么?是软件项目管理的方法论?
|
||||
|
||||
无论如何,软件工程是一门最年轻的学科,相比其他动辄跨世纪的自然科学而言,软件工程只有 50 年的历史。这门学科的实践太少了,任何一门学科的实践时间短的话,都很难沉淀出真正高效的经验总结,因为这些总结通常都是需要很多代人共同推动来完成的。
|
||||
|
||||
为什么说它只有 50 年时间呢?
|
||||
|
||||
我们先来看看 C 语言,一般意义上来说,我们可能认为它是现代语言的开始。C 语言诞生于 1970 年,到现在是 49 年。再看 Fortran,它被认定为是第一个高级语言,诞生于 1954 年,那时候主要面向的领域是科学计算。Fortran 的程序代码量普遍都还不大,量不大的时候谈不上工程的概念。
|
||||
|
||||
这也是我为什么说软件工程这门学科很年轻,它只有 50 岁。对于这样一个年轻的学科,我们对它的认知肯定还是非常肤浅的。
|
||||
|
||||
我在这个架构课的序言 “[开篇词 | 怎样成长为优秀的软件架构师?](https://time.geekbang.org/column/article/89668?utm_term=pc_interstitial_28)” 一上来就做了软件工程和建筑工程的对比。通过对比我们可以发现,二者有非常大的区别,具体在于两点:
|
||||
|
||||
其一,不确定性。为什么软件工程有很大的不确定性?大部分大型的软件系统都有几千甚至几万人的规模,而这几千几万人中,却没有两个人的工作是重复的。
|
||||
|
||||
虽然大家都在编程,但是编程的内容是不一样的。每个人昨天和今天的工作也是不一样的,没有人会写一模一样的代码,我们总是不停地写新的东西,做新的工作。这些东西是非常不同的,软件工程从事的是创造性的工作。
|
||||
|
||||
大家都知道创造是很难的,创造意味着会有大量的试错,因为我们没有做过。大部分软件的形成都是一项极其复杂的工程,它们远比传统的工程复杂得多,无论是涉及的人力、时间还是业务的变数都要多很多。这些都会导致软件工程有非常大的不确定性。
|
||||
|
||||
其二,快速变化。建筑工程在完工以后就结束了,基本上很少会进行变更。但在软件工程里,软件生产出来只是开始。只要软件还在服务客户中,程序员们的创造过程就不会停止,软件系统仍然持续迭代更新,以便形成更好的市场竞争力。
|
||||
|
||||
这些都与传统建筑工程的模式大相径庭。一幢建筑自它完成之后,所有的变化便主要集中在一些软装的细节上,很少会再发生剧烈的变动,更不会持续地发生变动。但软件却不是这样,它从诞生之初到其生命周期结束,自始至终都在迭代变化,从未停止。
|
||||
|
||||
以上这两点都会导致软件工程区别于传统意义上的所有工程,有非常强的管理难度。过去那么多年,工业界有非常多的工程实践,但是所有的工程实践对软件工程来说都是不适用的,因为二者有很大的不一样。
|
||||
|
||||
今天如果我们站在管理的视角再看软件工程的话,我们知道管理学谈的是确定性。管理学本身的目的之一就是要抑制不确定性,产生确定性。
|
||||
|
||||
比如,开发工期、时间成本是否能确定。比如,人力成本、研发成本以及后期运维的成本是否能确定。
|
||||
|
||||
所以,软件项目的管理又期望达到确定性。但软件工程本身是快速变化的,是不确定的。这就是软件工程本身的矛盾。我们的目标是在大量的不确定性中找到确定性,这其实就是软件工程最核心的点。
|
||||
|
||||
## 架构师的职责
|
||||
|
||||
如果用 “瀑布模型” 的方式来表达,现代软件工程的全过程大体如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/84/e95250171fc1ab33258895c10bd8dd84.png" alt="">
|
||||
|
||||
从开始的需求与历史版本缺陷,到新版本的产品设计,到架构设计,到编码与测试,到最终的产品发布,到线上服务的持续维护。
|
||||
|
||||
贯穿整个工程始终的,还有不变的团队分工与协同,以及不变的质量管理。
|
||||
|
||||
更为重要的是,这个过程并不是只发生一遍,而是终其生命周期过程中,反复迭代演进。
|
||||
|
||||
它是一个生命周期往往以数年甚至数十年计的工程。对于传统工程,我们往往也把一个工程称为项目,项目工程。但软件工程不同,虽然我们平常也有项目的概念,但软件工程并不是一个项目,而是无数个项目。每个项目只是软件工程的一个里程碑(Milestone)。
|
||||
|
||||
所以,光靠把控软件工程师的水平,依赖他们自觉保障工程质量,是远远不够的。软件工程是一项非常复杂的系统工程,**它需要依赖一个能够掌控整个工程全局的团队,来规划和引导整个系统的演变过程。这个团队就是架构师团队。**
|
||||
|
||||
软件架构师的职责,并不单单是我们通常理解的,对软件系统进行边界划分和模块规格的定义。从根本目标来说,软件架构师要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。
|
||||
|
||||
因此,虽然架构师的确是一个技术岗,但是架构师干的事情,并不是那么纯技术。
|
||||
|
||||
首先是用户需求的解读。怎么提升需求分析能力,尤其是需求演进的预判能力?它无关技术,关键是心态,心里得装着用户。除了需要 “在心里对需求反复推敲” 的严谨态度外,对用户反馈的尊重之心也至关重要。
|
||||
|
||||
其次是产品设计。产品边界的确立过程虽然是产品经理主导,但是架构师理应深度参与其中。原因在于,产品功能的开放性设计不是一个纯粹的用户需求问题,它通常涉及技术方案的探讨。因此,产品边界的确立不是一个纯需求,也不是一个纯技术,而是两者合而为一的过程。
|
||||
|
||||
以上两点,是架构本身的专业性带来的,在前面五章中已经谈过很多,我们这里不再展开。在本章中,我们更多是从工程本身出发。这些话题是因软件工程的工程性而来,属于工程管理的范畴,但它们却又通常和架构师的工作密不可分。
|
||||
|
||||
这里面最为突出但也非常基础的,是贯穿软件工程始终的 “团队分工与协同” 问题、“软件的质量管理” 问题。从 “团队分工与协同” 来说,话题可以是团队的目标共识,也可以是做事方式的默契,各类规范的制定。从 “软件的质量管理” 来说,话题可能涉及软件的版本发布,质量保障的过程体系等等。
|
||||
|
||||
从更宏观的视角看,我们还涉及人力资源规划的问题。什么东西应该外包出去,包给谁?软件版本的计划是什么样的,哪些功能先做,哪些功能后做?
|
||||
|
||||
看起来,这些似乎和架构师的 “本职工作” 不那么直接相关。但是如果你认同架构师的职责是 “对软件工程的执行结果负责”,那么就能够理解为什么你需要去关注这些内容。
|
||||
|
||||
## 结语
|
||||
|
||||
软件工程本身是一个非常新兴、非常复杂的话题。可能需要再花费 50 年这样漫长的时间才能形成更清晰的认知(例如,我们第四章 “服务治理篇” 专门探讨了现代软件工程全过程最后一个环节 “线上服务管理” 这个话题)。
|
||||
|
||||
作为架构课的一部分,这一章我们将主要精选部分与架构师的工作关系密切的话题来进行讨论,主要包括:
|
||||
|
||||
- 团队的共识管理;
|
||||
- 如何阅读别人的代码;
|
||||
- 怎么写设计文档;
|
||||
- 发布单元与版本管理;
|
||||
- 软件质量管理:单元测试、持续构建与发布;
|
||||
- 开源、云服务与外包管理;
|
||||
- 软件版本迭代的规划;
|
||||
- 软件工程的未来。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “团队的共识管理”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
110
极客时间专栏/geek/许式伟的架构课/软件工程篇/69 | 团队的共识管理.md
Normal file
110
极客时间专栏/geek/许式伟的架构课/软件工程篇/69 | 团队的共识管理.md
Normal file
@@ -0,0 +1,110 @@
|
||||
<audio id="audio" title="69 | 团队的共识管理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/94/02/94def47acfcfe0564aa3e5fef0bd2202.mp3"></audio>
|
||||
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
软件工程是一项团体活动,大家有分工更有协同。不同的个体因为能力差别,可以形成十倍以上的生产力差距。而不同团体更是如此,他们的差距往往可以用天壤之别来形容。
|
||||
|
||||
这差距背后的原因,关乎的是协同的科学。
|
||||
|
||||
## 团队共识
|
||||
|
||||
有的团体像一盘散沙,充其量可以叫团伙。有的团体则有极强的凝聚力,整个团队上下同心,拧成一股绳,这种团体才是高效率组织,是真正意义上的团队。
|
||||
|
||||
团队靠什么上下同心?靠的是共识。
|
||||
|
||||
那么,什么是团队的共识?
|
||||
|
||||
团队的共识分很多层次。其一,团队是不是有共同的目标。其二,团队是不是有共同的行事做人的准则。其三,对产品与市场的要与不要,以及为什么要或为什么不要,是否已达成一致。其四,对执行路径有没有共同的认知。其五,有没有团队默契,是否日常沟通交流很多地方不必赘述,沟通上一点即透。
|
||||
|
||||
一个团体如果缺乏共同的目标,那么它最多能够算得上是一个团伙,而不能称之为团队。
|
||||
|
||||
团队的目标也分很多层次。为什么很多企业都会谈他们的使命和愿景,是因为它是这个企业作为一个团队存在的意义,是企业所有人共同的长远目标。
|
||||
|
||||
人是愿景型动物,需要看到未来。越高级的人才越在乎团队存在的意义。所以高科技公司的人才通常只能去影响,而不是像一些人心中理解的那样,认为管理是去控制。
|
||||
|
||||
愿景是一种心力。人有很强的主观能动性。一旦人相信企业的使命与愿景,员工就变得有很强烈的使命感,有强烈的原动力。员工的行为方式也就会潜移默化发生变化。
|
||||
|
||||
不过,有共同的远景目标的团队仍然有可能走向分裂。
|
||||
|
||||
中国有句古话说得好:“道不同,不相为谋”。团队有没有相同的价值观,有没有相同的行事做人的准则,这些更根本性的基础共识,极有可能会成为压垮团队的稻草。
|
||||
|
||||
共识大于能力。如果一个人有很强的个人能力,但是却和团队没有共同的愿景,或者没有共同的价值观,那么能力越大产生的破坏性也就越大。
|
||||
|
||||
## 怎么达成共识?
|
||||
|
||||
团队有了共同的使命、愿景与价值观,就有了共同努力把一件事情干成的最大基础。然而,这并不代表这个团队就不会遇到共识问题。
|
||||
|
||||
团队仅有远期的目标是不够的,还要有中短期的目标。企业的使命和愿景需要由一个个的战略行动来落地。我们的产品定位怎么样,选择哪些细分市场去切入,这些同样需要团队达成共识。
|
||||
|
||||
怎么去达成共识?
|
||||
|
||||
越 “聪明” 的团队负责人,往往越容易忽视达成共识的难度。他们通常会召开会议,然后把自己的想法说给大家听。半个小时后,兄弟们迷茫地回去了。
|
||||
|
||||
在团队还小的时候,这种简单共识的方式很可能是可以奏效的,尤其是当团队负责人还能够一一去检查每个人的工作内容时,所有的理解偏差都能够得到比较及时的纠正。
|
||||
|
||||
但是团队规模稍微变大一些,这种简单共识突然就失效了。“我明明已经告诉他们要做什么了。” 负责人有时候困惑于团队成员为什么并没有理解他的话。
|
||||
|
||||
这是因为他还并不理解真正的共识意味着什么。也没有对达成共识的难度有足够的认知。
|
||||
|
||||
让更多人参与到决策形成的过程现场,是更好的共识达成的方式。通过同步足够充分的信息,通过共创而非传达决策的方式让结论自然产生。
|
||||
|
||||
这个共创过程不必团队所有人都参与,但要确保所有影响落地的关键角色都在,并确保参与这个过程的人都能够产生思想的碰撞,而非做个吃西瓜群众。
|
||||
|
||||
## 契约与共识效率
|
||||
|
||||
目标与执行路径达成了共识,这还不够。我们还需要把共识表达出来,形成文字。
|
||||
|
||||
为什么这很重要?
|
||||
|
||||
因为共识之所以为共识,是因为它不是空中楼阁,不是口号,而是指导我们做战略选择的依据,指导我们平常行为的依据。
|
||||
|
||||
所以,共识就是团队协作的契约。契约的表达越是精确而无歧义,团队协作中主观能动性就越高,执行的效率也就越高。
|
||||
|
||||
对于架构过程同样如此。
|
||||
|
||||
架构过程实际上是团队共识形成与确认的过程。架构设计需要回答两个基本的问题:
|
||||
|
||||
- 系统要做成什么样?
|
||||
- 怎么做?
|
||||
|
||||
架构设计为什么叫架构设计,是因为架构师的工作中除了架构,还有设计。设计其实谈的就是 “系统要做成什么样”。
|
||||
|
||||
>
|
||||
设计高于架构。
|
||||
|
||||
|
||||
设计强调规格,架构强调实现。规格设计是架构过程的最高共识。所以,规格高于实现。我们用架构的全局性和系统性思维去做设计。
|
||||
|
||||
一些架构师乐衷于画架构图,把它当作是架构师最重要的工作内容。但架构图在共识的表达上并不太好。因为共识是需要精确的、无歧义的。而架构图显然并不精确。
|
||||
|
||||
对于一个工程团队来说,没有精确的共识很可怕。它可能导致不同模块的工作牛头不对马嘴,完全无法连接起来,但是这个风险没有被暴露,直到最后一刻里程碑时间要到了,要出版本了,大家才匆匆忙忙联调,临时解决因为架构不到位产生的 “锅”。
|
||||
|
||||
这时候人们的动作通常会走形。追求的不再是架构设计的好坏,而是打补丁,怎么把里程碑的目标实现了,别影响了团队绩效。
|
||||
|
||||
我们作个类比,这种不精确的架构,就好比建筑工程中,设计师画了一个效果图,没有任何尺寸和关键细节的确认,然后大家就分头开工了。最后放在一起拼接(联调),发现彼此完全没法对上,只能临时修修改改,拼接得上就谢天谢地了。是不是能够和当初效果图匹配?让老天爷决定吧。
|
||||
|
||||
更精确描述架构的方法是定义每个模块的接口。接口可以用代码表达,这种表达是精确的、无歧义的。架构图则只是辅助模块接口,用于说明模块接口之间的关联。
|
||||
|
||||
尊重契约,尊重共识精确的、无歧义的表达,非常非常重要。
|
||||
|
||||
绝大部分哪怕是非常优秀的架构师,在系统设计(也叫概要设计)阶段通常也只会形成系统的概貌,把子系统的划分谈清楚,把子系统的接口规格谈清楚。
|
||||
|
||||
但实际上概要设计阶段最好的状态并不是只有设计文档。
|
||||
|
||||
为了降低风险,系统设计阶段也应该有代码产出。
|
||||
|
||||
这样做有两个方面的目的。其一,系统的初始框架代码。也就是说,系统的大体架子已经搭建起来了。其二,原型性的代码来验证。一些核心子系统在这个阶段提供了 mock 的系统。
|
||||
|
||||
这样做的好处是,一上来我们就关注了全局系统性风险的消除,并且给了每个子系统或模块的负责人一个更具象且确定性的认知。
|
||||
|
||||
代码即文档。代码是理解一致性更强的文档。
|
||||
|
||||
## 结语
|
||||
|
||||
这一讲我们谈的是协同的科学。为什么有的团队效率极高,有的团队却进展缓慢,从背后的协同效率来说,共识管理是根因中的根因。
|
||||
|
||||
共识有非常多的层次。不同层次的共识处于完全不同的维度。它们都极其重要,且相互不可替代。当某个层次的共识出问题的时候,我们需要在相应的层次去解决它。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “怎么写设计文档”。原计划我们下一讲是 “如何阅读别人的代码”,但是我想先顺着共识这个话题谈问题谈清楚。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
138
极客时间专栏/geek/许式伟的架构课/软件工程篇/70 | 怎么写设计文档?.md
Normal file
138
极客时间专栏/geek/许式伟的架构课/软件工程篇/70 | 怎么写设计文档?.md
Normal file
@@ -0,0 +1,138 @@
|
||||
<audio id="audio" title="70 | 怎么写设计文档?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c1/35/c184500f93932bd7434f01bd4bbaf035.mp3"></audio>
|
||||
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
在 “[68 | 软件工程的宏观视角](https://time.geekbang.org/column/article/182924)” 一讲中,我们用最基本的 “瀑布模型” 来描述现代软件工程的全过程,大体如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/71/41/7141be3e927921fa8a73cd3d4a753541.png" alt="">
|
||||
|
||||
在这个过程中,有两个阶段非常关键:一个是 “产品设计”,一个是 “架构设计”。产品设计由产品经理主导,关注的是 “如何以产品特性来系统化地满足用户需求”。架构设计由架构师主导,关注的是 “业务系统如何系统化地进行分解与交付”。
|
||||
|
||||
“设计” 一词非常精妙。无论是 “产品设计”,还是 “架构设计”,其实谈的都是 “需求如何被满足” 这件事情的共识。无论是 “产品文档”,还是 “架构文档”,它们都是设计文档的一种,都有团队内及团队间的协同价值。
|
||||
|
||||
上一讲 “[69 | 团队的共识管理](https://time.geekbang.org/column/article/183900)” 我们已经从团队的协同角度,谈了共识的重要性。本质上,我们也是在谈 “设计” 的重要性。换个角度来说,一个企业的使命、愿景与价值观,何尝不是这个企业最高维度的 “设计” 呢?
|
||||
|
||||
产品经理与架构师是一体两面,对人的能力要求的确会比较像,但是分工不同,关注的维度不同。产品经理关注的维度,其关键词是:用户需求、技术赋能、商业成功。而架构师关注的维度,其关键词是:用户需求、技术实现、业务迭代。
|
||||
|
||||
今天我们谈的 “设计文档”,重点聊的是 “架构设计文档” 怎么写,但是本质上所有 “设计文档” 的内容组织逻辑,都应该是相通的。它们的内容大体如下:
|
||||
|
||||
- 现状 :我们在哪里,现状是什么样的?
|
||||
- 需求:我们的问题或诉求是什么,要做何改进?
|
||||
<li>需求满足方式:
|
||||
<ul>
|
||||
- 要做成什么样,交付物规格,或者说使用界面(接口)是什么?
|
||||
- 怎么做到?交付物的实现原理。
|
||||
|
||||
关于设计文档内容组织的详细说明,我们在前面 “[45 | 架构:怎么做详细设计?](https://time.geekbang.org/column/article/142032)” 中已经进行过交代。概括来说,这些设计文档要素的关键在于以下几点。
|
||||
|
||||
现状:不要长篇累牍。现状更多的是陈述与我们要做的改变相关的重要事实,侧重于强调这些事实的存在性和重要性。
|
||||
|
||||
需求:同样不需要长篇累牍。痛点只要够痛,大家都知道,所以需求陈述是对痛点和改进方向的一次共识确认。
|
||||
|
||||
需求满足方式:要详写,把我们的设计方案谈清楚。具体来说,它包括 “交付物规格” 和 “实现原理” 两个方面。
|
||||
|
||||
交付物规格,或者说使用界面,体现的是别人要怎么使用我。对于 “产品设计”,交付物规格可能是 “产品原型”。对于 “架构设计”,交付物规格可能是 “网络 API 协议” 或者 “包(package)导出的公开类或函数”。
|
||||
|
||||
实现原理,谈的是我们是怎么做到的。对于 “产品设计”,它谈的是用户需求对应的 UserStory 设计,也就是业务流具体是怎么完成的。而对于 “架构设计”,它谈的是 UserStory 具体如何被我们的程序逻辑所实现。
|
||||
|
||||
以下这个公式大家都耳熟能详了:
|
||||
|
||||
>
|
||||
程序 = 数据结构 + 算法
|
||||
|
||||
|
||||
它是一个很好的指导思想。当我们谈程序实现逻辑时,我们总是从数据结构和算法两个维度去描述它。其中,“数据结构” 可以是内存数据结构,也可以是外存数据结构,还可以是数据库的 “表结构”。“算法” 基于 “数据结构”,它描述的是 UserStory 的具体实现,它可以是 UML 时序图(Sequence Diagram),也可以是伪代码(Pseudo Code)。
|
||||
|
||||
## 多个设计方案的对比
|
||||
|
||||
在现实中,一篇设计文档有时候不是只有一个设计方案,而是有多个可能的需求实现方式。在这个时候,通常我们会概要地描述清楚两个设计方案的本质差别,并且从如下这些维度进行对比:
|
||||
|
||||
- 方案的易实施性与可维护性。
|
||||
- 方案的时间复杂度与空间复杂度。
|
||||
|
||||
不同的业务系统倾向性不太一样。对于绝大部分业务,我们最关心的是工程效率,所以方案的易实施性与可维护性为先;但是对于部分对成本与性能非常敏感的业务,则通常在保证方案的时间复杂度与空间复杂度达到业务预期的前提下,再考虑工程效率。
|
||||
|
||||
在确定了设计方案的倾向性后,我们就不会就我们放弃的设计方案做过多的展开,整个设计文档还是以描述一种设计方案为主。
|
||||
|
||||
如果我们非要写两套设计方案,这时应该把设计文档分为两篇独立的设计文档,而不是揉在一起。
|
||||
|
||||
你可能觉得没有人会这么不怕麻烦,居然写两套设计方案。但是如果两套设计方案的比较优势没有那么显著时,现实中写两套设计方案确实是存在的,并且应该被鼓励。
|
||||
|
||||
为什么这么说?
|
||||
|
||||
这是因为 “设计” 是软件工程中的头等大事,我们应该在这里 “多浪费点时间”,这样的 “浪费” 最终会得到十倍甚至百倍以上的回报。
|
||||
|
||||
## 使用界面(接口)
|
||||
|
||||
在描述交付物的规格上,系统的概要设计,与模块的详细设计很不一样。
|
||||
|
||||
对于 “模块的详细设计” 来说,规格描述相对简单。因为我们关注的面只是模块本身,而非模块之间的关系。对于模块本身,我们核心关注点是以下两点:一是接口是否足够简单,是否自然体现业务需求。二是尽可能避免进行接口变更,接口要向前兼容。
|
||||
|
||||
关于接口变更,后面有机会我们还会进行详细的讨论,这一讲先略过。
|
||||
|
||||
但对于 “系统的概要设计” 来说,我们第一关心的是模块关系,第二关心的才是各个模块的核心接口。这些接口能够把系统的关键 UserStory 都串起来。
|
||||
|
||||
表达模块关系在某种程度来说的确非常重要,这可能是许多人喜欢画架构图的原因。
|
||||
|
||||
但描述模块间的关系的确是一件比较复杂的事情。我们在 “[32 | 架构:系统的概要设计](https://time.geekbang.org/column/article/117783)” 这一讲中实际上先回避了这个问题。
|
||||
|
||||
一种思路是我们不整体描述模块关系,直接基于一个个 UserStory 把模块之间的调用关系画出来。比如对于对象存储系统,我们上传一个文件的业务流程图看起来是这样的:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/89/a126729331be7854fad7435d293ced89.png" alt="">
|
||||
|
||||
这类图相信大家见过不少。但它从模块关系表达上并不是好的选择,因为根本并没有对模块关系进行抽象。这类图更多被用在面向客户介绍 API SDK 的背后的实现原理时采用,而非出现在设计文档。
|
||||
|
||||
如果只是对于 UserStory 业务流程的表达来说,UML 时序图通常是更好的表达方式。
|
||||
|
||||
但是,怎么表达模块关系呢?
|
||||
|
||||
一个方法是对模块的调用接口进行分类。通过 “[62 | 重新认识开闭原则 (OCP)](https://time.geekbang.org/column/article/175236)” 这一讲我们知道,一个模块对外提供的访问接口无非是:
|
||||
|
||||
- 常规 DOM API,即正常的模块功能调用;
|
||||
- 事件(Event)的发送与监听;
|
||||
- 插件(Plugin)的注册。
|
||||
|
||||
这些不同类型的访问接口,分别代表了模块间不同的依赖关系。我们回忆一下 MVC 的框架图,如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/32/cb/32c7df68c3f5d11a0a32f80d7c3a42cb.png" alt="">
|
||||
|
||||
在图中,View 监听 Model 层的数据变更事件。View 转发用户交互事件给 Controller。Controller 则负责将用户交互事件转为 Model 层的 DOM API 调用。
|
||||
|
||||
另一个表达模块关系的视角,是从架构分解看,我们把系统看作 “一个最小化的核心系统 + 多个彼此正交分解的周边系统”。例如,我们实战案例 — 画图程序的模块关系图如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/75/6270cc365ce1a19b230e243188ff7375.png" alt="">
|
||||
|
||||
需要清楚的是,模块关系图的表达是非常粗糙的,虽然它有助于我们理解系统分解的逻辑。为了共识的精确,我们仍然需要将各个模块核心的使用界面(接口)表达出来。
|
||||
|
||||
## 实现原理
|
||||
|
||||
谈清楚了交付物的规格,我们就开始谈实现。对于 “[系统的概要设计](https://time.geekbang.org/column/article/117783)” 与 “[模块的详细设计](https://time.geekbang.org/column/article/142032)”,两者实现上的表达有所不同。
|
||||
|
||||
对于模块的详细设计来说,需要先交代清楚 “数据结构” 是什么样的,然后再将一个个 UserStory 的业务流程讲清楚。
|
||||
|
||||
对于系统的概要设计来说,核心是交代清楚不同模块的配合关系,所以无需交代数据结构,只需要把一个个 UserStory 的业务流程讲清楚。
|
||||
|
||||
无论是否要画 UML 时序图,在表达上伪代码(Pseudo Code)的设计都是必需的。
|
||||
|
||||
伪代码的表达方式及语义需要在团队内形成默契。这种伪代码的语义表达必须是精确的。
|
||||
|
||||
比如,对于网络请求相关的伪代码,我们可以基于类似 [qiniu httptest](https://github.com/qiniu/httptest) 的语法,如下:
|
||||
|
||||
```
|
||||
# 请求
|
||||
post /v1/foo/bar json {...}
|
||||
|
||||
# 返回
|
||||
ret json {...}
|
||||
|
||||
```
|
||||
|
||||
类似地,对于 MongoDB,我们可以直接用 MongoDB 的 JavaScript 脚本文法。对于 MySQL,则可以直接基于 SQL 语法。等等。
|
||||
|
||||
## 结语
|
||||
|
||||
前面在 “[45 | 架构:怎么做详细设计?](https://time.geekbang.org/column/article/142032)” 我们实际上已经大体介绍了模块级的设计文档怎么写。所以这一讲我们主要较为全面地补充了各类设计文档,包括产品设计、系统的概要设计等在细节上与模块设计文档的异同。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “如何阅读别人的代码”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
122
极客时间专栏/geek/许式伟的架构课/软件工程篇/71 | 如何阅读别人的代码?.md
Normal file
122
极客时间专栏/geek/许式伟的架构课/软件工程篇/71 | 如何阅读别人的代码?.md
Normal file
@@ -0,0 +1,122 @@
|
||||
<audio id="audio" title="71 | 如何阅读别人的代码?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/45/9f/45109c2fd7858776abcf2d62ea0bfd9f.mp3"></audio>
|
||||
|
||||
你好,我是七牛云许式伟。今天聊聊如何阅读别人的代码。
|
||||
|
||||
## 为何要读别人的代码?
|
||||
|
||||
我们去阅读别人的代码,通常会带有一定的目的性。完整把一个系统的代码 “读懂” 需要极大的精力。所以明确阅读代码的目标很重要,因为它决定了你最终能够为这事付出多大的精力,或者说成本。
|
||||
|
||||
大体来说,我们可以把目标分为这样几种类型:
|
||||
|
||||
- 我要评估是否引入某个第三方模块;
|
||||
- 我要给某个模块局部修改一个 Bug(可能是因为使用的第三方模块遇到了一个问题,或者可能是你的上级临时指定了一个模块的 Bug 给你);
|
||||
- 我要以某个开源模块为榜样去学习;
|
||||
- 我要接手并长期维护某个模块。
|
||||
|
||||
为什么要把我们的目标搞清楚?
|
||||
|
||||
因为读懂源代码真的很难,它其实是架构的反向过程。它类似于反编译,但是并不是指令级的反编译,而是需要根据指令反推更高维的思想。
|
||||
|
||||
我们知道反编译软件能够将精确软件反编译为汇编,因为这个过程信息是无损的,只是一种等价变换。但是要让反编译软件能够精确还原出高级语言的代码,这就比较难。因为编译过程是有损的,大部分软件实体的名字已经在编译过程中被去除了。当然,大部分编译器在编译时会同时生成符号文件。它主要用于 debug 用途。否则我们在单步跟踪时,debug 软件就没法显示变量的名字。
|
||||
|
||||
即使我们能够拿到符号文件,精确还原出原始的高级语言的代码仍然非常难。它需要带一定的模型推理在里面,通过识别出这里面我们熟悉的 “套路”,然后按照套路进行还原。
|
||||
|
||||
我们可以想像一下,“一个精确还原的智能反编译器” 是怎么工作的。
|
||||
|
||||
第一步,它需要识别出所采用的编程语言和编译器。这通常相对容易,一个非常粗陋的分类器就可以完成。尤其是很多编译器都有 “署名”,也就是在编程出的软件中带上自己签名的习惯。如果假设所有软件都有署名,那么这一步甚至不需要训练与学习。
|
||||
|
||||
第二步,通过软件的二进制,结合可选的符号文件(没有符号文件的结果是很多软件实体,比如类或函数的名字,会是一个随机分配的符号),加上它对该编译器的套路理解,就可以进行反编译了。
|
||||
|
||||
编译器的套路,就如同一个人的行为,持续进行观察学习,是可以形成总结的。这只需要反编译程序持续地学习足够多的该编译器所产生的样本。
|
||||
|
||||
我之所以拿反编译过程来类比,是希望我们能够理解,阅读源代码过程一方面是很难的,另一方面来说,也是需要有产出的。
|
||||
|
||||
有产出的学习过程,才是最好的学习方式。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/66/198b0de04e36ce8caa0d50a0debbb266.png" alt="">
|
||||
|
||||
那么阅读源代码的产出应该是什么?答案是,构建这个程序的思路,也就是架构设计。
|
||||
|
||||
## 理解架构的核心脉络
|
||||
|
||||
怎么做到?
|
||||
|
||||
首先,有文档,一定要先看文档。如果原本就已经有写过架构设计的文档,我们还要坚持自己通过代码一步步去反向进行理解,那就太傻了。
|
||||
|
||||
但是,一定要记住文档和代码很容易发生脱节。所以我们看到的很可能是上一版本的,甚至是最初版本的设计。
|
||||
|
||||
就算已经发生过变化,阅读过时的架构设计思想对我们理解源代码也会有极大的帮助作用。在这个基础上,我们再看源代码,就可以相互进行印证。当然如果发生了冲突,我们需要及时修改文档到与代码一致的版本。
|
||||
|
||||
看源代码,我们首先要做到的是理解系统的概要设计。概要设计的关注点是各个软件实体的业务范畴,以及它们之间的关系。有了这些,我们就能够理解这个系统的架构设计的核心脉络。
|
||||
|
||||
具体来说,看源码的步骤应该是怎样的呢?
|
||||
|
||||
首先,把公开的软件实体(模块、类、函数、常量、全局变量等)的规格整理出来。
|
||||
|
||||
这一步往往有一些现成的工具。例如,对 Go 语言来说,运行 go doc 就可以帮忙整理出一个自动生成的版本。一些开源工具例如 doxygen 也能够做到类似的事情,而且它支持几乎所有的主流语言。
|
||||
|
||||
当然这一步只能让我们找到有哪些软件实体,以及它们的规格是什么样的。但是这些软件实体各自的业务范畴是什么,它们之间有什么关系?需要进一步分析。
|
||||
|
||||
一般来说,下一步我会先看 example、unit test 等。这些属于我们研究对象的客户,也就是使用方。它们能够辅助我们理解各个软件实体的语义。
|
||||
|
||||
通过软件实体的规格、说明文档、example、unit test 等信息,我们根据这些已知信息,甚至包括软件实体的名字本身背后隐含的语义理解,我们可以初步推测出各个软件实体的业务范畴,以及它们之间的关系。
|
||||
|
||||
接下来,我们需要进一步证实或证伪我们的结论。如果证伪了,我们需要重新梳理各个软件实体之间的关系。怎么去证实或证伪?我们选重点的类或函数,通过看它们的源代码来理解其业务流程,以此印证我们的猜测。
|
||||
|
||||
当然,如果你能够找到之前做过这块业务的人,不要犹豫,尽可能找到他们并且争取一个小时左右的交流机会,并提前准备好自己遇到迷惑的问题列表。这会大幅缩短你理解整个系统的过程。
|
||||
|
||||
最后,确保我们正确理解了系统,就需要将结论写下来,形成文档。这样,下一次有其他同学接手这个系统的时候,就不至于需要重新再来一次 “反编译”。
|
||||
|
||||
## 理解业务的实现机制
|
||||
|
||||
业务系统的概要设计、接口理清楚后,通常来说,我们对这个系统就初步有谱了。如果我们是评估第三方模块要不要采纳等相对轻的目标,那么到此基本就可以告一段落了。
|
||||
|
||||
只有在必要的情况下,我们才研究实现机制。刚才我们谈到系统架构梳理过程中,我们也部分涉及了源代码理解。但是,需要明确的是,前面我们研究部分核心代码的实现,其目的还是为了确认我们对业务划分猜测的正确性,而不是为了实现机制本身。
|
||||
|
||||
研究实现是非常费时的,毕竟系统的 UserStory 数量上就有很多。把一个个 UserStory 的具体业务流程都研究清楚写下来,是非常耗时的。如果这个业务系统不是我们接下来重点投入的方向,就没必要在这方面去过度投入。
|
||||
|
||||
这时候目标就很重要。
|
||||
|
||||
如果我们只是顺带解决一下遇到的 Bug,无论是用第三方代码遇到的,还是上级随手安排的临时任务,我们自然把关注点放在要解决的 Bug 本身相关的业务流程上。
|
||||
|
||||
如果我们是接手一个新的业务系统,我们也没有精力立刻把所有细节都搞清楚。这时候我们需要梳理的是关键业务流程。
|
||||
|
||||
怎么搞清楚业务流程?
|
||||
|
||||
>
|
||||
程序 = 数据结构 + 算法
|
||||
|
||||
|
||||
还是这个基础的公式。要搞清楚业务流程,接下来要做的事情是,把这些业务流程相关的数据结构先理清楚。
|
||||
|
||||
数据结构是容易梳理的,类的成员变量、数据库的表结构,通常都有快速提取的方式。除了 MongoDB 可能会难一些,因为弱 schema 的原因,我们需要通过阅读代码的方式去理解 schema。更麻烦的是,我们不确定历史上经历过多少轮的 schema 变更,这通过最新版本的源代码很可能看不出来。一个不小心,我们就可能会处理到非预期 schema 的数据。
|
||||
|
||||
理清楚数据结构,事情就解决了大半。
|
||||
|
||||
剩下来就是理各个 UserStory 的业务流程,并给这些业务流程画出它的 UML 时序图。这个过程随时可以补充。所以我们挑选对我们当前工作最为相关的来做就好了。
|
||||
|
||||
最后,还是同样地,我们要及时把我们整理的结论写下来,变成架构文档的一部分。这样随着越来越多人去补充完整架构设计文档,才有可能把我们的项目从混沌状态解脱出来。
|
||||
|
||||
## 结语
|
||||
|
||||
对于任何一个项目团队来说,阅读代码的能力都极其重要。哪怕你觉得你的团队共识管理很好,团队很默契,大家的工程习惯也很好,也都很乐意写文档,但这些都替代不了阅读代码这个基础活动。
|
||||
|
||||
阅读代码是不可或缺的能力。
|
||||
|
||||
为什么这么说?因为:代码即文档,代码是理解一致性更强的文档。
|
||||
|
||||
另外,作为一个小补充,我们需要指出的一点是:阅读代码的结果,有时不一定仅仅是架构设计文档的补充与完善。我们有时也会顺手修改几行代码。
|
||||
|
||||
这是正常现象,而且应该被鼓励。为什么鼓励改代码?是因为我们鼓励随时随地消除臭味。改几行明显风格不太好的代码,是非常好的一件事情。
|
||||
|
||||
但是我们也要有原则。
|
||||
|
||||
其一,不做大的改动,比如限定单个函数内的改动不能超过 10 行。
|
||||
|
||||
其二,确保改动前后的语义完全一致。这种一致需要包括所有 corner case 上的语义一致,例如错误码,条件语句的边界等。
|
||||
|
||||
其三,不管多自信,有改动就需要补全相关的单元测试,确保修改代码的条件边界都被覆盖。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “发布单元与版本管理”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
150
极客时间专栏/geek/许式伟的架构课/软件工程篇/72 | 发布单元与版本管理.md
Normal file
150
极客时间专栏/geek/许式伟的架构课/软件工程篇/72 | 发布单元与版本管理.md
Normal file
@@ -0,0 +1,150 @@
|
||||
<audio id="audio" title="72 | 发布单元与版本管理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d2/43/d23dc65e7e245eayya31770350ba6743.mp3"></audio>
|
||||
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
前面我们在 “[68 | 软件工程的宏观视角](https://time.geekbang.org/column/article/182924)” 一讲中谈到:一个软件工程往往是生命周期以数年甚至数十年计的工程。对于传统工程,我们往往把一个工程同时也称之为项目,项目工程。但软件工程不同,虽然我们平常也有项目的概念,但软件工程并不是一个项目,而是无数个项目。每个项目只是软件工程中的一个里程碑(Milestone)。
|
||||
|
||||
这意味着软件工程终其完整的生命周期中,是在反复迭代与演进的。这种反复迭代演进的工程,要保证其质量实际上相当困难。
|
||||
|
||||
## 源代码版本管理
|
||||
|
||||
怎么确保软件工程的质量?
|
||||
|
||||
很容易想到的一个思路是,万一出问题了,就召回,换用老版本。
|
||||
|
||||
这便是版本管理的来由。当然,如果仅仅只是为了召回,只需要对软件的可执行程序进行版本管理就好了。但我们如果要进一步定位软件质量问题的原因,那就需要找到一个方法能够稳定再现它。
|
||||
|
||||
这意味着我们需要对软件的源代码也进行版本管理,并且它的版本与可执行程序的版本保持一一对应。
|
||||
|
||||
但实际上这事并没有那么简单。
|
||||
|
||||
从软件的架构设计可知,软件是分模块开发的,不同模块可能由不同团队开发,甚至有些模块是外部第三方团队开发。这意味着,从细粒度的视角来看,一个软件工程的生命周期中,包含着很多个彼此完全独立的子软件工程。这些子软件工程它们有自己独立的迭代周期,我们软件只是它们的 “客户”。
|
||||
|
||||
这种拥有独立的迭代周期的软件实体,我们称之为 “发布单元”。你可能直觉认为它就是模块,但是实际上两者有很大的不同。
|
||||
|
||||
对于一个发布单元,我们直观的一个感受是它有自己独立的源代码仓库(repo)。
|
||||
|
||||
发布单元的输出不一定是可执行程序,它有如下可能:
|
||||
|
||||
- 可执行程序,或某种虚拟机的字节码程序;
|
||||
- 动态库(so/dylib/dll);
|
||||
- 某种虚拟机自己定义的动态库,比如 JVM 平台下的 jar 包;
|
||||
- 静态库(.a 文件),它通常实际上是可执行程序的半成品,比较严谨来说的编译过程是先把每个模块编译成半成品,然后由链接器把各个模块组装成成品;
|
||||
- 源代码本身,一些语言的价值主张是源代码发布,比如 Go 语言。
|
||||
|
||||
发布单元的输入,常规理解主要包含以下两部分的内容:
|
||||
|
||||
- 若干自己独立演进的模块,也就是源代码仓库(repo)托管的代码;
|
||||
- 自己依赖的发布单元列表,这些外部的发布单元有自己独立的迭代周期。
|
||||
|
||||
源代码仓库管理系统,比如 svn、git 等等,一般只能管到第一部分。它让我们对自己独立演进的代码可以有很好的质量跟踪。
|
||||
|
||||
我们以 github 为例,它提供了以下源代码质量的管理手段。
|
||||
|
||||
其一,团队成员开发活动的独立性。每个人可以极低成本地建立一个开发分支(branch),一个开发分支做一个功能(feature),这个工作没有完成时,他的工作对所有其他人不可见,所以团队成员有很好的并行开发的能力,彼此完全独立。
|
||||
|
||||
其二,完善的代码质量检查机制。当一个团队成员完成他某项功能(feature)开发时,他可以提交一个功能合并请求(pull request),以求将代码合并进主代码库。但在此之前,我们需要对这项新功能的代码质量进行检查。常见的手段如下:
|
||||
|
||||
- 自动化运行单元测试案例(unit test);
|
||||
- 单元测试覆盖率检查(code coverage);
|
||||
- 静态代码质量检查(lint);
|
||||
- 人工的代码互审(code review);
|
||||
- ……
|
||||
|
||||
代码质量检查过程,需求显然比较易变。所以在这里 github 做了开放设计。我们再一次感受到了开闭原则的威力。
|
||||
|
||||
其三,完善的回滚机制(revert)。在代码已经合并到主代码库后,如果我们突然发现它有 Bug,这时候并不是落子无悔,而是可以自己对某次有 Bug 的 pull request 做回滚(revert),这样主干就可以得到去除了该功能后的一个新的发行版本。
|
||||
|
||||
对于第二部分,也就是发布单元的外部依赖管理,通常不同语言有自己的惯例。例如,Go 语言早期并没有官方的版本管理手段,所以导致有很多社区版本的实现方案。直到最新的 go mod 机制终于统一了这一纷争。
|
||||
|
||||
从基本原理来说,所有外部依赖管理无非要达到这样一个目标:指定我这个发布单元依赖的各个模块(嗯,这是通俗说法,其实是指依赖的发布单元)的建议版本是什么。
|
||||
|
||||
这样,我们理论上就可以稳定持续地通过源代码构建出相同能力的输出结果。
|
||||
|
||||
注意,这里有一个前提假设,是要求所有人都自觉遵循的:一个打好了版本号的发布单元是只读的,我们不能对其做任何改动。这句话的意思包括:
|
||||
|
||||
其一,我们不能修改发布单元自身包含的各个模块的的代码。这很容易理解,我们不展开。
|
||||
|
||||
其二,我们不能修改发布单元依赖的外部模块(同样地,其实指依赖的发布单元)的版本。比如我们依赖 opencv,把依赖的版本号从 v1.0 升级到 v2.0,这是不行的,这也是一次变更,需要修改我们的版本号。
|
||||
|
||||
如果有人破坏了版本的只读语义,就会导致所有依赖它的发布单元的版本只读语义也被破坏。这是我们需要极力去避免发生的事情。
|
||||
|
||||
从严谨意义来说,仅保证发布单元自身的源代码和依赖的外部模块只读,仍然不足以保证输出结果的确定性。为什么这么说,因为还有两个东西没有做到只读:
|
||||
|
||||
其一,操作系统内核。不同版本的操作系统内核行为不完全一致,它的一些动态库可能行为不完全一致,这些都可能会导致我们的软件行为有所不同。
|
||||
|
||||
其二,编译器。不同版本的编译器同样存在理论上与编译的结果行为上不一样的可能。
|
||||
|
||||
为什么没有把它们纳入到源代码版本管理的范畴管起来?这当然是因为操作系统和编译器大部分情况下质量是有所保证的,所以当软件在不同版本的操作系统下行为不一致时,这会被看做软件 Bug 记录下来,而不是修改操作系统。
|
||||
|
||||
## 软件发布的版本管理
|
||||
|
||||
但并不是在所有时刻,我们都能够相信操作系统和编译器。从源代码版本管理的角度,它的好处是软件构建(build)过程是一个相对封闭可预期的环境,这个环境我们甚至直接规定操作系统的种类和版本、编译器的版本,系统预装哪些软件等等。
|
||||
|
||||
但是软件发布过程却并非如此。
|
||||
|
||||
我们大家可能都接触过各种软件发布的管理工具,比如apt、rpm、brew 等等。在这些管理工具的使用过程中,我们每个人或多或少都有过不少 “失败教训”。并不是每一次软件安装过程都能够如愿。
|
||||
|
||||
这些软件发布的管理工具,背后有不少实际上基于的就是源代码的版本管理。但是为什么这个时候它会不 work 呢?因为用户之间系统环境的差异太大了。让每个软件的发布者都能够想到多样化的环境并加以适配,这是非常高的要求。
|
||||
|
||||
所以,软件安装有时会不成功,实在是在所难免。
|
||||
|
||||
怎么才能彻底解决这个问题?
|
||||
|
||||
答案是,容器化。
|
||||
|
||||
容器的镜像(image),不只是包含了软件发布的可执行程序本身,也完整包含了运行它的所有环境,包括依赖的动态库和运行时,甚至包括了它依赖的 “操作系统”。这意味着容器的镜像(image)的版本管理,比之源代码的版本管理更进一步,实现完完全全的自描述,不再依赖任何外部环境。
|
||||
|
||||
这给我们线上服务的版本管理带来了巨大的便捷性。新版本的服务有缺陷 ?回滚到老版本即可。
|
||||
|
||||
## 只读设计的确定性
|
||||
|
||||
版本的只读设计,带来巨大的收益,这是因为版本是一个 “基线”,对于这个基线,我们心理上对它的预期是确定性的。这种确定性非常重要。
|
||||
|
||||
在 “[68 | 软件工程的宏观视角](https://time.geekbang.org/column/article/182924)” 一讲中我们提到:
|
||||
|
||||
>
|
||||
软件项目的管理期望达到确定性。但软件工程本身是快速变化的,是不确定的。这就是软件工程本身的矛盾。我们的目标是在大量的不确定性中找到确定性,这其实就是软件工程最核心的点。
|
||||
|
||||
|
||||
只读设计提升了软件工程的确定性,所以只读思想被广泛运用。前面我们说开闭原则背后的架构治理哲学,也是模块,或者说软件实体,其业务范畴只读。在业务只读,接口稳定的预期下,模块与模块之间就可以自由组合,构建越来越复杂的系统。
|
||||
|
||||
往小里说,我们开发的时候,有时候会倾向于变量只读,以提高内心对确定性的预期。我并没有去用严谨的方式实证过变量只读的收益究竟有多大,但它的确成为了很重要的一种编程流派,即函数式编程。
|
||||
|
||||
函数式编程从编程范式来说比较小众,但是其只读思想被广泛借鉴。
|
||||
|
||||
这里面最典型的就是大数据领域的 Spark。Spark 的核心是建立在统一的抽象弹性分布式数据集(Resiliennt Distributed Datasets,RDD)之上。
|
||||
|
||||
而 RDD 的核心思想正是只读。对一个只读的 RDD 施加一个变换(transform),即得到另一个 RDD,这不就是函数式编程么?但这种只读设计,让我们的分布式运算在重试、延迟计算、缓存等过程都变得极其简单。
|
||||
|
||||
## 版本的兼容问题
|
||||
|
||||
版本管理的最后一个问题是兼容性。让一个模块依赖另一个模块(严谨来说是发布单元)的特定版本,这解决了版本的确定性问题。
|
||||
|
||||
但是,在某个特定的时刻,我们总是会希望将依赖的模块升级到新版本。无论是基于我们需要使用该模块的新功能,又或者是为了修复的 Bug,或者纯粹是心理上想要更好的东西。
|
||||
|
||||
更换到新版本多多少少冒了一些风险。这里面最大风险是所依赖的模块完成了一次重构。
|
||||
|
||||
为什么依赖模块的重构会给我们的系统带来未知风险?这其中的原因就在于版本兼容的难度。
|
||||
|
||||
兼容一个模块的主体功能并不复杂,既然我们重构了,这部分肯定是得到了解决。但兼容的难度全在细节上。错误码、低频的分支行为等等,这些都需要兼容。
|
||||
|
||||
如果这种分支兼容太麻烦,我们干脆就放弃兼容,连软件实体(如函数)的名字都改了。这倒是干脆,客户升级版本后一看,编译不过了,老老实实用新的接口进行重写,重新测试。
|
||||
|
||||
但有时候我们无法放弃兼容。这发生在我们在做一个互联网服务时。一旦我们发布了一个 api,它就很难收回,因为使用这个 api 的客户端可能有很多。如果我们放弃这个 api 就意味着我们放弃了很多用户,这是不可接受的。
|
||||
|
||||
为了应对这个问题,比较常见的做法是为所有 api 引入版本号,如 “/v2/foo/bar”。当我们对 api 发生不兼容的修改时,就升级版本号,比如 “/v3/foo/bar”。
|
||||
|
||||
这样做有一个额外的好处。如果我们对某个复杂模块进行了全局重构,并且兼容老版本的行为细节非常困难时,我们可以直接升级所有 api 的版本号。这样在线上我们可以保留两个版本的服务同时存在。这通过前面放 nginx 作为 api 分派的网关来做到。
|
||||
|
||||
这样两个版本服务并行,就不需要重构时做太细节的行为兼容。但应当注意,这也是不得已的办法,如果能够兼容,还是鼓励尽可能去兼容。毕竟客户端在升级版本之后,不兼容的地方越多,修改的心智负担就越大。
|
||||
|
||||
## 结语
|
||||
|
||||
今天我们聊的是怎么做版本管理。一个复杂的软件,总可以被分割为若干个独立迭代的发布单元,以便分而治之。发布单元的切割不宜过细,应该以一个小团队负责起来比较舒服为宜,不太小但也不太大。
|
||||
|
||||
版本的只读设计提高了系统的确定性预期,这是非常非常好的收益。但我们也应注意版本兼容上带来的坑。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “软件质量管理:单元测试、持续构建与发布”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
109
极客时间专栏/geek/许式伟的架构课/软件工程篇/73 | 软件质量管理:单元测试、持续构建与发布.md
Normal file
109
极客时间专栏/geek/许式伟的架构课/软件工程篇/73 | 软件质量管理:单元测试、持续构建与发布.md
Normal file
@@ -0,0 +1,109 @@
|
||||
<audio id="audio" title="73 | 软件质量管理:单元测试、持续构建与发布" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e6/b1/e6d5c548d6bb69962e5960e123e037b1.mp3"></audio>
|
||||
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
上一讲 “[72 | 发布单元与版本管理](https://time.geekbang.org/column/article/187641)” 我们聊了版本管理中,只读思想给软件工程带来的确定性价值,它在软件工程质量管理中也是很核心的一点。
|
||||
|
||||
## 软件质量管理
|
||||
|
||||
今天我们聊聊软件工程中,我们在质量管理上其他方面的一些思考。事实上,软件质量管理横跨了整个软件工程完整的生命周期。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b8/0e/b86b9e0e6c9185e6993e7cc90175980e.png" alt="">
|
||||
|
||||
软件工程与传统工程非常不同。它快速变化,充满不确定性。不仅如此,一个软件工程往往是生命周期以数年甚至数十年计的工程。对于传统工程,我们往往把一个工程同时也称之为项目,项目工程。但软件工程不同,虽然我们平常也有项目的概念,但软件工程并不是一个项目,而是无数个项目。每个项目只是软件工程中的一个里程碑(Milestone)。
|
||||
|
||||
这些都决定了软件工程质量管理的思想与传统工程截然不同。在传统工程中,设计的工作往往占比极少,重复性的工作占据其生命周期的绝大部分时间。所以传统工程有极大的确定性。检查清单(Check List)很可能就已经可以很好地实现其工程质量的管理。
|
||||
|
||||
但对于软件工程来说,设计工作在整个工程中持续发生。哪怕是非设计工作,比如编码实现,也仍然依赖个体的创造力,同样存在较强的不确定性。显然,检查清单(Check List)完全无法满足软件工程的质量管理需要。
|
||||
|
||||
那么,到底应该怎么管理软件工程的质量?每次谈软件工程质量保障的时候,我总会先画下面这张图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/78/e0/78599a7460714c080c8324d83a827fe0.png" alt="">
|
||||
|
||||
它谈的是软件的生命周期,或者也可以理解为软件中某项功能的生命周期。我们把软件或软件的某项功能生命周期分成两个大的阶段,一个阶段是开发期,一个阶段是维护期。开发期与维护期是相对而言的,只是在表征上,开发期有更强的设计属性。维护期虽然也持续会有设计工作,但是工作量会小一个数量级以上。
|
||||
|
||||
为什么划分出开发期与维护期是重要的?
|
||||
|
||||
因为开发期的时间跨度虽然可能不长,但是它的影响太大了,基本决定了后期维护期的成本有多高。
|
||||
|
||||
这也意味着软件工程是需要有极强预见性的工程。我们在开发期恰如其分地多投入一分精力,后面在维护期就有十倍甚至百倍以上的回报。
|
||||
|
||||
设计工作的质量至关重要。但是它执行上又不太有复制性,可复制的只是设计范式和设计思维。
|
||||
|
||||
我们只能在这种执行的不确定性中找工程上的确定性。
|
||||
|
||||
如何做到?
|
||||
|
||||
## 单元测试
|
||||
|
||||
首先,做好自动化测试。自动化测试对软件工程的重要性是不言而喻的。如果是一项一次性的工程,我们可以基于常规的手工测试。但常规测试的缺点在于:
|
||||
|
||||
其一,一般常规测试是基于手工的,不具备可回归性。因此,常规测试的效率不高,一次完整的测试集跑下来可能需要几天甚至一周之久。
|
||||
|
||||
其二,易于缺乏效率,所以往往为了赶工会导致测试仅仅针对典型数据,测试的覆盖率往往也很低。
|
||||
|
||||
软件工程的生命周期往往几年甚至几十年之久,我们必然关注单次测试的效率。所以自动化测试的核心价值就在于可回归性与提高测试的覆盖率。
|
||||
|
||||
自动化测试与常规测试相比,风格上有很明显的不一样,它有如下重要特征。
|
||||
|
||||
- 自动化、可回归性。
|
||||
- 静默(Quiet)。没有发生错误的时候,就不说话。
|
||||
- 案例执行的安全受控。某个案例执行的失败,不会影响其他案例的正常运行。
|
||||
|
||||
从分类来说,一般自动化测试我们分两个层次:一个是模块级的单元测试,一个是系统级的集成测试。
|
||||
|
||||
无论从什么角度来看,模块的单元测试都是重中之重的大事。原因是,单元测试的成本是最低的。
|
||||
|
||||
关于测试成本,我们可以从两个维度看。
|
||||
|
||||
其一,单元测试的实施成本低,最容易去做。不少高级语言比如 Go 语言甚至在语言内建的工具链上就直接支持。而集成测试虽然也有自动化的方法和支持工具,但是往往需要更高额的代价。
|
||||
|
||||
其二,减少问题发现的周期,进而降低问题的修复成本。单元测试将问题发现周期缩短,基本上在问题现场就发现问题,这降低了Bug的修复成本。如果问题在系统的集成测试阶段发现,那么从问题定位,到回忆当初实现这段代码时候的思路,到最终去解决掉它,必然需要多花费几倍甚至几十倍的时间。
|
||||
|
||||
因此,我们鼓励更严格的单元测试要求,更高的单元测试覆盖率,以尽可能把发现问题做到前头。
|
||||
|
||||
但仍然有不少公司在推广单元测试上遇到了不小的麻烦,推不起来。
|
||||
|
||||
对于这一点,我们认为首先要改变的是对推广单元测试这件事情的认知。我们不把推广单元测试看作是让大家去多做一件额外的事情,而是规范大家做单元测试的方法。
|
||||
|
||||
为什么这么说?因为实际上单元测试大家都会去做,很少有人会不经验证就直接交付。但是验证方式上可能有各种 “土” 方法,比如用 print,用可视化的界面做输入测试,用调试工具做单步跟踪等等。
|
||||
|
||||
但是这些方法代价其实一样不低,但是却不可回归,正确与否还需要人脑临时去判断。
|
||||
|
||||
更重要的是,这些方法最大的问题是没有办法去固化已知的 Bug,最大程度保留下来我们的测试案例。
|
||||
|
||||
这其实才是最核心的一个认知问题:我们应当重视我们的测试代码,它同样也是我们的开发成果,理应获得和模块的功能代码同等重要的地位,理应被保留下来。
|
||||
|
||||
解决了这个认知上的共识问题,自动化测试就能够被很好地推动起来。当前这方面的工具链已经非常完善,不至于会在工具上遇到太大的障碍。
|
||||
|
||||
## 持续构建,持续发布
|
||||
|
||||
其次,我们降低软件工程不确定性的方法是:持续构建,持续发布。
|
||||
|
||||
我们鼓励更小的发布。我们鼓励更短的发布周期,更高的发布频率。这能够让发布的负担降低到最低。
|
||||
|
||||
这种极度高频交付的机制与传统工程的质量管理机制迥异。但是它被证明是应对软件工程不确定性的最佳方式。为什么会这样?
|
||||
|
||||
其一,交付的功能越少,因为错误而发生回滚的代价越低,影响面越小。如果我们同时发布了数十个功能,却因为某一个功能不达标而影响整体交付,这其实是降低了软件的功能交付效率。更好的方式显然是把这个出问题的功能回滚,把其他所有功能都放行。
|
||||
|
||||
其二,交付频率越高,我们对交付过程的训练越频繁,过程的熟练度越高,执行效率也越高。当交付成为一个自然习惯后,我们会把交付看作功能开发的一部分,而不是以前大家对研发的理解,认为做完功能就完事,后续上不上线与我无关。我们会鼓励更多把研发的绩效与功能线上的表现关联起来,面向客户价值,而非仅仅面向功能开发。
|
||||
|
||||
当然这种极度高频交付的机制,意味着它对软件工程的系统化建设有更高的要求。
|
||||
|
||||
当然,除了日构建与发布平台外,我们也需要在其中加入各种质量管理的抓手。比如:
|
||||
|
||||
- 自动化运行单元测试案例(unit test);
|
||||
- 单元测试覆盖率检查(code coverage);
|
||||
- 静态代码质量检查(lint);
|
||||
- 人工的代码互审(code review);
|
||||
- 灰度发布(gray release);
|
||||
- A/B 测试(A/B testing);
|
||||
- ……
|
||||
|
||||
## 结语
|
||||
|
||||
今天我们更加完整地探讨了软件工程的质量管理。整体来说,软件工程与传统工程在质量管理上的理念是迥异的,甚至往往是反其道而行之的。究其原因,还是因为软件工程的核心在于如何在高度的不确定性中找到确定性。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “开源、云服务与外包管理”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
149
极客时间专栏/geek/许式伟的架构课/软件工程篇/74 | 开源、云服务与外包管理.md
Normal file
149
极客时间专栏/geek/许式伟的架构课/软件工程篇/74 | 开源、云服务与外包管理.md
Normal file
@@ -0,0 +1,149 @@
|
||||
<audio id="audio" title="74 | 开源、云服务与外包管理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/66/8b/66d354ccda6dyy536b7cfc32e5fbc68b.mp3"></audio>
|
||||
|
||||
你好,我是七牛云许式伟。今天我们聊的话题是有关于分工的。
|
||||
|
||||
在这一讲之前,我们涉及到分工这个话题,基本上都局限于企业内部,且大多数情况下主要在同一个团队内部。但今天我们聊的是更大的分工:跨组织的分工与协作。
|
||||
|
||||
## 外包及其理想模型
|
||||
|
||||
在软件工程中,我们第一个接触的外部分工毫无疑问是外包。所谓外包,就是将我们软件的全部或部分模块的实现职能交给外部团队来做。
|
||||
|
||||
但是,软件工程项目的外包实际上成功率非常低。这背后有其必然性,它主要表现在以下这些方面。
|
||||
|
||||
其一,任务表达的模糊,双方容易扯皮。期望需求方能够把需求边界说清楚,把产品原型画清楚,把业务流程讲清楚,这非常难。有这样专业的需求表达能力的,通常软件工程水平不低,遇到这样的需求方,绝对应该谢天谢地。这种专业型的甲方,它大部分情况下只发生在项目交付型外包,而非产品功能外包。更多的产品外包,一般是甲方不太懂技术,需要有团队替自己把事情干了,他好拿着产品去运营。
|
||||
|
||||
其二,交付的代码质量低下,长期维护的代价高。软件工程不是项目,它都需要长长久久地运行下去。但是接包方的选择相当重要。因为接包方的质量相当参差不齐,遇上搬砖的概率远高于设计能力优良的团队。事实上,有良好设计能力的团队,多数情况下也不甘于长期做外包。
|
||||
|
||||
其三,项目交接困难,知识传承效率很低。软件工程并非普通的工程,就算交付的结果理想,项目交接也非常困难。所以外包项目第一期结束后,如果运营得好,往往项目还继续会有第二期、第三期。这里的原因是你只能找同一拨人做,如果换一波人接着做,考虑到知识传承效率低下,往往需要很长的一个交接周期。
|
||||
|
||||
那么,外包的理想模型是什么?
|
||||
|
||||
上面我们已经说到,外包在通常情况下,专业的甲方需要说清楚需求,这样双方就没有分歧。但是,更好的做法其实不是外包需求,而是外包实现。
|
||||
|
||||
也就是说,作为专业的甲方,我自己做好需求分析,做好系统的概要设计。进一步,我们把每个模块的业务范畴与接口细化下来。我们以此作为外包边界。假设分了N个模块,我们可以把它们平均分给若干个接包方。
|
||||
|
||||
这种方式的外包,甲方相当于只留了架构师团队,实现完全交给了别人。但是它与普通的外包完全不同,因为根本不担心知识传承的问题。每个模块的接包方对甲方来说就真的只是干活的。
|
||||
|
||||
接包方拿到的是模块的规格说明书。他要做的是模块的详细设计的实现部分,其中最为核心的是数据结构设计。对于服务端,甲方可以规定所采用的数据库是什么,但是把表结构的设计交出去。
|
||||
|
||||
进一步,如果模块的外包说明书中还规定了单元测试的案例需要包含哪些,那么这个模块发生设计偏离的可能性就很低。
|
||||
|
||||
外包的验收需要包含模块的实现设计文档,里面描述了数据结构+算法。另外,单元测试部分,每个测试场景,也填上对应的测试函数的名称。
|
||||
|
||||
实际会有人这样去外包么?
|
||||
|
||||
我不确定。但我们可以把它看作一种分工的假想实验。这个假想实验可以充分说明架构师团队的重要性。有了一个好的架构师团队,他们设计合适的系统架构,对每个模块的规格都做了相应的定义,他们验收模块的实现。
|
||||
|
||||
这样,项目就可以有条不紊地展开。甚至,研发进度可以自如控制。嫌项目进展太慢?找一倍的接包方,就可以让工程加速一倍。
|
||||
|
||||
所以,这个外包假想实验也说明了一点:我们的平常项目之所以进度无法达到预期,无他,团队缺乏优秀的架构师而已。
|
||||
|
||||
让我们把软件工程看作一门科学。我们以[工程师思维](https://time.geekbang.org/column/article/148208)的严谨态度来看它。我们减少项目中的随意性,把架构设计的核心,模块规格,也就是接口,牢牢把控住。这样,项目的执行风险就完全消除了。
|
||||
|
||||
哦不,还有一个最大的执行风险没有消除。我怎么证明这个系统架构的分解是对的?不会出现每个模块做好了,但是最终却拼不起来?
|
||||
|
||||
我们前面在 “[架构:系统的概要设计](https://time.geekbang.org/column/article/117783)” 这一讲中实际上已经谈过这事的解决方法:系统设计的产出要有源代码,它是项目的原型。关键模块有 mock 的实现,业务系统的关键 UserStory 都串了一遍,确保系统设计的正确性。
|
||||
|
||||
这个假想实验是有趣的,它可以让你想明白很多事情。甚至可以把它看作理解这个专栏的架构思维核心思想的钥匙。
|
||||
|
||||
我希望,它不只是一个假想实验。
|
||||
|
||||
## 开源与众包
|
||||
|
||||
我们把话题拉回到跨组织的分工。
|
||||
|
||||
除了传统的外包外,在软件工程中出现的第二类外包是众包,它以开源这样一个形态出现。
|
||||
|
||||
从分工角度,开源的核心思想是让全社会的程序员共同来完成一个业务系统。
|
||||
|
||||
开源的优势非常明显。对于一个热门的开源项目,它的迭代进度是非常惊人的,因为它撬动的资源太大了。
|
||||
|
||||
但不是开源了就能够获得这样的好处。
|
||||
|
||||
虽然成功的开源项目风风火火,但是我们也应该意识到,对于那些并没有得到关注的开源项目,它们的迭代速度完全无法保障。最终,你可能还是只能靠自己的团队来完成它的演进。
|
||||
|
||||
从这个意义上看,开源是一种商业选择。你得持续经营它。没有经营的开源项目不会成功。你需要宣传它,你自己也得持续迭代它,你还要为它拉客户。有客户的开源项目自然就有了生命力。
|
||||
|
||||
另外,开源这种形态,注定了它只能做大众市场。如果一个业务系统它的受众很少,就比较难通过开源获得足够的外部支持。
|
||||
|
||||
所以绝大部分成功的开源项目,都属于基础设施性质的业务系统,有极其广泛适用的场景。例如,语言、操作系统、基础库、编程框架、浏览器、应用网关、各类中间件等等。我们这个架构课重点介绍的内容,大部分都有相应的开源实现。
|
||||
|
||||
开源对信息科技的影响极其巨大,它极大地加速了信息科技前进的进程,是全球共同精诚协作的典范。
|
||||
|
||||
没有参与过开源的程序员是需要心有遗憾的。开源沉淀下来的协同方法与工作流,今天被无数公司所借鉴。
|
||||
|
||||
没有开源,我们无法想象这件事情:那么多形形色色的企业,今天其中绝大部分,它们的软件工程协同方法与业务流竟然如此相似。
|
||||
|
||||
这是开源带来的另一种无形资产。
|
||||
|
||||
如果大家没有忘记的话,可能能够回忆起来,在谈完软件工程的宏观视角之后,我首先聊的是 “[团队的共识管理](https://time.geekbang.org/column/article/183900)”。为什么这很重要?因为它是团队协作效率的最大基础。如果连对协作的工作流都没有共识,那团队真的是一盘散沙了。
|
||||
|
||||
今天我们几乎不会遇到工作方式上的问题,不是别的原因,是开源给予我们的礼物。它让全球的程序员、全球的科技企业,都养成了一模一样的工程习惯。
|
||||
|
||||
## 云计算与服务外包
|
||||
|
||||
云服务是新的跨组织分工的形态。无论是传统的外包,还是开源的众包,它们都属于源代码外包。这类外包的共同特点是,它们不对结果负责。
|
||||
|
||||
对于传统外包,项目验收结束,双方一手交钱一手交货,至于用得好不好,那是甲方自己的事情。
|
||||
|
||||
对于开源软件来说,那更是完全免责,你爱用不用,用了有什么问题责任自负。当然有很多公司会购买开源软件的商业支持,这不难理解,除了有人能够帮助我一起完成项目上线外,最重要的是要有人能够给我分担出问题的责任。
|
||||
|
||||
互联网为跨组织协同带来了新的机会。我可以24小时为另一个组织服务,而无需跑到对方的办公室,和他们团队物理上处在一起。
|
||||
|
||||
这就是云计算。云计算从跨组织协同的角度来看,不过是一种新的交付方式。我们不再是源代码交付,而是服务交付。所以,你也可以把云计算看着一种外包,我们称之为服务外包。
|
||||
|
||||
大部分的基础设施,都可以以服务外包的方式进行交付。这中间释放的生产力是惊人的。
|
||||
|
||||
一方面,云计算与传统外包不同,它对结果负责,有服务 SLA 承诺。一旦出问题,问题也可以由云服务提供方自己解决,而无需业务方介入,这极大降低了双方的耦合,大家各司其职。
|
||||
|
||||
另一方面,它简化了业务方的业务系统,让它得以能专注自己真正的核心竞争力的构建。
|
||||
|
||||
站在生产效率角度看,不难理解为什么我们会坚信云服务是未来必然的方向。
|
||||
|
||||
## 外包方式的选择
|
||||
|
||||
任何企业都存在于社会生态之中,我们无法避开组织外部的分工协同问题。
|
||||
|
||||
怎么选择跨组织的协同方式?
|
||||
|
||||
在七牛,自成立以来我们就一直有一句话谈我们对跨组织协同的看法:
|
||||
|
||||
>
|
||||
我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。
|
||||
|
||||
|
||||
>
|
||||
在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。
|
||||
|
||||
|
||||
这句话有它一定的道理,但也有它模糊的地方。
|
||||
|
||||
首先是关于 “核心竞争力相关”。我们并没有太清晰地去定义什么样的东西是我们核心竞争力相关,什么不相关。
|
||||
|
||||
一些程序员对此理解可能会比较 “技术化”,认为业务系统的核心模块就是核心竞争力。与它相关的东西就是核心竞争力相关。
|
||||
|
||||
但更合理的视角不是技术视角,而是业务视角。我们每一家企业都是因为服务客户而存在。所以,与服务客户的业务流越相关,越不能外包,而是要自己迭代优化,建立服务质量与效率的竞争优势。
|
||||
|
||||
另外,外包的选择需要非常谨慎。很多开发人员都有随意引用开源项目的习惯,这一定程度上给项目带来了不确定的风险。
|
||||
|
||||
我一直认为,开源项目的引入需要严格把关。严谨来说,开源项目引入大部分情况下是属于我说的 “基础架构” 选择的范畴,这同样是架构师团队需要承担的重要职责,一定要有正规的评估流程。
|
||||
|
||||
## 结语
|
||||
|
||||
今天我们聊的话题是跨组织的分工与协同。在形态上,我们可以分为:传统外包、开源与云服务。当然还有就是我们今天没有讨论的使用外部商业软件。
|
||||
|
||||
从形态来说,商业软件很接近传统外包,但是从它的边界来说,因为商业软件往往有明确的业务边界,所以在品质上会远高于外包。当然定制过于严重的商业软件例外,它在某种程度上来说退化为了传统外包。
|
||||
|
||||
在外包方式的选择上,我们的建议是:
|
||||
|
||||
>
|
||||
我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。
|
||||
|
||||
|
||||
>
|
||||
在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。
|
||||
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “软件版本迭代的规划”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
171
极客时间专栏/geek/许式伟的架构课/软件工程篇/75 | 软件版本迭代的规划.md
Normal file
171
极客时间专栏/geek/许式伟的架构课/软件工程篇/75 | 软件版本迭代的规划.md
Normal file
@@ -0,0 +1,171 @@
|
||||
<audio id="audio" title="75 | 软件版本迭代的规划" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/65/2e/65d6b73ef4a54fc320f5aedfb137e22e.mp3"></audio>
|
||||
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
到今天为止,我们专栏的话题主要集中在软件工程的质量与效率上。我们在专栏的开篇中就已经明确:
|
||||
|
||||
>
|
||||
从根本目标来说,软件架构师要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。
|
||||
|
||||
|
||||
但是今天,我们将探讨一个更高维的话题:软件版本迭代的规划。后续我们简称为 “版本规划”。简单说,就是下一步的重点应该放在哪里,到底哪些东西应该先做,哪些东西应该放到后面做。
|
||||
|
||||
这是一个极其关键的话题。它可以影响到一个业务的成败,一个企业的生死存亡。方向正确,并不代表能够走到最后,执行路径和方向同等重要。
|
||||
|
||||
那么,版本规划的套路是什么?
|
||||
|
||||
探讨这个问题前,我想先看一个实际的案例。这个案例大家很熟悉:Go 语言的版本迭代。
|
||||
|
||||
我们从 Go 语言的演进,一起来看看 Go 团队是如何做软件版本迭代规划的。这有点长,但是细致地琢磨对我们理解版本规划背后的逻辑是极其有益的。
|
||||
|
||||
## Go 版本的演进历史
|
||||
|
||||
Go 语言的版本迭代有明确的周期,大体是每半年发布一个版本。
|
||||
|
||||
Go 1.0 发布于 2012 年 3 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1](https://tip.golang.org/doc/go1)。它是 Go 语言发展的一个里程碑。
|
||||
|
||||
在这个版本,Go 官方发布了兼容性文档:[https://tip.golang.org/doc/go1compat](https://tip.golang.org/doc/go1compat),承诺会保证未来的 Go 版本将保持向后兼容。也就是说,将始终兼容已有的代码,保证已有代码在 Go 新版本下编译和运行的正确性。
|
||||
|
||||
在 Go 1.0 之前,Go 在持续迭代它的使用范式,语法规范也在迭代优化。比如 os.Error 到了 Go 1.0 就变成了内置的 error 类型。这个改变看似很小,但实际上是一个至关重要的改变。因为 Go 推荐可能出错的函数返回值都带上 err 值,如果 os.Error 不改为内建类型,就会导致很多模块不得不因为 os.Error 类型而依赖 os 包。
|
||||
|
||||
Go 1.0 最被诟病的问题是它的 GC 效率。相比 Java 近 20 年的长期优化,其成熟度只能以稚嫩来形容。
|
||||
|
||||
与此相对应的是,Go 从一开始就是一门极度重视工程的语言。Go 1.0 就已经有非常完善的工程工具支持。比如:
|
||||
|
||||
- 单元测试:go test;
|
||||
- 文档:go doc;
|
||||
- 静态检查工具:go vet;
|
||||
- 性能 Profile 工具: go tool pprof。
|
||||
|
||||
Go 1.1 发布于 2013 年 5 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.1](https://tip.golang.org/doc/go1.1)。这个版本主要专注于语言内在机制的改善和性能提升(编译器、垃圾回收、map、goroutine调度)。改善后的效果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8f/99/8fbfe652213163c2ae447ecdd7825e99.png" alt="">
|
||||
|
||||
这个版本还发布了一个竞态探测器(race detector),它对 Go 这种以高并发著称的语言显然是重要的。详细可参考 Go 官方博客文章:[https://blog.golang.org/race-detector](https://blog.golang.org/race-detector)。
|
||||
|
||||
Go 1.2 发布于 2013 年 12 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.2](https://tip.golang.org/doc/go1.2)。这个版本发布了单元测试覆盖率检查工具:go tool cover。详细可参考 Go 官方博客文章:[https://blog.golang.org/cover](https://blog.golang.org/cover)。
|
||||
|
||||
Go 1.3 发布于 2014 年 6 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.3](https://tip.golang.org/doc/go1.3)。这个版本栈的内存分配引入了连续段(contiguous segment)的分配模式,以提升执行效率。之前的分页式的栈分配方式(segment stack)存在频繁地分配/释放栈段导致栈内存分配耗时不稳定且效率较低。引入新机制后,分配稳定性和性能都有较大改善。
|
||||
|
||||
Go 1.3 还引入了 sync.Pool,即内存池组件,以减少内存分配的次数。标准库中的 encoding/json、net/http 等都受益于它带来的内存分配效率提升。另外,Go 还对 channel 进行了性能优化:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4c/e3/4c78ebd5be2a3c24e011afa6026af2e3.png" alt="">
|
||||
|
||||
Go 1.4 发布于 2014 年 12 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.4](https://tip.golang.org/doc/go1.4)。从功能来说,这个版本最大的一件事情是增加了 Android/iOS 支持([http://golang.org/x/mobile](http://golang.org/x/mobile)),Gopher 可以使用 Go 编写简单的 Android/iOS 应用。
|
||||
|
||||
但实际上如果从重要程度来说,Go 1.4 最重要的变化是将之前版本中大量用 C 语言和汇编语言实现的 runtime 改为用 Go 实现,这让垃圾回收器执行更精确,它让堆内存的分配减少了 10~30%。
|
||||
|
||||
另外,Go 1.4 引入了 go generate 工具。这是在没有泛型之前解决重复性代码问题的方案。详细见[https://blog.golang.org/generate](https://blog.golang.org/generate)。
|
||||
|
||||
Go 1.5 发布于 2015 年 8 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.5](https://tip.golang.org/doc/go1.5)。这个版本让 Go 实现了自举。这让GC 效率优化成为可能。所以在这个版本中,GC 被全面重构。由于引入并发垃圾回收,回收阶段带来的延迟降低了一个数量级。
|
||||
|
||||
这个版本还有一个很重要的尝试,是引入了 vendor 机制以试图解决 Go 模块的版本管理问题。自从 Go 解决了 GC 效率后,Go 版本管理就成了老大难问题。下图是 Go 社区对 Go 面临的最大挑战的看法:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/cf/4b6aafbca1ce62c50ca9a822359065cf.png" alt="">
|
||||
|
||||
当然后来事实证明 vendor 机制并不成功。
|
||||
|
||||
另外,Go 1.5 引入了 go tool trace,通过该命令我们可以实现执行器的跟踪(trace)。详细参考 [https://golang.org/cmd/trace/](https://golang.org/cmd/trace/)。
|
||||
|
||||
Go 1.6 发布于 2016 年 2 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.6](https://tip.golang.org/doc/go1.6)。垃圾回收器的延迟在这个版本中进一步降低。如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/6c/c3ca946093cbef50d6a5f42349ac216c.png" alt="">
|
||||
|
||||
从功能上来说,这个版本支持了 HTTP/2。
|
||||
|
||||
Go 1.7 发布于 2016 年 8 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.7](https://tip.golang.org/doc/go1.7)。这个版本有一个很重要的变化,是 context 包被加入标准库。这事之所以重要,是因为它和 os.Error 变成内建的 error 类型类似,在网络接口中,context 是传递上下文、超时控制及取消请求的一个标准设施。
|
||||
|
||||
另外,Go 编译器的性能得到了较大幅度的优化,编译速度更快,二进制文件size更小,有些时候幅度可达 20~30%。
|
||||
|
||||
Go 1.8 发布于 2017 年 2 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.8](https://tip.golang.org/doc/go1.8)。GC 延迟在这个版本中进一步得到改善,延迟时间降到毫秒级别以下。
|
||||
|
||||
另外,这个版本还大幅提升了defer的性能。如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/eb/03/ebd7dc28203a1de76ee968184b83d203.png" alt="">
|
||||
|
||||
Go 1.9 发布于 2017 年 8 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.9](https://tip.golang.org/doc/go1.9)。这个版本引入了 type alias 语法。例如:
|
||||
|
||||
```
|
||||
type byte = uint8
|
||||
|
||||
```
|
||||
|
||||
这实际上是一个迟到的语法。我在 Go 1.0 就认为它应该被加入了。另外,sync 包增加了 Map 类型,以支持并发访问(原生 map 类型不支持)。
|
||||
|
||||
Go 1.10 发布于 2018 年 2 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.10](https://tip.golang.org/doc/go1.10)。在这个版本中,go test 引入了一个新的缓存机制,所有通过测试的结果都将被缓存下来。当 test 没有变化时,重复执行 test 会节省大量时间。类似地,go build 也维护了一个已构建的包的缓存以加速构建效率。
|
||||
|
||||
Go 1.11 发布于 2018 年 8 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.11](https://tip.golang.org/doc/go1.11)。这个版本最重要的新功能是 Go modules。前面我们说 Go 1.5 版本引入 vendor 机制以解决模块的版本管理问题,但是不太成功。这是 Go 团队决定推翻重来引入 module 机制的原因。
|
||||
|
||||
另外,这个版本引入了一个重要的试验功能:支持 WebAssembly。它允许开发人员将 Go 源码编译成一个兼容当前主流浏览器的 wasm 文件。这让 Go 作为 Web 开发语言成为可能。
|
||||
|
||||
Go 1.12 发布于 2019 年 2 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.12](https://tip.golang.org/doc/go1.12)。这个版本的 go vet 命令基于 analysis 包进行了重写,使得 go vet 更为灵活并支持 Gopher 编写自己的 checker。详细参考 “[How to Build Your Own Analyzer](https://medium.com/@blanchon.vincent/go-how-to-build-your-own-analyzer-f6d83315586f)” 一文。
|
||||
|
||||
Go 1.13 发布于 2019 年 8 月,详细 ReleaseNote 见 [https://tip.golang.org/doc/go1.13](https://tip.golang.org/doc/go1.13)。这个版本的 sync.Pool 性能得到进一步的改善。当 GC 时,Pool 中对象不会被完全清理掉。它引入了一个 cache,用于在两次 GC 之前清理 Pool 中未使用的对象实例。
|
||||
|
||||
另外,这个版本的逃逸分析(escape analysis)被重新实现了,这让 Go 更少地在堆上分配内存。下图是新旧逃逸分析的基准测试对比:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5e/92/5e415ba622b53a7f9d63060a02125092.png" alt="">
|
||||
|
||||
另外,Go modules 引入的 GOPROXY 变量的默认值被改为:
|
||||
|
||||
```
|
||||
GOPROXY=https://proxy.golang.org,direct
|
||||
|
||||
```
|
||||
|
||||
但在国内无法访问 Go 官方提供的 proxy.golang.org 站点。建议改为:
|
||||
|
||||
```
|
||||
export GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
```
|
||||
|
||||
这里 [https://goproxy.cn](https://goproxy.cn/) 由七牛云赞助支持。
|
||||
|
||||
## Go 版本迭代的背后
|
||||
|
||||
Go 语言的版本迭代的规划非常值得认真推敲与学习。
|
||||
|
||||
Go 的版本迭代还是比较高频的,但是有趣的是,在 Go 1.0 版本之后,语言本身的功能基本上已经非常稳定,只有极少量的变动。比如 type alias 这样的小特性,都已经可以算是关键语法变化了。
|
||||
|
||||
那么,这些年 Go 语言都在变化些什么?
|
||||
|
||||
其一,性能、性能、性能!尤其在 GC 效率这块,持续不断地优化。为了它,大范围重构 Go 的实现,完成了自举。其他还有很多,比如连续栈、内存池(sync.Pool)、更快的编译速度、更小的可执行文件尺寸。
|
||||
|
||||
其二,强化工程能力。各种 Go tool 的增加就不说了,这其中最为突出的就是 Go 模块的版本管理,先后尝试了 vendor 和 module 机制。
|
||||
|
||||
其三,标准库的能力增强,如 context,HTTP 2.0 等等。这块大部分比较常规,但 context 的引入可以算是对网络编程最佳实践的一次标准化过程。
|
||||
|
||||
其四,业务领域的扩展。这块 Go 整体还是比较专注于服务端领域,只是对 Android、iOS、WebAssembly 三个桌面平台做了经验性的支持。
|
||||
|
||||
## 如何做版本规划
|
||||
|
||||
蛮多技术背景的同学在做版本规划的时候,往往容易一开始就陷入到技术细节的泥潭。但其实对于一个从 0 到 1 的业务来说,首先应该把焦点放到什么地方,这个选择才至关重要。
|
||||
|
||||
Go 语言在这一点上给出了非常好的示范。它首先把焦点放在了用户使用姿势的迭代上。凡与此无关的事情,只要达到及格线了就可以先放一放。这也是 Go 为什么一上来虽然有很多关于 GC 效率的吐槽,但是他们安之若素,仍然专注于用户使用姿势的迭代。
|
||||
|
||||
但是一旦语言开始大规模推广,进入从 1 到 100 的扩张阶段,版本迭代的关注点反而切换到了用户看不见的地方:非功能性需求。生产环境中用户最关心的指标,就成了 Go 团队最为关注的事情,日复一日,不断进行迭代优化。
|
||||
|
||||
这是很了不起的战略定力:知道什么情况下,最该做的事情是什么。
|
||||
|
||||
那么,遇到重大的客户需求,对之前我们培养的用户习惯将形成重大挑战怎么办?一些人可能会习惯选择快速去支持这类重大需求,因为这些需求通常很可能听起来很让人振奋。
|
||||
|
||||
其实 Go 语言也遇到了这样的需求:泛型的支持。
|
||||
|
||||
泛型被 Go 团队非常认真地对待。可以预期的是,Go 2.0 一定会支持泛型。但是,他们并没有急着去实现它。Go 社区不少人在 Go 1.9 的时候,很激动地期待着 Go 2.0,期待着泛型,但是 Go 出来了 Go 1.10,甚至到现在的 Go 1.13。
|
||||
|
||||
显然,泛型被放到了一个旁路的版本。这个旁路版本独立演化直到最终验证已经成熟,才会被合并到 Go 1.x 中。这时,Go 2.0 就会诞生了。
|
||||
|
||||
这其实才是正确响应会招致巨大影响面的功能需求的姿势。
|
||||
|
||||
客户是需要尊重的。而尊重客户的正确姿势毫无疑问是:别折腾他们。
|
||||
|
||||
## 结语
|
||||
|
||||
今天我们聊的话题是版本迭代的规划。在不同阶段,版本迭代的侧重点会有极大的不同。从 0 到 1 阶段,我们验证的是用户使用姿势,性能并不是第一位的。但是进入扩张阶段,产品竞争力就是关键指标,这时候我们迭代的是用户价值最大的,也是用户真正最在乎的那部分。
|
||||
|
||||
遇到会对产品产生巨大冲击的需求,头脑别发热,谨慎处理。回到从 0 到 1 阶段的方法论,在少量客户上先做灰度。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “软件工程的未来”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
76
极客时间专栏/geek/许式伟的架构课/软件工程篇/76 | 软件工程的未来.md
Normal file
76
极客时间专栏/geek/许式伟的架构课/软件工程篇/76 | 软件工程的未来.md
Normal file
@@ -0,0 +1,76 @@
|
||||
<audio id="audio" title="76 | 软件工程的未来" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2b/99/2b3921da14fd0e2197fed92a5b7cb999.mp3"></audio>
|
||||
|
||||
你好,我是七牛云许式伟。现在正值中国年,我在这里祝大家新年快乐。开开心心过大年的同时,注意安全第一,出门记得戴上口罩,少去人员聚集的地方。
|
||||
|
||||
好,那我们开始今天的学习,今天我们想聊聊软件工程的未来。
|
||||
|
||||
软件工程是一门非常年轻的学科,相比其他动辄跨世纪的自然科学而言,软件工程只有 50 年的历史。只有如此短暂实践的科学,今天我们来探讨它的未来,条件其实还并不算太充分。
|
||||
|
||||
但是我们的宗旨就是要每个领域都应该谈清楚过去(历史)与未来(趋势判断),所以今天不妨也理性来探讨一下。
|
||||
|
||||
在 “[软件工程的宏观视角](https://time.geekbang.org/column/article/182924)” 一讲中,我们引入了下图来表达软件工程的瀑布模型:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b8/0e/b86b9e0e6c9185e6993e7cc90175980e.png" alt="">
|
||||
|
||||
在这样一个模型里面,涉及的角色分工已经非常多:
|
||||
|
||||
- 产品经理;
|
||||
- 架构师;
|
||||
- 开发工程师;
|
||||
- 质量保障(QA)工程师;
|
||||
- 网站可靠性工程师(SRE);
|
||||
- ……
|
||||
|
||||
但这还只是常规描述的工种。实际的分工要细致很多。更不要说对特殊的领域,比如企业服务,也就是大家常说的 2B 行业,它的基本过程是这样的:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/0e/e9d2093b9bd61775812dcd19b32fa50e.png" alt="">
|
||||
|
||||
比之纯粹的产品研发上线过程,它多了单个客户的跟进与落地实施过程,也由此引入更多的角色分工,比如:售前工程师、交付(实施)工程师、售后工程师、项目经理等。
|
||||
|
||||
未来软件工程会走向何方?
|
||||
|
||||
首先 “快速变化” 是软件工程的自然属性,其 “不确定性” 也只能抑制而无法消除。
|
||||
|
||||
但显而易见的是,软件工程的问题最终还是由软件解决。事实上今天很多问题已经解决得很好,比如源代码的管理。我们经历了 cvs、svn,最终到今天的 git。基本上开发人员的协同问题已经形成非常约定俗成的方法论,并以软件或云服务的方式被固化下来。
|
||||
|
||||
今天,线上服务管理正如火如荼的发展。假以时日,不需要多久之后,一个全新的时代开启,我们中大部分人不必再为线上服务的稳定性操心。关于这块更详细的讨论,可以参考第四章 “服务治理篇”。
|
||||
|
||||
需求管理与测试这块也已经得到很好的解决。唯一比较遗憾就是是界面(UI)相关的测试虽然也有相关的工具链,但当前的普及率仍然极低。
|
||||
|
||||
这可能与大部分公司都较难保证界面的稳定性有关。如果我们经常变动界面,这就如同我们经常调整一个模块对外的接口规格一样,必然导致相关的测试案例编译通不过,或者测试通不过。这会让人沮丧,进而丧失对实现界面(UI)测试自动化的信心。自动化测试极其依赖被测模块接口的稳定性,这是我们今天常规自动化测试方法的限制。
|
||||
|
||||
当然另一方面,这也与界面测试相对高维,大部分公司的质量保障水平都还没有到达这个级别有关。从现实来看,虽然单元测试方法论已经极其成熟,但是仍然有不少企业在推行中遇到不少障碍。
|
||||
|
||||
可以预期,随着企业的平均工程水平逐步提升,最终会形成越来越多的有效的界面测试最佳实践的方法论,并得以大范围的推广。
|
||||
|
||||
从全局来看,今天软件工程已经形成较为成熟的分工。但各类分工的最佳实践与软件系统,仍然是相对孤立的。
|
||||
|
||||
这一定程度上也与软件工程还很年轻有关。从软件工程的软件系统发展来说,可以预期的是,未来一定会形成更加一体化的系统,上一道 “工序” 的输出就是下一道 “工序” 的输入。
|
||||
|
||||
但是今天一些 “工序” 的输出仍然是人肉进行传递,甚至没有标准化的仓库管理它。例如,产品经理输出的产品界面设计原型、架构师输出的架构设计文档,其传递过程仍然有极大的随意性。
|
||||
|
||||
但是,软件工程的最大不确定性就来源于 “设计” 类工作,包括产品设计与软件的架构设计。今天虽然产品设计和架构设计也都有一些独立的工具,但普及度与刚才说的开发与测试类工程实践相比完全是小巫见大巫。
|
||||
|
||||
这是可以理解的,产品经理与架构师在软件工程中属于小众群体,其培养难度极高,很多经验也很难形成传统意义上的 “知识点” 来传递。所以真正意义上合格的产品经理与架构师是比较少的,和程序员(软件开发工程师)的规模完全无法相比。
|
||||
|
||||
就拿架构师这个岗位举例。架构师的职责是什么,架构师工作的方法论是什么、培养架构师的方法论又是怎样的,这些今天并没有一个被广泛接受的实践。
|
||||
|
||||
为什么我会写这个架构课专栏,以及为什么成立七牛大学开启线下的架构师实战训练营,也是希望能够在一定程度上找到这些问题的最佳答案。
|
||||
|
||||
而事实上,产品经理的培养有更高的难度。严格意义上来说,成为产品经理前,首先应该成为架构师。我这个观念可能与大部分人的常识相悖,但是我个人对此深信不疑。
|
||||
|
||||
软件工程的未来发展会怎样,细节上很难给出确定性的判断。但是,我们相信,软件工程极大成熟的标志,是一体化的软件工程支撑系统,和高效的人才培养体系。包括今天仍然极为稚嫩的架构师培养体系,和产品经理培养体系,都应该得到了极大的完善。
|
||||
|
||||
到那个时候,软件工程就成为了一门真正成熟的科学。
|
||||
|
||||
## 结语
|
||||
|
||||
软件工程项目迭代快速、充满变化、充满不确定性。这使得软件工程成为一门极其独特魅力的科学。今天这门科学仍然还非常年轻,其发展只能以日新月异来形容。
|
||||
|
||||
软件工程的未来,它的成熟不单单是工程方法论和业务系统软件的成熟,也需要包括人才培养体系的成熟。因为,软件工程的不确定性与它充满设计与创造有关,人的主观能动性是它的优势,但也意味着不确定性无法得到彻底的消除。
|
||||
|
||||
我们要做的,只能说在大量的不确定性中,找到尽可能多的确定性。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。至此,本章 “软件工程篇” 已经到尾声阶段,下一讲我们将对本章的内容进行回顾与总结。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
106
极客时间专栏/geek/许式伟的架构课/软件工程篇/77 | 软件工程篇:回顾与总结.md
Normal file
106
极客时间专栏/geek/许式伟的架构课/软件工程篇/77 | 软件工程篇:回顾与总结.md
Normal file
@@ -0,0 +1,106 @@
|
||||
<audio id="audio" title="77 | 软件工程篇:回顾与总结" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4d/f9/4d7425f5b60bfb589871eed560ed15f9.mp3"></audio>
|
||||
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
我们架构课的最后一章软件工程篇到此就要结束了。今天我们就本章的内容进行回顾与总结。
|
||||
|
||||
架构师并不是一个纯技术岗位。我们从软件工程的视角来看,架构师的职责就是要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b8/0e/b86b9e0e6c9185e6993e7cc90175980e.png" alt="">
|
||||
|
||||
软件工程所覆盖的范畴非常广泛。从开始的需求与历史版本缺陷,到新版本的产品设计,到架构设计,到编码与测试,到最终的产品发布,到线上服务的持续维护。
|
||||
|
||||
还有贯穿整个工程始终的,是不变的团队分工与协同,以及不变的质量管理。
|
||||
|
||||
我们这个专栏并没有打算站在完整的软件工程角度去谈,更多还是从架构师与软件工程的关联入手。
|
||||
|
||||
本章的内容大体如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c1/0e/c154a572ef5bf03f169b05e8bd13030e.png" alt="">
|
||||
|
||||
软件工程是一项团体活动,大家有分工更有协同。不同的个体因为能力差别,可以形成十倍以上的生产力差距。而不同的团体更是如此,他们的差距可能更上一个数量级,达到百倍以上的生产力差距。
|
||||
|
||||
百倍以上的差距是什么概念?这就是说,一个团队只需要三四天做出来的东西,另一个团队可能需要一年才能做出来。两者之间的差距之大,只能用天壤之别来形容。
|
||||
|
||||
个人与个人的差距,你可以认为是技术上的能力差距的反映。但团队与团队的差距,不是简单的技术上的能力差距,而是有着更为深刻的原因。
|
||||
|
||||
高效团队的效率,核心体现在以下两个方面:
|
||||
|
||||
- 团队开发一个新功能的效率。它体现的是架构的老化程度。
|
||||
- 团队新人的融入效率。新人多快的速度可以融入到团队,理解业务系统的现状及团队的做事方式。
|
||||
|
||||
开发新功能的效率,主要取决于架构的优劣。这初听起来是一项纯技术上的事情。但如果我们站在时间维度上长达数年甚至数十年的软件工程的角度看,能够维持架构设计的持续优异,这绝非某个人的技术能力可以做到的事情,而是要靠团队共同的坚持。
|
||||
|
||||
而从新人融入效率看,更非技术能力所能够简单囊括,而是仰仗团队对业务传承的坚持。
|
||||
|
||||
这些东西的背后,关乎的都是有关于协同的科学。
|
||||
|
||||
有的团体像一盘散沙,充其量可以叫团伙。有的团体则有极强的凝聚力,整个团队上下同心,拧成一股绳,这种团体才是高效率组织,是真正意义上的团队。
|
||||
|
||||
共识是团队效率的基础。
|
||||
|
||||
从软件工程角度来说,产品设计和架构设计是团队最大的共识。架构过程就是一次团队共识确认的过程,从项目的混沌之初,到团队形成越来越清晰且一致的视图( Picture)。
|
||||
|
||||
高效团队往往还有极高的团队默契,这让他们无论是维护老项目还是做什么新项目都如鱼得水。团队默契可以包含很多东西,比如:
|
||||
|
||||
- 共同的目标;
|
||||
- 团队的做事态度与价值观;
|
||||
- 编码规范;
|
||||
- 架构设计文档的模板;
|
||||
- 软件工程的方法论;
|
||||
- 基础架构及技术选型;
|
||||
- ……
|
||||
|
||||
对于一个团队新人来说,融入一个团队或一个项目的基础过程就是阅读别人写的源代码。既有的文档越清楚,新人阅读代码的障碍就越小,融入的速度就越快。
|
||||
|
||||
文档要怎样才能把问题说清楚?
|
||||
|
||||
文档传递的是思维方式。大多数程序员不善于写文档,甚至讨厌写文档。这背后的根源不在于文档本身,而在于有效的思维表达方式,这需要长期的训练。
|
||||
|
||||
软件工程的各个环节都有其交付物。理想情况下,上一个环节的输出是下一环节的输入。软件系统的质量管理一般从这些交付物的管理入手。例如:交付物的版本管理、单元测试、持续构建,灰度发布,等等。
|
||||
|
||||
从更宏观的视角看,我们还涉及人力资源规划的问题。什么东西应该外包出去,包给谁?软件版本的计划是什么样的,哪些功能先做,哪些功能后做?
|
||||
|
||||
这些选择非常非常重要。因为他们属于业务架构的顶层设计。
|
||||
|
||||
除了传统意义上的外包外,外包方式还有:开源(众包)、云服务(服务外包)、商业软件(产品外包)。在外包方式的选择上,我们的建议是:
|
||||
|
||||
>
|
||||
我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。
|
||||
|
||||
|
||||
>
|
||||
在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。
|
||||
|
||||
|
||||
当然,哪些事情是非核心竞争力相关,这一点不同公司可能判断不尽相同。但基本的判断逻辑是,越与我们面向用户所提供的业务流程相关,越靠近企业的核心竞争力,也就越不能外包。
|
||||
|
||||
软件版本迭代的规划需要根据业务的发展阶段而定。在不同阶段,版本迭代的侧重点会有极大的不同。
|
||||
|
||||
从 0 到 1 阶段,我们验证的是用户使用姿势,也就是产品设计的规格。这时性能并不是第一位的。
|
||||
|
||||
但是进入扩张阶段,产品竞争力就是一些用户关心的关键指标。这时候我们迭代的不再是用户使用姿势,它已经非常稳定。我们迭代的往往是看不见的非功能性需求,是那些用户真正最在乎的部分。
|
||||
|
||||
而遇到会对产品产生巨大冲击的功能需求,头脑别发热,谨慎处理。回到从 0 到 1 阶段的方法论,在少量客户上先做灰度。
|
||||
|
||||
## 结语
|
||||
|
||||
软件工程还很年轻,只有 50 年的历史。有关于软件工程的系统与方法论都仍然在快速演化与迭代中。
|
||||
|
||||
这意味着意我们不必墨守成规。要勇于探索,勇于打破固有的惯例,去建立新的方法论,新的惯例。
|
||||
|
||||
但需要强调的是,打破惯例不是胡闹,不是要做不尊重科学的 “野蛮人”。今天仍然有那么一批工程师,人数还不在少数,他们随心所欲、任性而为,不喜欢写架构设计文档,不喜欢写单元测试,不喜欢代码互审(code review)。
|
||||
|
||||
我们首先需要尊重团队协同的科学,在尊重的基础上去探索新的更高效的协同方法论。
|
||||
|
||||
很早之前我说过以下这段话,它很长一段时间里,被贴在某家公司墙上:
|
||||
|
||||
>
|
||||
严谨并非创新的对立面,而是创新的重要基础。每个人都有灵光乍现的时刻,但是唯有那些拥有严谨的科学态度的人才能抓住它,把它变成现实。
|
||||
|
||||
|
||||
我想,它非常适合作为软件工程篇的结束语。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。至此,本章 “软件工程篇” 结束,下一讲将结束本专栏的内容。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
Reference in New Issue
Block a user