first commit
This commit is contained in:
166
专栏/许式伟的架构课/62 重新认识开闭原则(OCP).md
Normal file
166
专栏/许式伟的架构课/62 重新认识开闭原则(OCP).md
Normal file
@ -0,0 +1,166 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
62 重新认识开闭原则 (OCP)
|
||||
62 | 重新认识开闭原则 (OCP)你好,我是七牛云许式伟。
|
||||
|
||||
架构的本质是业务的正交分解。
|
||||
|
||||
在上一讲 “61 | 全局性功能的架构设计” 中我们提到,架构分解中有两大难题:其一,需求的交织。不同需求混杂在一起,也就是存在所谓的全局性功能。其二,需求的易变。不同客户,不同场景下需求看起来很不一样,场景呈发散趋势。
|
||||
|
||||
我们可能经常会听到各种架构思维的原则或模式。但,为什么我们开始谈到架构思维了,也不是从那些耳熟能详的原则或模式谈起?
|
||||
|
||||
因为,万变不离其宗。
|
||||
|
||||
就架构的本质而言,我们核心要掌握的架构设计的工具其实就只有两个:
|
||||
|
||||
|
||||
组合。用小业务组装出大业务,组装出越来越复杂的系统。
|
||||
如何应对变化(开闭原则)。
|
||||
|
||||
|
||||
开闭原则(OCP)
|
||||
|
||||
今天我们就聊聊怎么应对需求的变化。
|
||||
|
||||
谈应对变化,就不能不提著名的 “开闭原则(Open Closed Principle,OCP)”。一般认为,最早提出开闭原则这一术语的是勃兰特·梅耶(Bertrand Meyer)。他在 1988 年在 《面向对象软件构造》 中首次提出了开闭原则。
|
||||
|
||||
什么是开闭原则(OCP)?
|
||||
|
||||
|
||||
软件实体(模块,类,函数等)应该对于功能扩展是开放的,但对于修改是封闭的。
|
||||
|
||||
|
||||
一个软件产品只要在其生命周期内,都会不断发生变化。变化是一个事实,所以我们需要让软件去适应变化。我们应该在设计时尽量适应这些变化,以提高项目的稳定性和灵活性,真正实现 “拥抱变化”。
|
||||
|
||||
开闭原则告诉我们,应尽量通过扩展软件实体的行为来应对变化,满足新的需求,而不是通过修改现有代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
|
||||
|
||||
为什么会有这样的架构设计原则?它背后体现的架构哲学是什么?
|
||||
|
||||
本质上,开闭原则的背后,是推崇模块业务的确定性。我们可以修改模块代码的缺陷(Bug),但不要去随意调整模块的业务范畴,增加功能或减少功能都并不鼓励。这意味着,它认为模块的业务变更是需要极其谨慎的,需要经得起推敲的。
|
||||
|
||||
我个人非常推崇 “开闭原则”。它背后隐含的架构哲学,和我说的 “架构的本质是业务的正交分解” 一脉相承。
|
||||
|
||||
与其修改模块的业务,不如实现一个新业务。只要业务的分解一直被正确执行的话,实现一个新的业务模块来完成新的业务范畴,是一件极其轻松的事情。从这个角度来说,开闭原则鼓励写 “只读” 的业务模块,一经设计就不可修改,如果要修改业务就直接废弃它,转而实现新的业务模块。
|
||||
|
||||
这种 “只读” 思想,大家可能很熟悉。比如基于 Git 的源代码版本管理、基于容器的服务治理都是通过 “只读” 设计来改善系统的治理难度。
|
||||
|
||||
对于架构设计来说同样如此。“只读” 的架构分解让我们逐步沉淀下越来越多可复用的业务模块。如此,我们不断坚持下去,随着时间沉淀,我们的组织就会变得很强大,组装复杂业务系统也将变得越来越简单。
|
||||
|
||||
所以开闭原则,是架构治理的根本哲学。
|
||||
|
||||
CPU 背后的架构思维
|
||||
|
||||
一种广泛的误解认为,开闭原则是一种 “面向对象编程(OOP)” 领域提出来的编程思想。但这种理解显然太过狭隘。虽然开闭原则的正式提出可能较晚,但是在信息科技的发展历程中,开闭原则思想的应用就太多了,它是信息技术架构的基本原则。注意我这里没有用 “软件架构” 而是用 “信息技术架构”,因为它并不只适用于软件设计的范畴。
|
||||
|
||||
我们在 “02 | 大厦基石:无生有,有生万物” 一讲介绍冯·诺依曼体系的规格时就讲过:
|
||||
|
||||
|
||||
从需求分析角度来说,关键要抓住需求的稳定点和变化点。需求的稳定点,往往是系统的核心价值点;而需求的变化点,则往往需要相应去做开放性设计。
|
||||
|
||||
|
||||
冯·诺依曼体系的中央处理器(CPU)的设计完美体现了 “开闭原则” 的架构思想。它表现在:
|
||||
|
||||
|
||||
指令是稳定的,但指令序列是变化的,只有这样计算机才能够实现 “解决一切可以用 ‘计算’ 来解决的问题” 这个目标。
|
||||
计算是稳定的,但数据交换是多变的,只有这样才能够让计算机不必修改基础架构却可以适应不断发展变化的交互技术革命。
|
||||
|
||||
|
||||
体会一下:我们怎么做到支持多变的指令序列的?我们由此发明了软件。我们怎么做到支持多变的输入输出设备的?我们定义了输入输出规范。
|
||||
|
||||
我们不必去修改 CPU,但是我们却支持了如此多姿多彩的信息世界。
|
||||
|
||||
多么优雅的设计。它与面向对象无关,完全是开闭原则带来的威力。
|
||||
|
||||
CPU 的优雅设计远不止于此。在 “07 | 软件运行机制及内存管理” 这一讲中,我们介绍了 CPU 对虚拟内存的支持。通过引入缺页中断,CPU 将自身与多变的外置存储设备,以及多变的文件系统格式进行了解耦。
|
||||
|
||||
中断机制,我们可以简单把它理解为 CPU 引入的回调函数。通过中断,CPU 把对计算机外设的演进能力交给了操作系统。这是开闭原则的鲜活案例。
|
||||
|
||||
插件机制
|
||||
|
||||
一些人对开闭原则的错误解读,认为开闭原则不鼓励修改软件的源代码来响应新需求。
|
||||
|
||||
这个说法当然有点极端化。开闭原则关注的焦点是模块,并不是最终形成的软件。模块应该坚持自己的业务不变,这是开闭原则所鼓励的。
|
||||
|
||||
当然软件也是一个业务系统,但对软件系统这个大模块来说,如果我们坚持它的业务范畴不变,就意味着我们放弃进步。
|
||||
|
||||
让软件的代码不变,但业务范畴却能够适应需求变化,有没有可能?
|
||||
|
||||
有这个可能性,这就是插件机制。
|
||||
|
||||
常规我们理解的插件,通常以动态库(dll/so)形式存在,这种插件机制是操作系统引入的,可以做到跨语言。当然部分语言,比如 Java,它有自己的插件机制,以 jar 包的形式存在。
|
||||
|
||||
在上一讲 “61 | 全局性功能的架构设计” 中我们提到,微软的大部分软件,以 Office 和 Visual Studio 为代表,都提供了二次开发能力。
|
||||
|
||||
这些二次开发接口构成了软件的插件机制,并最终让它成为一个生态型软件。
|
||||
|
||||
一般来说,提供插件机制的二次开发接口需要包含以下三个部分。
|
||||
|
||||
其一,软件自身能力的暴露,也就是我们经常说的 DOM API。插件以此来调用软件已经实现的功能,这是最基础的部分,我们这里不进一步展开。
|
||||
|
||||
其二,插件加载机制。通常,这基于文件系统,比如我们规定把所有插件放到某个目录下。在 Windows 平台下会多一个选择,把插件信息写到注册表。
|
||||
|
||||
其三,事件监听。这是关键,也是难点所在。没有事件,插件没有机会介入到业务中去。但是应该提供什么样的事件,提供多少个事件,这非常依赖架构能力。
|
||||
|
||||
原则来说,在提供的能力相同的情况下,事件当然越少越好。但是怎么做到少而精,这非常有讲究。一般来说,事件分以下三类:
|
||||
|
||||
其一,界面操作类。最原始的是鼠标和键盘操作,但它们太过于底层,提供出去会是双刃剑,一般对二次开发接口来说会选择不提供。更多的时候会选择暴露更高级的界面事件,比如菜单项或按钮的点击。
|
||||
|
||||
其二,数据变更类。在数据发生变化的时候,允许捕获它并做点什么。最为典型的是 onSelectionChanged 这个事件,基本上所有的软件二次开发接口都会提供。当然它属于界面数据变更,只能说是数据变更的特例。如果我们回忆一下 MVC 框架(参见 “22 | 桌面程序的架构建议”),就能够记得 Model 层会发出数据变更通知,也就是 onDataChanged 类的事件出来给 View 或 Controller。
|
||||
|
||||
其三,业务流程类。它通常发生在某个业务流的中间某个环节,或者业务流完成之后。比如对 Office 软件来说,打开文件之初或之后,都可能发出相应的事件,以便插件做些什么。
|
||||
|
||||
通过以上分析可以看出,完整的插件机制还是比较庞大的。但实际应用中插件机制未必要做得如此之重。
|
||||
|
||||
比如,Go语言中的 image 包,它提供的 Decode 和 DecodeConfig 等功能都支持插件,我们可以增加一种格式支持,而无需修改 image 包。
|
||||
|
||||
这里面最大的简化,是放弃了插件加载机制。我们自己手工来加载插件,比如:
|
||||
|
||||
import "image"
|
||||
import _ "image/jpeg"
|
||||
import _ "image/png"
|
||||
|
||||
|
||||
这段代码为 image 包加载了两个插件,一个支持 jpeg,一个支持 png 格式。
|
||||
|
||||
如果大家仔细研究过我们实战案例 “画图程序” 的代码(参见 “加餐 | 实战:画图程序的整体架构”)就会发现,类似的插件机制的运用有很多。我们说的架构分解,把复杂系统分解为一个最小化的核心系统,加上多个相互正交的周边系统,它背后的机制往往就是我们这里提的插件机制。
|
||||
|
||||
插件机制的确让核心系统与周边系统耦合度大大降低。但插件机制并非没有成本。插件机制本身也是核心系统的一个功能,它本身也需要考虑与核心系统其他功能的耦合度。
|
||||
|
||||
如果某插件机制没有多少客户,也就是说,没有几个功能基于它开发,而它本身代码又散落在核心系统的各个角落,那么投入产出就显然不成比例。
|
||||
|
||||
所以维持足够的通用性,是提供插件机制的重大前提。
|
||||
|
||||
单一职责原则
|
||||
|
||||
到此为止,相信大家已经对开闭原则(OCP)非常了解了。总结来说就两点:
|
||||
|
||||
第一,模块的业务要稳定。模块的业务遵循 “只读” 设计,如果需要变化不如把它归档,放弃掉。这种模块业务只读的思想,是架构治理的基础哲学。
|
||||
|
||||
第二,模块的业务变化点,简单一点的,通过回调函数或者接口开放出去,交给其他的业务模块。复杂一点的,通过引入插件机制把系统分解为 “最小化的核心系统+多个彼此正交的周边系统”。事实上回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。
|
||||
|
||||
平常,我们大家也经常会听到 “单一职责原则(Single Responsibility Principle,SRP)”,它强调的是每个模块只负责一个业务,而不是同时干多个业务。而开闭原则强调的是把模块业务的变化点抽离出来,包给其他的模块。它们谈的本质上是同一个问题的两个面。
|
||||
|
||||
结语
|
||||
|
||||
从来没有人这样去谈架构的本质,也没有人这样解读开闭原则(OCP),对吧?
|
||||
|
||||
其实对于这部 “架构课” 的革命性,我自己从没怀疑过。它的内容是精心设计的,为此我准备了十几年。我们用了四章内容来谈信息科技的需求与架构的演进,然后才进入正题。
|
||||
|
||||
用写文章的角度来说,这个伏笔的确够深的。
|
||||
|
||||
当然这不完全是伏笔。如果我们把整个信息科技看作最大的一个业务系统,我们有无数人在为之努力奋进,迭代它的架构。大家在竟合中形成自然的分工。学习信息科技的演进史,是学习架构的必要组成部分。我们一方面从中学习怎么做需求分析,另一方面也从中体悟做架构的思维哲学。
|
||||
|
||||
当然,还有最重要的一点是,我们要知道演进的结果,也就是信息科技最终形成的基础架构。
|
||||
|
||||
作为架构师,我们除了做业务架构,还有一个同等难度的大事,就是选择合适的基础架构。基础架构+业务架构,才是你设计的软件的全部。作为架构师,千万不要一叶障目,不见泰山,忘记基础架构选择的重要性。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “接口设计的准则”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
209
专栏/许式伟的架构课/63 接口设计的准则.md
Normal file
209
专栏/许式伟的架构课/63 接口设计的准则.md
Normal file
@ -0,0 +1,209 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
63 接口设计的准则
|
||||
63 | 接口设计的准则你好,我是七牛云许式伟。
|
||||
|
||||
上一讲 “[62 | 重新认识开闭原则 (OCP)]” 我们介绍了开闭原则。这一讲的内容非常非常重要,可以说是整个架构课的灵魂。总结来说,开闭原则包含以下两层含义:
|
||||
|
||||
第一,模块的业务要稳定。模块的业务遵循 “只读” 设计,如果需要变化不如把它归档,放弃掉。这种模块业务只读的思想,是架构治理的基础哲学。我平常和小伙伴们探讨模块边界的时候,经常会说这样一句话:
|
||||
|
||||
|
||||
每一个模块都应该是可完成的。
|
||||
|
||||
|
||||
这实际上是开闭原则的业务范畴 “只读” 的架构治理思想的另一种表述方式。
|
||||
|
||||
第二,模块业务的变化点,简单一点的,通过回调函数或者接口开放出去,交给其他的业务模块。复杂一点的,通过引入插件机制把系统分解为 “最小化的核心系统+多个彼此正交的周边系统”。事实上回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。
|
||||
|
||||
今天,我们想聊聊怎么做接口设计。
|
||||
|
||||
不过在探讨这个问题前,我想和大家探讨的第一个问题是:什么是接口?
|
||||
|
||||
你可能会觉得这个问题挺愚蠢的。毕竟这几乎是我们嘴巴里天天会提及的术语,会不知道?但让我们用科学家的严谨作风来看待这个问题。接口在不同的语义环境下,主要有两个不同含义。
|
||||
|
||||
一种是模块的使用界面,也就是规格,比如公开的类或函数的原型。我们前面在这个架构课中一直强调,模块的接口应该自然体现业务需求。这里的接口,指的就是模块的使用界面。
|
||||
|
||||
另一种是模块对依赖环境的抽象。这种情况下,接口是模块与模块之间的契约。在架构设计中我们经常也会听到 “契约式设计(Design by Contract)” 这样的说法,它鼓励模块与模块的交互基于接口作为契约,而不是依赖于具体实现。
|
||||
|
||||
对于这两类的接口语义,我们分别进行讨论。
|
||||
|
||||
模块的使用界面
|
||||
|
||||
对于模块的使用界面,最重要的是 KISS 原则,让人一眼就明白这个模块在做什么样的业务。
|
||||
|
||||
KISS 的全称是 Keep it Simple, Stupid,直译是简单化与傻瓜化。用土话来说,就是要 “让傻子也能够看得懂”,追求简单自然,符合惯例。
|
||||
|
||||
这样说比较抽象,我们拿七牛开源的 mockhttp 项目作为例子进行说明。
|
||||
|
||||
这个项目早期的项目地址为:
|
||||
|
||||
|
||||
代码主页:https://github.com/qiniu/mockhttp.v1
|
||||
文档主页:https://godoc.org/github.com/qiniu/mockhttp.v1
|
||||
|
||||
|
||||
最新的项目地址变更为:
|
||||
|
||||
|
||||
代码主页:https://github.com/qiniu/x/tree/master/mockhttp
|
||||
文档主页:https://godoc.org/github.com/qiniu/x/mockhttp
|
||||
|
||||
|
||||
mockhttp 是做什么的呢?它用于启动 HTTP 服务作为测试用途。
|
||||
|
||||
当然 Go 的标准库 net/http/httptest 已经有自己的 HTTP 服务启动方法,如下:
|
||||
|
||||
package httptest
|
||||
|
||||
type Server struct {
|
||||
URL string
|
||||
...
|
||||
}
|
||||
|
||||
func NewServer(service http.Handler) (ts *Server)
|
||||
func (ts *Server) Close()
|
||||
|
||||
|
||||
httptest.NewServer 分配一个空闲可用的 TCP 端口,并将它与传入的 HTTP 服务器关联起来。最后我们得到的 ts.URL 就是服务器的访问地址。使用样例如下:
|
||||
|
||||
import "net/http"
|
||||
import "net/http/httptest"
|
||||
|
||||
func TestXXX(t *testing.T) {
|
||||
service := ... // HTTP 业务服务器
|
||||
ts := httphtest.NewServer(service)
|
||||
defer ts.Close()
|
||||
|
||||
resp, err := http.Get(ts.URL + "/foo/bar")
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
mockhttp 有所不同,它并不真的启动 HTTP 服务,没有端口占用。这里我们不谈具体的原理,我们看接口。mockhttp.v1 版本的使用界面如下:
|
||||
|
||||
package mockhttp
|
||||
|
||||
var Client rpc.Client
|
||||
|
||||
func Bind(host string, service interface{})
|
||||
|
||||
|
||||
这里比较古怪的是 service,它并不是 http.Handler 类型。它背后做了一件事情,就是帮 service 这个 HTTP 服务器自动实现请求的路由分派能力。这有一定的好处,使用上比较便捷:
|
||||
|
||||
import "github.com/qiniu/mockhttp.v1"
|
||||
|
||||
func TestXXX(t *testing.T) {
|
||||
service := ... // HTTP 业务服务器
|
||||
mockhttp.Bind("example.com", service)
|
||||
resp, err := mockhttp.Client.Get("http://example.com/foo/bar")
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
但是它有两个问题。
|
||||
|
||||
一个问题是关于模块边界上的。严谨来说 mockhttp.v1 并不符合 “单一职责原则(SRP)”。它干了两个业务:
|
||||
|
||||
|
||||
启动 HTTP 测试服务;
|
||||
实现 HTTP 服务器请求的路由分派。
|
||||
|
||||
|
||||
另一个是关于接口的 KISS 原则。mockhttp.Bind 虽然听起来不错,也很简单,但实际上并不符合 Go 语言的惯例语义。另外就是 mockhttp.Client 变量。按 Go 语义的惯例它可能叫 DefaultClient 会更好一些,另外它的类型是 rpc.Client,而不是 http.Client,这样方便是方便了,但却产生了多余的依赖。
|
||||
|
||||
mockhttp.v1 这种业务边界和接口的随意性,一定程度上是因为它是测试用途,所以有点怎么简单怎么来的意思。但是后来的发展表明,所有的偷懒总会还回来的。于是就有了 mockhttp.v2 版本。这个版本在我们做小型的 package 合并时,把它放到了https://github.com/qiniu/x 这个package 中。接口如下:
|
||||
|
||||
package mockhttp
|
||||
|
||||
var DefaultTransport *Transport
|
||||
var DefaultClient *http.Client
|
||||
|
||||
func ListenAndServe(host string, service http.Handler)
|
||||
|
||||
|
||||
这里暴露的方法和变量,一方面 Go 程序员一看即明其义,另一方面语义上和 Go 标准库既有的HTTP package 可自然融合。它的使用方式如下:
|
||||
|
||||
import "github.com/qiniu/x/mockhttp"
|
||||
|
||||
func TestXXX(t *testing.T) {
|
||||
service := ... // HTTP 业务服务器
|
||||
mockhttp.ListenAndServe("example.com", service)
|
||||
resp, err := mockhttp.DefaultClient.Get("http://example.com/foo/bar")
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
从上面的例子可以看出,我们说接口要 KISS,要简单自然,这里很重要的一点是符合语言和社区的惯例。如果某类业务在语言中已经有约定俗成的接口,我们尽可能沿用相同的接口语义。
|
||||
|
||||
模块的环境依赖
|
||||
|
||||
接口的另一种含义是模块对依赖环境的抽象,也就是模块与模块之间的契约。我们大部分情况下提到的接口,指的是这一点。
|
||||
|
||||
模块的环境依赖,也分两种,一种是使用界面依赖,一种是实现依赖。所谓使用界面依赖是指用户在使用该模块的使用界面时自然涉及的。所谓实现依赖则是指模块当前实现方案中涉及到的组件,它带来的依赖条件。如果我换一种实现方案,这类依赖可能就不再存在,或者变成另外的依赖。
|
||||
|
||||
在环境依赖上,我们遵循的是 “最小依赖原则”,或者叫 “最少知识原则(Least Knowledge Principle,LKP)”,去尽可能发现模块中多余的依赖。
|
||||
|
||||
具体到细节,使用界面依赖与实现依赖到处置方式往往还是有所不同。
|
||||
|
||||
从使用界面依赖来说,我们接口定义更多考虑的往往是对参数的泛化与抽象,以便让我们可以适应更广泛的场景。
|
||||
|
||||
比如,我们前面谈到 IO 系统的时候,把存盘与读盘的接口从 *.os.File 换成 io.Reader、io.Writer,以获得更强的通用性,比如对剪贴板的支持。
|
||||
|
||||
类似的情况还有很多,一个接口的参数类型稍加变化,就会获得更大的通用性。再比如,对于上面 mockhttp.v1 中 rpc.Client 这个接口就存在多余的依赖,改为 http.Client 会更好一些。
|
||||
|
||||
不过有的时候,我们看起来从接口定义似乎更加泛化,但是实际上却是场景的收紧,这需要特别注意避免的。比如上面 mockhttp.v1 的接口:
|
||||
|
||||
func Bind(host string, service interface{})
|
||||
|
||||
|
||||
与 mockhttp.v2 的接口:
|
||||
|
||||
func ListenAndServe(host string, service http.Handler)
|
||||
|
||||
|
||||
看似 v1 版本类型用的是 interface{},形式上更加泛化,但实际上 v1 版本有更强的假设,它内部通过反射机制实现了 HTTP 服务器请求的路由分派。而 v2 版本对 service 则用的是 HTTP 服务器的通用接口,是更加恰如其分的描述方式。
|
||||
|
||||
当然,在接口参数的抽象上,也不适合过度。如果某种泛化它不会发生,那就是过度设计。不要一开始就把系统设计得非常复杂,而陷入“过度设计”的深渊。应该让系统足够的简单,而却又不失扩展性,这其中的平衡完全依赖你对业务的理解,它是一个难点。
|
||||
|
||||
聊完使用界面依赖,我们接着聊实现依赖。
|
||||
|
||||
从模块实现的角度,我们环境依赖有两个选择:一个是直接依赖所基于的组件,一个是将所依赖的组件所有被引用的方法抽象成一个接口,让模块依赖接口而不是具体的组件。
|
||||
|
||||
那么,这两种方式应该怎么选择?
|
||||
|
||||
我的建议是,大部分情况下应该选择直接依赖组件,而不必去抽象它。
|
||||
|
||||
如无必要,勿增实体。
|
||||
|
||||
如果我们大量抽象所依赖的基础组件,意味着我们系统的可配置性(Configurable)更好,但学习成本也更高。
|
||||
|
||||
什么时候该当考虑把依赖抽象化?
|
||||
|
||||
其一,在需要提供多种选择的时候。比较典型的是日志的 Logger 组件。对于绝大部分的业务模块,都并不希望绑定 Logger 的选择,把决策权交给使用方。
|
||||
|
||||
但是有的时候,在这一点上过度设计也会比较常见。比如,不少业务模块会选择抽象对数据库的依赖,以便于在 MySQL 和 MongoDB 之间自由切换。但这种灵活性绝大部分情况下是一种过度设计。选择数据库应该是非常谨慎严谨的行为。
|
||||
|
||||
其二,在需要解除一个庞大的外部系统的依赖时。有时候我们并不是需要多个选择,而是某个外部依赖过重,我们测试或其他场景可能会选择 mock 一个外部依赖,以便降低测试系统的依赖。
|
||||
|
||||
其三,在依赖的外部系统为可选组件时。这个时候模块会实现一个 mock 的组件,并在初始化时将接口设置为 mock 组件。这样的好处是,除非用户关心,否则客户可以当模块不存在这个可选的配置项,这降低了学习门槛。
|
||||
|
||||
整体来说,对模块的实现依赖进行接口抽象,本质是对模块进行配置化,增加很多配置选项,这样的配置化需要谨慎,适可而止。
|
||||
|
||||
结语
|
||||
|
||||
接口设计是一个老生常谈的话题。接口有分模块的使用界面和模块的环境依赖这两种理解。
|
||||
|
||||
对于模块的使用界面,我们推崇 KISS 原则,简单自然,符合业务表达的惯例。
|
||||
|
||||
对于模块的环境依赖,我们遵循的是 “最小依赖原则”,或者叫 “最少知识原则(Least Knowledge Principle,LKP)”,尽可能发现模块中多余的依赖。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “不断完善的架构范式”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
136
专栏/许式伟的架构课/64 不断完善的架构范式.md
Normal file
136
专栏/许式伟的架构课/64 不断完善的架构范式.md
Normal file
@ -0,0 +1,136 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
64 不断完善的架构范式
|
||||
64 | 不断完善的架构范式你好,我是七牛云许式伟。
|
||||
|
||||
我们在 “[62 | 重新认识开闭原则 (OCP)]” 这一讲中介绍了开闭原则。这篇内容非常非常重要,可以说是整个架构课的灵魂。
|
||||
|
||||
总结来说,开闭原则包含以下两层含义:
|
||||
|
||||
第一,模块的业务要稳定。模块的业务遵循 “只读” 设计,如果需要变化不如把它归档,放弃掉。这种模块业务只读的思想,是架构治理的基础哲学。我平常和小伙伴们探讨模块边界的时候,经常会说这样一句话:
|
||||
|
||||
|
||||
每一个模块都应该是可完成的。
|
||||
|
||||
|
||||
这实际上是开闭原则的业务范畴 “只读” 的架构治理思想的另一种表述方式。
|
||||
|
||||
第二,模块业务的变化点,简单一点的,通过回调函数或者接口开放出去,交给其他的业务模块。复杂一点的,通过引入插件机制把系统分解为 “最小化的核心系统+多个彼此正交的周边系统”。事实上回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。
|
||||
|
||||
上一讲我们介绍了接口设计。到此为止,我们的架构思维篇也已经基本接近尾声。可能有人会越来越奇怪,为什么我还是没有去聊那些大家耳熟能详的架构设计原则?
|
||||
|
||||
实际上,并不是这些架构设计原则不好,它们之中不乏精彩绝伦、振聋发聩的总结。比如:
|
||||
|
||||
|
||||
接口隔离原则(Interface Segregation Principle,ISP):一个模块与另一个模块之间的依赖性,应该依赖于尽可能小的接口。
|
||||
依赖倒置原则(Dependence Inversion Principle,DIP):高层模块不应该依赖于低层模块,它们应该依赖于抽象接口。
|
||||
无环依赖原则(Acyclic Dependencies Principle,ADP):不要让两个模块之间出现循环依赖。怎么解除循环依赖?见上一条。
|
||||
组合/聚合复用原则(Composition/Aggregation Reuse Principle,CARP):当要扩展功能时,优先考虑使用组合,而不是继承。
|
||||
高内聚与低耦合(High Cohesion and Low Coupling,HCLC):模块内部需要做到内聚度高,模块之间需要做到耦合度低。这是判断一个模块是在做一个业务还是多个业务的依据。如果是在做同一个业务,那么它所有的代码都是内聚的,相互有较强的依赖。
|
||||
惯例优于配置(Convention over Configuration,COC):灵活性会增加复杂性,所以除非这个灵活性是必须的,否则应尽量让惯例来减少配置,这样才能提高开发效率。如有可能,尽量做到 “零配置”。
|
||||
命令查询分离(Command Query Separation,CQS):读写操作要分离。在定义接口方法时,要区分哪些是命令(写操作),哪些是查询(读操作),要将它们分离,而不要揉到一起。
|
||||
关注点分离(Separation of Concerns,SOC):将一个复杂的问题分离为多个简单的问题,然后逐个解决这些简单的问题,那么这个复杂的问题就解决了。当然这条说了等于没说,难在如何进行分离,最终还是归结到对业务的理解上。
|
||||
|
||||
|
||||
这些都是很好很好的。但是,我们需要意识到的一点是,熟读架构思维并不足以让我们成为优秀的架构师。
|
||||
|
||||
要始终记住的一点是,我们做的是软件工程。软件工程的复杂性它自然存在,不会因为好的架构思维而消除。
|
||||
|
||||
所以虽然理解架构思维是非常重要的,但是架构师真正的武器库并不是它们。
|
||||
|
||||
那么架构师的武器库是什么?
|
||||
|
||||
这就要从 “架构治理” 开始谈起。
|
||||
|
||||
前面我们说过,“开闭原则” 推崇模块业务 “只读” 的思想,是很好的架构治理哲学。它告诉我们,软件是可以以 “搭积木” 的方式搭出来的。
|
||||
|
||||
核心的一点是,我们如何形成更多的 “积木”,即一个个业务只读、接口稳定、易于组合的模块。
|
||||
|
||||
所以,真正提高我们工程效率的,是我们的业务分解能力和历史积累的成果。
|
||||
|
||||
前面我们说过,架构分解中有两大难题:其一,需求的交织。不同需求混杂在一起,也就是存在所谓的全局性功能。其二,需求的易变。不同客户,不同场景下需求看起来很不一样,场景呈发散趋势。
|
||||
|
||||
在 “[61 | 全局性功能的架构设计]” 这一讲我们重点聊的是第一点。对于全局性功能怎么去拆解,把它从我们的业务中剥离出来,并无统一的解决思路。
|
||||
|
||||
但好的一点是,绝大部分全局性功能都会有很多人去拆解,并最终会被基础设施化。所以具体业务中我们会碰到的全局性功能并不会非常多。
|
||||
|
||||
比如,怎么做用户的鉴权?怎么保障软件 24 小时持续服务?怎么保障快速定位用户反馈的问题?这些需求和所有业务需求是交织在一起的,也足够普适,所以就会有很多人去思考对应的解决方案。
|
||||
|
||||
作为架构师,心性非常重要。
|
||||
|
||||
架构师需要有自己的信仰。我们需要坚持对业务进行正交分解的信念,要坚持不断地探索各类需求的架构分解方法。这样的思考多了,我们就逐步形成了各种各样的架构范式。
|
||||
|
||||
这些架构范式,并不仅仅是一些架构思维,而是 “一个个业务只读、接口稳定、易于组合的模块 + 组合的方法论”,它们才是架构师真正的武器库。
|
||||
|
||||
这个武器库包含哪些内容?
|
||||
|
||||
首先,它应该包括信息科技形成的基础架构。努力把前辈们的心血,变成我们自己真正的积累。光会用还不够,以深刻理解它们背后的架构逻辑,确保自己与基础架构最大程度上的 “同频共振”。
|
||||
|
||||
只有让基础架构完全融入自己的思维体系,同频共振,我们才有可能在架构设计需要的时候 “想到它们”。这一点很有趣。有些人看起来博学多才,头头是道,但是真做架构时完全想不到他的 “博学”。
|
||||
|
||||
从体系结构来说,这个基础架构包含哪些内容?
|
||||
|
||||
其一,基础平台。包括:冯·诺依曼体系、编程语言、操作系统。
|
||||
|
||||
其二,桌面开发平台。包括:窗口系统、GDI 系统、浏览器与小程序。当然我们也要理解桌面开发背后的架构逻辑,MVC 架构。
|
||||
|
||||
其三,服务端开发平台。包括:负载均衡、各类存储中间件。服务端业务开发的业务逻辑比桌面要简单得多。服务端难在如何形成有效的基础架构,其中大部分是存储中间件。
|
||||
|
||||
其四,服务治理平台。主要是以容器技术为核心的 DCOS(数据中心操作系统),以及围绕它形成的整个服务治理生态。这一块还在高速发展过程中,最终它将让服务端开发变得极其简单。
|
||||
|
||||
理解了这些基础架构,再加上你自己所处行业的领域知识,基本上设计出一个优秀业务系统,让它健康运行,持续不间断地向用户提供服务就不是问题。
|
||||
|
||||
读到这里,你可能终于明白,为什么这个架构课的内容结构是目前这个样子组织的。因为消化基础架构成为架构师自身的本领,远比消化架构设计原则,架构思维逻辑要难得多。
|
||||
|
||||
消化基础架构的过程,同时也是消化架构思维的过程。
|
||||
|
||||
把虚的事情往实里做,才有可能真正做好。
|
||||
|
||||
理解了基础架构,剩下的就是如何沉淀业务架构所需的武器库。这一般来说没有太统一的体系可以参考,如果有,大部分都会被基础设施化了。
|
||||
|
||||
所以,业务只能靠你自己的架构设计能力去构建它。而这,其实也是架构师的乐趣所在。
|
||||
|
||||
还没有被基础设施化但比较通用的,有一个大门类是数据相关的体系。数据是软件的灵魂。它可能包括以下这些内容:
|
||||
|
||||
|
||||
存盘与读盘(IO);
|
||||
文本处理;
|
||||
存储与数据结构;
|
||||
Undo/Redo;
|
||||
……
|
||||
|
||||
|
||||
我们在下一讲,会专门聊聊其中的 “文本处理” 这个子课题。
|
||||
|
||||
从完整性讲,我们的架构课并没有包括所有的基础架构。我们把话题收敛到了 “如何把软件跑起来,并保证它持续健康运行” 这件事情上。
|
||||
|
||||
但从企业的业务运营角度来说,这还远不是全部。“[54 | 业务的可支持性与持续运营]” 我们稍稍展开了一下这个话题。但要谈透这个话题,它会是另一本书,内容主题将会是 “数据治理与业务运营体系构建”。
|
||||
|
||||
我希望有一天能够完成它,但这可能要很久之后的事情了。它是我除架构课外的另一个心愿。
|
||||
|
||||
结语
|
||||
|
||||
我们在 “[56 | 服务治理篇:回顾与总结]” 这一讲,也就是第四章结束的时候,谈到我们下一章的内容时提到:
|
||||
|
||||
|
||||
我个人不太喜欢常规意义上的 “设计模式”。或者说,我们对设计模式常规的打开方式是有问题的。理解每一个设计模式,都应该放到它想要解决的问题域来看。所以,我个人更喜欢的架构范式更多的是 “设计场景” 的总结。“设计场景” 和设计模式的区别在于它有自己清晰的问题域定义,是一个实实在在的通用子系统。
|
||||
|
||||
是的,这些 “通用的设计场景”,才是架构师真正的武器库。如果我们架构师总能把自己所要解决的业务场景分解为多个 “通用的设计场景” 的组合,这就代表架构师有了极强的架构范式的抽象能力。而这一点,正是架构师成熟度的核心标志。
|
||||
|
||||
|
||||
结合今天这一讲我们聊的内容,相信你对这段话会有新的理解。
|
||||
|
||||
“开闭原则” 推崇模块业务 “只读” 的思想,是很好的架构治理哲学。它告诉我们,软件是可以以 “搭积木” 的方式搭出来的。核心的一点是,我们如何形成更多的 “积木”,即一个个业务只读、接口稳定、易于组合的模块。
|
||||
|
||||
结合今天这一讲的内容,相信你终于完全能理解我们这个架构课的内容组织为什么是现在你看到的样子了。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “架构范式:文本处理”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
472
专栏/许式伟的架构课/65 架构范式:文本处理.md
Normal file
472
专栏/许式伟的架构课/65 架构范式:文本处理.md
Normal file
@ -0,0 +1,472 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
65 架构范式:文本处理
|
||||
65 | 架构范式:文本处理你好,我是七牛云许式伟。
|
||||
|
||||
上一讲 “[64 | 不断完善的架构范式]” 我们提到架构师的武器库是不断完善的架构范式。今天我们围绕一个具体的问题域,看看我们日常能够积累什么样的经验和成果,来完善作为一个架构师的知识体系。
|
||||
|
||||
我们选择的问题是 “文本处理”。
|
||||
|
||||
计算机之所以叫计算机,是因为计算机的能力基本上就是“计算+I/O”两部分。I/O 只是为了让计算机与物理世界打交道,它也是为计算服务的。所以数据是软件的灵魂,数据处理是软件的能力。
|
||||
|
||||
今天我们聊的文本处理,不是通用的数据处理能力,而是收敛在数据的 I/O 上。这里说的文本,是指写入到磁盘的非结构化数据。它可能真的是文本内容,比如 HTML 文档、CSS 文档;也可能是二进制内容,比如 Word 文档、Excel 文档。文本处理则是指对这类非结构化数据的处理过程,常见文本处理的需求场景有:
|
||||
|
||||
|
||||
数据验证(Data Validation)。比如判断用户输入的文本是否合法,值的范围是否符合期望。
|
||||
数据抽取(Data Extraction)。比如从某 HTML 页面中抽取出结构化的机票信息(什么时间,从哪里出发,到哪里去,价格几何等等)。
|
||||
编译器(Compiler)。特殊地,在文本格式是某种语言的代码时,我们可以将文本编译成可执行的机器码,或虚拟机解释执行的字节码。当然我们也可以边解释文本的语义边执行。
|
||||
……
|
||||
|
||||
|
||||
从用户需求的角度来说,文本处理的需求场景是不可穷尽的。网络爬虫与搜索引擎需要文本处理,Office 软件需要文本处理,编程语言的编译器需要文本处理,网络协议解析需要文本处理,等等。
|
||||
|
||||
那么,怎么才能从这些多变的需求场景中,抽出正交分解后可复用的架构范式?
|
||||
|
||||
我们今天聊聊文本处理的通用思路。
|
||||
|
||||
我的文本处理技术栈演进
|
||||
|
||||
文本处理,很多人都会遇到,只不过大家各自遇到的场景不同。我这里先回顾下我个人遇到的文本处理场景。我总结了一个图,如下:
|
||||
|
||||
|
||||
|
||||
在 2000 年初,我作为实习生拿到的第一个任务,是金山电子表格自身的文件格式设计和 Excel 文件的读写。此后,我参与了多个版本的 Word 文件读写工作。为了便于分析 Excel 和 Word 文件的格式,我实现了 ExcelViewer 和 DocViewer 这两个文件格式查看器。
|
||||
|
||||
实际上这两个 Viewer 非常重要,因为它第一次让文件格式的理解过程用程序固化了下来。这非常利于知识传承。大家可以设想一下,假如没有 Viewer,那么后面接手的人基本上只能靠阅读 ExcelReader 和 DocReader 模块的代码来理解文件格式。
|
||||
|
||||
但是这有两个问题。其一,Reader 模块有大量的业务逻辑,对我们理解 Excel 和 Word 文件格式本身会造成干扰。其二,Reader 模块增加功能会比较慢,对于那些我们本身不支持的功能,或者我们还暂时来不及兼容的功能,是没有对应的解析代码的。
|
||||
|
||||
但是 Viewer 就不一样。我们会尽可能地把我们对 Excel 和 Word 的理解记录下来,成为稳定可传承的知识,而无需关心是否已经支持该功能。另外,从时间的维度来说,应该先有 Viewer,在理解了文件格式之后,再设计出 Reader 才比较合理。
|
||||
|
||||
这个时期的 ExcelViewer 和 DocViewer,它主要抽象出来的是界面呈现部分。具体 ExcelViewer 和 DocViewer 的代码不需要有一行涉及到界面。这有诸多好处。实际上可视化界面只是 ExcelViewer 和 DocViewer 的一种输出终端,它们同时也生成了一个纯文本结果到磁盘文件中。这有助于我们用常规的 diff 工具来对比两个文件的差异,从而加速我们对未知数据格式的了解。
|
||||
|
||||
但,此时的 ExcelViewer 和 DocViewer 并没有将文件格式的处理过程抽象出通用的模块。也可以说,还没有抽象出文本处理范式。
|
||||
|
||||
这个时期同期还有一个探索性的 WPS for Linux 项目。为了支持跨平台编译,我实现了一个简单的 mk 程序。这个程序区别于 Linux 标准化的 make 程序,没有那么复杂的逻辑需要理解。它的输入是一个类 Windows 平台的 ini 文件,里面只需要指定选择的编译器、相关的编译选项、源代码文件列表等,就可以进行编译。甚至源代码列表可以直接指定为从 Visual C++ 的项目配置 dsp 文件中抽取,极易使用。
|
||||
|
||||
这个 mk 程序除了要解析一个类 ini 的配置文件外,也会解析 C/C++ 源代码文件形成源代码文件的依赖表,以更好地支持增量编译。不只是源代码文件本身的修改会触发重新编译,任何依赖文件的修改也会触发重新编译。
|
||||
|
||||
同样地,这个时期的 mk 程序同样没有引入任何通用的文本处理范式。
|
||||
|
||||
此后大约在 2004 年,我开始在金山办公软件内部推 KSDN。KSDN 这个名字承自 MSDN,我们希望打造一个全局的文档系统,它自动从项目的源代码中提取并生成。每天日构建完毕后得到最新版本的 KSDN。
|
||||
|
||||
KSDN 处理的输入主要是 C++ 和 Delphi 源代码文件(当时的界面是 Delphi 写的),是纯文本的。这和 ExcelViewer、DocViewer 不同,他们的输入是二进制文件。
|
||||
|
||||
KSDN 第一次引入了一个通用的脚本,来表达我们想从源代码中抽取什么内容。整个 KSDN 处理单个源代码文件的工作原理可以描述为:
|
||||
|
||||
|
||||
通过文件后缀选择源代码文件的解析脚本,通过该脚本解析 C++ 或 Dephi 的源代码,并输出 XML 格式的文件;
|
||||
通过 XSLT 脚本,将 XML 文件渲染为一个或多个 HTML 文件。XSLT 全称是 Extensible Stylesheet Language Transformations(可扩展样式表转换),是 XML 生态中的一项技术。
|
||||
|
||||
|
||||
在 2006 年的时候,我决定实现 KSDN 2.0 版本。这个版本主要想解决第一个版本的脚本语法表达能力比较局限的问题。
|
||||
|
||||
于是 C++ 版本的 TPL(Text Processing Language)诞生了。它非常类似于 Boost Spirit,但功能要强大很多。它的项目主页为:
|
||||
|
||||
|
||||
https://github.com/xushiwei/tpl
|
||||
|
||||
|
||||
它依赖基础库 stdext,项目主页为:
|
||||
|
||||
|
||||
https://github.com/xushiwei/stdext
|
||||
|
||||
|
||||
C++ 版本的 TPL 支持的表达能力,已经完全不弱于 UNIX 经典的 LEX + YACC 组合,使用上却轻量很多。KSDN 2.0 的工作原理变成了:
|
||||
|
||||
|
||||
基于 TPL 将 C++ 或 Delphi 文件转为 json 格式;
|
||||
与 XSLT 类似地,我们引入了 JSPT,即以 json 为输入,PHP 为 formatter,将内容转为一个或多个 HTML 文件。
|
||||
|
||||
|
||||
这个过程非常通用,可以用于实现任意文件格式之间的变换。包括我们前面的 mk 程序,它本质上也是类 ini 文件格式变换到 Makefile 的过程,我们基于 TPL 很轻松就改造了一个 mk 2.0 版本。
|
||||
|
||||
2009 年的时候,我们基于 C++ 实现一个名为 CERL 的网络库,它和 Go 语言的 goroutine 类似,也是基于协程来实现高并发。在这个网络库中,我们定义了一个名为 SDL(Server Description Language)的语言来描述服务器的网络协议。很自然地,我们基于 TPL + JSPT 来实现了 SDL 文件的解析过程。
|
||||
|
||||
2011 年,七牛云成立,我们选择了 Go 语言作为技术栈的核心。在转 Go 语言后,除了 TPL,我个人沉淀的大部分 C++ 基础库都不再需要,因为它们往往已经被 Go 语言的标准库解决得很好。
|
||||
|
||||
在 2015 年的时候,出于某种原因我实现了一个网络爬虫,这个爬虫会在收到网页内容后,抽取网页中的结构化信息并存储起来。这个抽取信息的过程,最终导致 Go 语言版本 TPL 的诞生。它的项目主页为:
|
||||
|
||||
|
||||
https://github.com/qiniu/text
|
||||
|
||||
|
||||
为了验证 Go 语言版本 TPL 的有效性,我在实现了经典的 “计算器(Calculator)” 之余,顺手实现了一门语言,这就是 qlang。它的项目主页为:
|
||||
|
||||
|
||||
https://github.com/qiniu/qlang
|
||||
|
||||
|
||||
由于 Go 语言中实现泛型数据结构的需要,我给 qlang 实现了一个 embedded 版本,简称 eql。它是类似 erubis/erb 的东西。结合 go generate,它可以很方便地让 Go 支持模板(不是 html template,是指泛型)。
|
||||
|
||||
在 2017 年,出于 rtmp 网络协议理解的需要,我创建了 BPL(Binary Processing Language),它的项目主页为:
|
||||
|
||||
|
||||
https://github.com/qiniu/bpl
|
||||
|
||||
|
||||
区别于 TPL 的是,BPL 主要用于处理二进制文档。前面我们谈到 ExcelViewer 和 DocViewer 时说过,我们并没有建立任何通用的架构范式。这一直是我引以为憾的事情,所以 2006 年 C++ 版本的 TPL 诞生后就有过 BPL 相关的尝试。这里是尝试残留的痕迹:
|
||||
|
||||
|
||||
tpl/binary/*
|
||||
|
||||
|
||||
但是二进制文档的确很难,它的格式描述中通常有一定的条件判断逻辑,所以 BPL 背后需要依赖一门语言。在 qlang 诞生后,这个条件就得到了满足,这是最终 BPL 得以能够诞生的原因。
|
||||
|
||||
BPL 非常强大,它可以处理任意的二进制文件,也可以用于处理任意的 TCP 网络协议数据流。有了 BPL,我们最初的 ExcelViewer 和 DocViewer 可以轻松得以实现。关于 BPL 更详细的介绍,请参阅 https://github.com/qiniu/bpl 中的文档说明。
|
||||
|
||||
文本内容的处理范式
|
||||
|
||||
介绍了我个人文本处理的技术栈演进过程后,我们把话题重新回到架构范式。
|
||||
|
||||
首先,让我们把焦点放在文本内容的处理上。
|
||||
|
||||
文本内容的处理,有非常标准的方式。它通常分词法分析(Lex)和语法分析(Parser)两个阶段。UNIX 系的操作系统还提供了 lex 和 yacc 两个经典的程序来协助我们做文本文件的分析处理。
|
||||
|
||||
词法分析(Lex)通常由一个 Scanner 来完成,它负责将文本内容从字节流(Byte Stream)转为 Token 流(Token Stream)。我们以解析 Go 源代码的 Scanner 为例(参见 https://godoc.org/go/scanner),其 Scan 函数的原型如下:
|
||||
|
||||
type Scanner struct {
|
||||
Scan() (pos token.Pos, tok token.Token, lit string)
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
其使用范式如下:
|
||||
|
||||
import (
|
||||
"go/scanner"
|
||||
"go/token"
|
||||
)
|
||||
|
||||
func doScan(s *scanner.Scanner) {
|
||||
for {
|
||||
pos, tok, lit := s.Scan()
|
||||
if tok == token.EOF {
|
||||
break
|
||||
}
|
||||
...
|
||||
// pos 是这个 token 的位置
|
||||
// tok 是这个 token 的类型,见 https://godoc.org/go/token
|
||||
// lit 是这个 token 的文本内容
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Scanner 有时候也叫 Tokenizer。例如 Go 语言中 HTML 的 Tokenizer 类(参阅 https://godoc.org/golang.org/x/net/html)的原型如下:
|
||||
|
||||
type Token struct {
|
||||
Type TokenType
|
||||
DataAtom atom.Atom
|
||||
Data string
|
||||
Attr []Attribute
|
||||
}
|
||||
|
||||
type Tokenizer struct {
|
||||
Next() TokenType
|
||||
Err() error
|
||||
Token() Token
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
其使用范式如下:
|
||||
|
||||
import (
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func doScan(z *html.Tokenizer) error {
|
||||
for {
|
||||
if z.Next() == html.ErrorToken {
|
||||
// Returning io.EOF indicates success.
|
||||
return z.Err()
|
||||
}
|
||||
token := z.Token()
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
词法分析(Lex)过程非常基础,大部分情况下我们不会直接和它打交道。我们打交道的基本都是语法分析器,通常叫 Parser。而从Parser 的使用方式来说,分为 SAX 和 DOM 两种模型。SAX 模型基于事件机制,DOM 模型则基于结构化的数据访问接口。
|
||||
|
||||
前面我们已经多次分析过 SAX 与 DOM 的优劣,这里不再展开。通常来说,我们会倾向于采用 DOM 模型。这里我们还是以 Go 文法和 HTML 文法的解析为例。
|
||||
|
||||
先看 Go 文法的 Parser(参阅 https://godoc.org/go/parser),它的原型如下:
|
||||
|
||||
func ParseExpr(x string) (ast.Expr, error)
|
||||
|
||||
func ParseFile(
|
||||
fset *token.FileSet,
|
||||
filename string, src interface{},
|
||||
mode Mode) (f *ast.File, err error)
|
||||
|
||||
|
||||
这里看起来有点复杂的是 ParseFile,它输入的字节流(Byte Stream)可以是:
|
||||
|
||||
|
||||
scr != nil,且为 io.Reader 类型;
|
||||
src != nil,且为 string 或 []byte 类型;
|
||||
src == nil,filename 非空,字节流从 filename 对应的文件中读取。
|
||||
|
||||
|
||||
而 Parser 的输出则统一是一个抽象语法树(Abstract Syntax Tree,AST)。显然,它基于的是 DOM 模型。
|
||||
|
||||
我们再看 HTML 文法的 Parser(参阅 https://godoc.org/golang.org/x/net/html),它的原型如下:
|
||||
|
||||
func Parse(r io.Reader) (*Node, error)
|
||||
|
||||
|
||||
超级简单的基于 DOM 模型的使用接口,任何解释都是多余的。
|
||||
|
||||
那么,我前面提的 TPL(Text Processing Language)是做什么的呢?它实现了一套通用的 Scanner + Parser 的机制。首先是词法分析,也就是 Scanner,它负责将文本流转换为 Token 序列。简单来说,就是一个从 text []byte 到 tokens []Token 的过程。
|
||||
|
||||
尽管世上语言多样,但是词法非常接近,所以在词法分析这块 ,TPL 抽象了一个 Tokenizer 接口,方便用户自定义。TPL 也内置了一个与 Go 语言词法类似的 Scanner,只是做了非常细微的调整,增加了 ?、~、@ 等操作符。
|
||||
|
||||
TPL 的 Parser 通过类 EBNF 文法表达。比如一个浮点运算的计算器(Calculator),支持加减乘除、函数调用、常量(如 pi 等)的类 EBNF 文法如下:
|
||||
|
||||
term = factor *('*' factor/mul | '/' factor/quo | '%' factor/mod)
|
||||
|
||||
doc = term *('+' term/add | '-' term/sub)
|
||||
|
||||
factor =
|
||||
FLOAT/push |
|
||||
'-' factor/neg |
|
||||
'(' doc ')' |
|
||||
(IDENT '(' doc %= ','/ARITY ')')/call |
|
||||
IDENT/ident |
|
||||
'+' factor
|
||||
|
||||
|
||||
关于这个类 EBNF 文法,有以下补充说明:
|
||||
|
||||
|
||||
我们用 *G 和 +G 来表示重复,而不是用 {G}。要记住这条规则其实比较简单。在编译原理的图书中,我们看到往往是 G* 和 G+。但语言文法中除了 ++ 和 – 运算符,很少是后缀形式,所以我们选择改为前缀。
|
||||
我们用 ?G 来表示可选,而不是用 [G]。同上,只要能够回忆起编译原理中我们用 G? 表示可选,我们就很容易理解这里为什么可选是用 ?G 表示。
|
||||
我们直接用 G1 G2 来表示串接,而不是 G1, G2。
|
||||
我们用 G1 % G2 和 G1 %= G2 表示 G1 G2 G1 G2 … G1 这样的列表。其中 G1 % G2 和 G1 %= G2 的区别是前者不能为空列表,后者可以。在上面的例子中,我们用 doc %= , 表示函数的参数列表。
|
||||
我们用 G/action 表示在 G 匹配成功后执行 action 这个动作。action 最终是调用到 Go 语言中的回调函数。在上面这个计算器中大量使用了 G/action 文法。
|
||||
|
||||
|
||||
与 UNIX 实用程序 yacc 不同的是,TPL 中文法描述的脚本,与执行代码尽可能分离,以加业务语义的可读性。
|
||||
|
||||
从模型的归属来说,TPL 属于 SAX 模型。但 G/action 不一定真的是动作。在 extractor 模式下,G/action 被视为 G/marker,TPL 变成 DOM 模型。也就是说,此时 action 只是一个标记,用于形成输出的 DOM 树。
|
||||
|
||||
关于 TPL 更详细的介绍需要很长的篇幅,你可以参考 TPL Doc。
|
||||
|
||||
在文本内容处理的技术栈中,还有一个分支是正则表达式(Regular Expression)。在简单场景下,正则表达式是比较方便的,但是它的缺点也比较明显,可伸缩性和可读性都不强。
|
||||
|
||||
二进制内容的处理范式
|
||||
|
||||
接下来我们讨论二进制内容的通用处理范式。
|
||||
|
||||
二进制内容的处理过程整体来说,似乎比较 “容易”。如果要说出一点问题的话,那就是 “有点繁琐”。
|
||||
|
||||
还记得序列化机制吧?它基本上算得上二进制内容的 I/O 框架了。它看起来是这样的:
|
||||
|
||||
type Foo struct {
|
||||
A uint32
|
||||
B string
|
||||
C float64
|
||||
D Bar
|
||||
}
|
||||
|
||||
func readFoo(foo *Foo, ar *Archive) {
|
||||
readUint32(&foo.A, ar)
|
||||
readString(&foo.B, ar)
|
||||
readFloat64(&foo.C, ar)
|
||||
readBar(&foo.D, ar)
|
||||
}
|
||||
|
||||
|
||||
在 C++ 的操作符重载的支持下,这段代码看起来会更简洁一些:
|
||||
|
||||
Archive& operator>>(Archive& ar, Foo& foo) {
|
||||
ar >> foo.A >> foo.B >> foo.C >> foo.D;
|
||||
return ar;
|
||||
}
|
||||
|
||||
|
||||
当然,上面只是最基础的情形,所以看起来还比较简洁。但在考虑可选、重复、数组等场景,实际上并不会那么简单。比如对于数组,理想情况下代码是下面这样的:
|
||||
|
||||
type Foo struct {
|
||||
N uint16
|
||||
Bars []Bar // [N]Bar
|
||||
}
|
||||
|
||||
func readFoo(foo *Foo, ar *Archive) {
|
||||
readUint16(&foo.N, ar)
|
||||
readArray(&foo.Bars, int(foo.N), ar)
|
||||
}
|
||||
|
||||
|
||||
对于 Go 语言来说,这里我们想要的 readArray 并不存在。而在 C++ 则可以通过泛型来做到,我们示意如下:
|
||||
|
||||
template <class T>
|
||||
void readArray(T[]& v, int n, Archive& ar) {
|
||||
v = new T[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
ar >> T[i];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
呼唤一下 Go 语言的泛型吧。不过泛型大概率需要破坏 Go 的一些基础假设,比如不支持重载。所以 Go 的泛型之路不会那么容易。
|
||||
|
||||
回到序列化机制。常规意义的序列化,通常还提供了 Object 动态序列化与反序列化的能力。但是实际上这个机制属于过度设计。
|
||||
|
||||
为什么这么说?
|
||||
|
||||
因为 Object 动态序列化的确带来了一定的便捷性,但是这个便捷性的背后是让使用者放弃了对磁盘文件格式设计的思考。这是非常不正确的指导思想。
|
||||
|
||||
数据是软件的灵魂,文件是软件最重要的资产。
|
||||
|
||||
|
||||
文件 I/O 的序列化机制,最重要的是定义严谨的数据格式,而非提供任何出于便捷性考虑的智能。
|
||||
|
||||
|
||||
所以我们只需要保留序列化的形式就好了,任何额外的 “智能” 都是多余的。
|
||||
|
||||
基于这样的基本原则,稍作探究你就会发现,在数据结构清晰的情况下,其实整个序列化的代码是非常平庸的。假如我们参考 TPL 的类 EBNF 文法,定义以下这样一条规则:
|
||||
|
||||
Foo = {
|
||||
N uint16
|
||||
Bars [N]Bar
|
||||
}
|
||||
|
||||
|
||||
这样,我们就可以自动帮这里的 Foo 类型实现它的序列化代码了。
|
||||
|
||||
而这正是 BPL 诞生的灵感来源。
|
||||
|
||||
BPL 设计的核心思想是,不破坏 TPL 的 EBNF 文法的任何语义,把自己作为 TPL 的扩展。这就好比,如果我们把 TPL 看作 C 的话,BPL 就是 C++。所有 TPL 的功能,BPL 都应该具备而且行为一致。
|
||||
|
||||
我们以 MongoDB 的网络协议为例,看看 BPL 文法是什么样的:
|
||||
|
||||
document = bson
|
||||
|
||||
MsgHeader = {/C
|
||||
int32 messageLength; // total message size, including this
|
||||
int32 requestID; // identifier for this message
|
||||
int32 responseTo; // requestID from the original request (used in responses from db)
|
||||
int32 opCode; // request type - see table below
|
||||
}
|
||||
|
||||
OP_UPDATE = {/C
|
||||
int32 ZERO; // 0 - reserved for future use
|
||||
cstring fullCollectionName; // "dbname.collectionname"
|
||||
int32 flags; // bit vector. see below
|
||||
document selector; // the query to select the document
|
||||
document update; // specification of the update to perform
|
||||
}
|
||||
|
||||
OP_INSERT = {/C
|
||||
int32 flags; // bit vector - see below
|
||||
cstring fullCollectionName; // "dbname.collectionname"
|
||||
document* documents; // one or more documents to insert into the collection
|
||||
}
|
||||
|
||||
OP_QUERY = {/C
|
||||
int32 flags; // bit vector of query options. See below for details.
|
||||
cstring fullCollectionName; // "dbname.collectionname"
|
||||
int32 numberToSkip; // number of documents to skip
|
||||
int32 numberToReturn; // number of documents to return
|
||||
// in the first OP_REPLY batch
|
||||
document query; // query object. See below for details.
|
||||
document? returnFieldsSelector; // Optional. Selector indicating the fields
|
||||
// to return. See below for details.
|
||||
}
|
||||
|
||||
OP_GET_MORE = {/C
|
||||
int32 ZERO; // 0 - reserved for future use
|
||||
cstring fullCollectionName; // "dbname.collectionname"
|
||||
int32 numberToReturn; // number of documents to return
|
||||
int64 cursorID; // cursorID from the OP_REPLY
|
||||
}
|
||||
|
||||
OP_DELETE = {/C
|
||||
int32 ZERO; // 0 - reserved for future use
|
||||
cstring fullCollectionName; // "dbname.collectionname"
|
||||
int32 flags; // bit vector - see below for details.
|
||||
document selector; // query object. See below for details.
|
||||
}
|
||||
|
||||
OP_KILL_CURSORS = {/C
|
||||
int32 ZERO; // 0 - reserved for future use
|
||||
int32 numberOfCursorIDs; // number of cursorIDs in message
|
||||
int64* cursorIDs; // sequence of cursorIDs to close
|
||||
}
|
||||
|
||||
OP_MSG = {/C
|
||||
cstring message; // message for the database
|
||||
}
|
||||
|
||||
OP_REPLY = {/C
|
||||
int32 responseFlags; // bit vector - see details below
|
||||
int64 cursorID; // cursor id if client needs to do get more's
|
||||
int32 startingFrom; // where in the cursor this reply is starting
|
||||
int32 numberReturned; // number of documents in the reply
|
||||
document* documents; // documents
|
||||
}
|
||||
|
||||
OP_REQ = {/C
|
||||
cstring dbName;
|
||||
cstring cmd;
|
||||
document param;
|
||||
}
|
||||
|
||||
OP_RET = {/C
|
||||
document ret;
|
||||
}
|
||||
|
||||
Message = {
|
||||
header MsgHeader // standard message header
|
||||
let bodyLen = header.messageLength - sizeof(MsgHeader)
|
||||
read bodyLen do case header.opCode {
|
||||
1: OP_REPLY // Reply to a client request. responseTo is set.
|
||||
1000: OP_MSG // Generic msg command followed by a string.
|
||||
2001: OP_UPDATE
|
||||
2002: OP_INSERT
|
||||
2004: OP_QUERY
|
||||
2005: OP_GET_MORE // Get more data from a query. See Cursors.
|
||||
2006: OP_DELETE
|
||||
2007: OP_KILL_CURSORS // Notify database that the client has finished with the cursor.
|
||||
2010: OP_REQ
|
||||
2011: OP_RET
|
||||
default: {
|
||||
body [bodyLen]byte
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doc = *Message
|
||||
|
||||
|
||||
我们对比 MongoDB 官方的协议文档(参考 https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/),你会发现很有趣的一点是,我们 BPL 文法几乎和 MongoDB 官方采用的伪代码完全一致,除了一个小细节:在 BPL 中,我们用 {…} 表示采用 Go 语言结构体的文法,而 {/C … } 表示采用 C 语言结构体的文法。
|
||||
|
||||
当前 BPL 还只支持解释执行,但这只是暂时的。就像在 TPL 中我们除了动态解释执行外,也已经提供 tpl generator 来生成 Go 代码以静态编译执行。
|
||||
|
||||
要进一步了解 BPL 的功能,请参阅 https://github.com/qiniu/bpl。我们也还提供了不少具体 BPL 的样例,详细可参考:
|
||||
|
||||
|
||||
https://github.com/qiniu/bpl/tree/master/formats
|
||||
|
||||
|
||||
结语
|
||||
|
||||
文本处理是一个非常庞大的课题,本文详细解剖了我个人在这个领域下的经验总结。相信这些经验对你面对相关场景时会有帮助。
|
||||
|
||||
但是更重要的一点是,我们平常需要有意识去分析我们工作中遇到的业务场景,从中提炼通用的需求场景形成架构范式的积累。
|
||||
|
||||
如此,架构的正交分解思想方能得到贯彻。而我们的业务迭代,也就越来越容易。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “架构老化与重构”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
173
专栏/许式伟的架构课/66 架构老化与重构.md
Normal file
173
专栏/许式伟的架构课/66 架构老化与重构.md
Normal file
@ -0,0 +1,173 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
66 架构老化与重构
|
||||
66 | 架构老化与重构你好,我是七牛云许式伟。
|
||||
|
||||
在 “[64 | 不断完善的架构范式]” 这一讲中,我们强调了架构师在日常工作过程中不断积累和完善架构范式的重要性。而上一讲 “65 | 架构范式:文本处理” 则以我个人经历为例,介绍了文本处理领域的通用架构范式。
|
||||
|
||||
架构的老化
|
||||
|
||||
架构的功夫全在平常。
|
||||
|
||||
无论是在我们架构范式的不断完善上,还是应对架构老化的经验积累上,都是在日常工作过程中见功夫。我们不能指望有一天架构水平会突飞猛进。架构能力提升全靠平常一点一滴地不断反思与打磨得来。
|
||||
|
||||
今天我们要聊的话题是架构老化与重构。
|
||||
|
||||
架构老化源于什么?
|
||||
|
||||
在我们不断给系统添加各种新功能的时候,往往会遇到功能需求的实现方式不在当初框架设定的范围之内,于是很多功能代码逸出框架的范围之外。
|
||||
|
||||
这些散落在各处的代码,把系统绞得支离破碎。久而久之,代码就出现老化,散发出臭味。
|
||||
|
||||
代码老化的标志,是添加功能越来越难,迭代效率降低,问题却是持续不断,解决了一个问题却又由此生出好几个新问题。
|
||||
|
||||
在理想的情况下,如果我们坚持以 “最小化的核心系统 + 多个相互正交的周边系统” 这个指导思想来构建应用,那么代码就很难出现老化。
|
||||
|
||||
当然,这毕竟是理想情况。现实情况下,有很多原因会导致架构老化难以避免,比如:
|
||||
|
||||
|
||||
软件工程师的技术能力不行,以功能完成为先,不考虑项目的长期维护成本;
|
||||
公司缺乏架构评审环节,系统的代码质量缺乏持续有效的关注;
|
||||
需求理解不深刻,最初架构设计无法满足迭代发展的需要;
|
||||
架构迭代不及时,大量因为赶时间而诞生的补丁式代码;
|
||||
……
|
||||
|
||||
|
||||
那么,怎么应对架构老化?
|
||||
|
||||
这个问题可以从两个视角来看:
|
||||
|
||||
|
||||
该怎么重构系统,才能让我们的软件重新恢复活力?
|
||||
在重构系统之前,我们应该如何进行局部改善,如果增加新功能又应该如何考虑?
|
||||
|
||||
|
||||
我们先聊后者,毕竟重构系统听起来是一件系统性的工程。而添加新功能与局部调整则在日常经常发生。
|
||||
|
||||
老系统怎么添加新功能
|
||||
|
||||
先说说添加新功能。
|
||||
|
||||
正常来说,我们添加功能的时候,尤其是自己加入项目组比较晚,已经有大量的历史代码沉淀在那里的时候,通常我们应该把自己要添加的功能定位为周边功能。对于周边功能,往往考虑最多的点是如何少给核心系统添加麻烦,能够少改就少改。
|
||||
|
||||
但是,这其实还不够。实际上当我们视角放在周边系统的时候,其实它本身也应该被看作独立业务系统。这样看的时候,我们自然而然会有新的要求:如何让新功能的代码与既有系统解耦,能够不依赖尽量不依赖。
|
||||
|
||||
这个不依赖是有讲究的。
|
||||
|
||||
不依赖核心的含义是业务不依赖。新功能的绝大部分代码独立于既有业务系统,只有少量桥接的代码是耦合的。
|
||||
|
||||
实际上对于任何被正交分解的周边系统 B 与核心系统 A,理想情况我们最终得到的应该是三个模块:A、B(与 A 无关部分)、A 与 B 桥接代码(与 A 相关的部分)。虽然从归属来说,A 与 B 桥接代码我们通常也会放到 B 模块,但是它应该尽可能小,且尽可能独立于与核心系统无关的代码。
|
||||
|
||||
理解这一点至关重要。只有这样我们才能保护自己的投资,今天开发新功能的投入产出可以最大程度得以保留。未来,万一需要做重构,我们的重构成本也能够尽可能最小化。
|
||||
|
||||
不依赖的另一个重要话题是要不要依赖公司内部的基础库。这一点需要辩证来看,不能简单回答依赖或不依赖。完全不依赖意味着放弃生产力。
|
||||
|
||||
这里基本的判断标准是,成熟度越高的基础库越值得依赖。成熟度的评估依赖于个人经验,首先应该评估的是模块规格的成熟度,因为实现上的问题让时间来解决就行。模块规格是否符合你的预期,以及经过了多少用户使用的打磨,这些是评估成熟度的依据。
|
||||
|
||||
还是以我做办公软件时期的经历为例。从重构角度来说它很典型,既有的代码有几百万行。我第一个做读盘与存盘之外的新功能是电子表格的智能填充。这个功能比较常用,用户可以选择一个区域,然后移动鼠标到被选区域右下角,在鼠标变成十字时,按下鼠标左键不放并移动鼠标以进行单元格内容的自动填充。填充方向是上下左右都可以。
|
||||
|
||||
我怎么做这个功能?首先是实现一个基本纯算法的模块,输入一个值矩阵(可以是数值、日期,也可以是字符串等),要预测的序列个数,输出对应预测的值矩阵。为什么自动填充的方向在算法这里消失了?因为我们按填充方向构建值矩阵,而不是用户屏幕上直观看到的矩阵。
|
||||
|
||||
然后抽象了核心系统的两个接口,一个是取一个区域的单元格数据,包括值和格式,一个是设置一个单元格的值和格式。基于这个抽象接口,我们实现了完整的自动填充逻辑。
|
||||
|
||||
最后,是对接这个自动填充模块与既有的业务系统。从 Model 层来说,只需要在既有的业务系统包装对应要求的接口即可。而且取区域单元格、设置一个单元格的值,这些是非常通用的接口,无论既有系统长什么样,我们都可以轻松去实现所需接口。
|
||||
|
||||
这就是做新功能的思路,尽可能与既有系统剥离,从独立业务视角去实现业务,抽象对环境的依赖。最后,用最少量的对接代码把整个系统串起来。
|
||||
|
||||
架构的局部优化
|
||||
|
||||
聊完添加新功能,我们谈谈局部调整。它的目标是优化某个功能与核心系统的耦合关系。
|
||||
|
||||
局部调整看似收效甚微,但是它的好处是可以快速推动。而且,日拱一卒,如果我们能够坚持下来,最后的效果远比你想象得好。
|
||||
|
||||
它有两种常见做法。
|
||||
|
||||
一种是重写,或者叫局部重构。它相当于从系统中彻底移除掉与该功能相关的代码,重新写一份新的。这和开发一个新功能没什么两样,最多看看被移除的代码里面,有哪些函数设计比较合理,可以直接拿过来用,或者稍微重新包装一下能够让规格更合理的。
|
||||
|
||||
但是我们不能太热衷于做局部重构。局部重构一定要发生在你对这块代码的业务比较了解的情形,比如你已经维护过它一阵子了。
|
||||
|
||||
另外,局部重构一定要把老代码清理干净,不要残留一些不必要的代码在系统里面。剩下来的事情,完全可以参考我上面提的实现新功能的方法论来执行。
|
||||
|
||||
另一种是依赖优化。它关注的重心不是某项功能本身的实现,而是它与系统之间的关系。
|
||||
|
||||
依赖优化整体上做的是代码的搬运工。怎么搬代码?和删除代码类似,我们要找到和该功能相关的所有代码。但是我们做的不是删除,而是将散落在系统中的代码集中起来。我们把对系统的每处修改变成一个函数,比如叫 doXXX_yyyy。这里 XXX 是功能代号,yyyy 则依据这段搬走的代码语义命个名。
|
||||
|
||||
你可能觉得这个名字太丑了。但是某种程度来说这是故意的。它可以作为团队的约定俗成,代表此处待重新考虑边界。
|
||||
|
||||
不要理解错了,它不是说我们需要重新思考我们现在正在做代码优化的功能边界。它是说我们要重新考虑核心系统的边界。尤其是如果某个地方有好几个功能都加了 doXXX_yyyy 这样的调用,这就意味着这里需要提供一个事件机制,以便这些功能能够进行监听。而一旦我们做了这件事,你就发现核心系统变得更稳定了,不再需要因为添加功能而修改代码。而这不正是 “开闭原则(OCP)” 所追求的么?
|
||||
|
||||
回到我们要进行依赖优化的功能。集中了这个功能所有代码后,这个功能与系统的耦合也就清楚了。有多少个 doXXX_yyyy,就有多少对系统的伤害(参阅 “[58 | 如何判断架构设计的优劣?]” 中的伤害值计算)。
|
||||
|
||||
如果伤害值不大,代表耦合在合理范围,做到这一步暂时不再往下走是可接受的。如果耦合过多,那就意味着我们需要站在这个功能本身的业务视角看依赖的合理性了。如果不合理,可以考虑推动局部重构。
|
||||
|
||||
所以,局部重构不应该很盲目,而应依赖于基于 “伤害值” 的客观判断。习惯于在不理解的情况下就重构,这实在不太好。认同他人是很重要的能力修炼。况且作为架构师,事情优先级的排列是第一位的,有太多重要的事情值得去做。
|
||||
|
||||
依赖优化的好处比较明显。其一,工作量小,做的是代码搬运,不改变任何业务逻辑。其二,可以不必深入功能的细节,只需要找到该功能的所有相关代码,这是难点,然后把它们集中起来。
|
||||
|
||||
尽可能把我们认为非核心系统的功能,都基于依赖优化的方式独立出去。这样核心系统与周边系统的耦合就理清楚了。
|
||||
|
||||
依赖优化,可以把周边系统对核心系统的代码注入,整理得清清楚楚。这是事件机制的需求来源。
|
||||
|
||||
依赖优化也能够及时发现糟糕的模块,和核心系统藕断丝连,斩不断理还乱,这时我们就需要对这个功能进行局部重构。
|
||||
|
||||
核心系统的重构
|
||||
|
||||
完成这些,我们下一步,就要进入重构的关键阶段,进行核心系统重构。
|
||||
|
||||
对于一个积弊已久的系统,要想成功完成整体的重构是非常艰难的。
|
||||
|
||||
如果我们一上来就去重构核心系统,风险太高。一方面,牵一发而动全身,我们无法保证工程的交付周期。另一方面,没有谁对全局有足够的了解,重构会过于盲目,项目的执行风险难以把控。
|
||||
|
||||
确定要对核心系统进行重构,那么最高优先级是确定它的边界,也就是使用界面(接口)。
|
||||
|
||||
能够在不修改实现的情况下调整核心系统的使用界面到我们期望的样子是最好的。
|
||||
|
||||
周边系统对核心系统的依赖无非两类:一是核心系统的功能,表现为它提供的 DOM 接口;二是核心系统提供的事件,让周边系统能够介入它的业务流程。
|
||||
|
||||
对所有周边模块进行依赖优化的整理,细加分析后可以初步确定核心系统需要暴露的事件集合。
|
||||
|
||||
进一步要做的事情是把核心系统的 DOM 接口也抽象出来。这一步比较复杂。它包含两件事情:
|
||||
|
||||
|
||||
让周边系统对它的依赖,变成依赖接口,而非依赖实现;
|
||||
审视核心系统功能的 DOM 接口的合理性,明确出我们期望的接口设计。
|
||||
|
||||
|
||||
我们可以分步骤做。可以先做实现依赖到接口依赖的转变。这有点像前面依赖优化的工作。只不过它不是搬代码,而是把周边模块独立出去,将它与核心系统的依赖关系全部调整为接口。这样,不管抽离出来的 DOM 接口是否合理,至少它代表了当前系统的模块边界。
|
||||
|
||||
这一步做完,理论上 mock 一个核心系统出来和周边系统对接也是可行的。只不过可能这个 DOM 模型太大,要 mock 不那么容易。
|
||||
|
||||
接下来,就是最重要的时刻。
|
||||
|
||||
我们需要对核心系统的接口进行重新设计。这一步的难点在于:
|
||||
|
||||
第一,我们对业务的理解的确有了长足的进步。我们抽象的业务接口有了更加精炼符合业务本质的表达方式,而不是换汤不换药,否则我们就需要质疑这次重构的必要性。
|
||||
|
||||
第二,对周边系统切换到新接口的成本有充足的预计。对周边系统来说,这是从老接口过度到新接口的过程。虽然理论上让核心系统维护两套 DOM 接口同时存在,在技术上是可行的,但是这个过渡期不能太长,否则容易让人困惑,不清楚我们倡导的是什么。
|
||||
|
||||
完成了接口改造,剩下来就简单了。核心系统,每一个周边系统,彼此完全独立,可以单独调整和优化。嫌当前的核心系统太糟糕?那就搞搞。为什么可以这么轻松决策?因为就算我们要重新写核心系统,要做的事情也很收敛,不会影响到大局。
|
||||
|
||||
这不像那些系统边界分解得不清不楚的业务系统。要改核心系统的代码?
|
||||
|
||||
不要命了么?
|
||||
|
||||
结语
|
||||
|
||||
重构工作是很有技巧性的,很能培养一个人的架构能力。做多了,我们可以建立对代码耦合的条件反射,看一眼就知道架构是否合理。
|
||||
|
||||
但重构不是技巧性那么简单。
|
||||
|
||||
实际上从难度来说,重构比一个全新业务的架构过程要难得多。重构,不只是一个架构的合理性问题。它包含了架构合理性的考量,因为我们需要知道未来在哪里,我们迭代方向在哪里。
|
||||
|
||||
但重构的挑战远不只是这些。这是一个集架构设计(未来架构应该是什么样的)、资源规划与调度(与新功能开发的优先级怎么排)、阶段规划(如何把大任务变小,降低内部的抵触情绪和项目风险)以及持久战的韧性与毅力的庞大工程。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “架构思维篇:回顾与总结”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
179
专栏/许式伟的架构课/67架构思维篇:回顾与总结.md
Normal file
179
专栏/许式伟的架构课/67架构思维篇:回顾与总结.md
Normal file
@ -0,0 +1,179 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
67 架构思维篇:回顾与总结
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
到今天为止,我们第五章 “架构思维篇” 就要结束了。今天这篇文章我们对整章的内容做一个回顾与总结。
|
||||
|
||||
架构之道
|
||||
|
||||
架构思维篇的内容大体如下图所示。
|
||||
|
||||
|
||||
|
||||
在前面几个章节,我们已经陆续介绍了架构的全过程:
|
||||
|
||||
|
||||
[17 | 架构:需求分析 (上)]
|
||||
[18 | 架构:需求分析(下)-实战案例]
|
||||
[32 | 架构:系统的概要设计]
|
||||
[45 | 架构:怎么做详细设计?]
|
||||
|
||||
|
||||
但架构师面临的问题往往是错综复杂的。
|
||||
|
||||
给你一个明确的需求说明文档,干干净净地从头开始做 “需求分析”,做 “概要设计”,做模块的 “详细设计”,最后编码实现,这是理想场景。
|
||||
|
||||
现实中,大多数情况并不是这样。而是:你拿到了一份长长的源代码,加上少得可怜的几份过时的文档。然后被安排做一个新功能,或者改一个顽固缺陷(Bug)。
|
||||
|
||||
我们应该怎么做架构设计?
|
||||
|
||||
架构设计架构设计,设计为先,架构为魂。用架构的系统化和全局性思维来做设计。
|
||||
|
||||
整体来说,我们这个架构课的知识密度比较高。这在某种程度来说,也是一种必然结果,这是因为架构师需要 “掌控全局” 带来的。
|
||||
|
||||
所以这个架构课对大多数人而言,多多少少都会有一些盲点。如果遇到不能理解的地方,从构建完整知识体系的角度,建议通过其他的相关资料补上。当然也欢迎在专栏中提问。
|
||||
|
||||
相比一般的架构书籍来说,我们这一章架构思维篇的内容写得并不长。原因是架构思维的本源比架构规则重要。规则可能会因为环境变化而发生变化,会过时。但是架构思维的内核不会过时。
|
||||
|
||||
所以我们把关注的焦点放到了不变的思维内核上。
|
||||
|
||||
架构之道,是虚实结合之道。
|
||||
|
||||
我们要理论与实践相结合。架构设计不可能只需要熟读某些架构思维的理论,否则架构师早就满天飞了。如果两者只能取其一,我选实践。
|
||||
|
||||
从实悟虚,从虚就实,运用得当方得升华。这其实是最朴素的虚实结合的道理。对学架构这件事来说尤其如此。架构思维的感悟并不能一步到位,永远有进步的空间,需要我们在不断实践中感悟,升华自己的认知。
|
||||
|
||||
这个架构课内容的前四章为 “基础平台”、“桌面开发”、“服务端开发”、“服务治理”。
|
||||
|
||||
从内容上来说,由 “基础平台(硬件架构 / 编程语言 / 操作系统)”,到 “业务开发(桌面开发 / 服务端开发)”,再到 “业务治理(服务治理 / 技术支持 / 用户增长)”,基本上覆盖了信息技术主体骨架的各个方面。
|
||||
|
||||
有了骨架,就有了全貌,有了全局的视角。
|
||||
|
||||
前面四章,我们内容体系的侧重点放在了架构演变的过程。我们研究什么东西在迭代。这样,我们就不是去学习一个 “静态的”、“不变的” 信息技术的骨架,更重要的是我们也在学信息技术的发展历史。
|
||||
|
||||
有了基础平台,有了前端与后端,有了过去与未来,我们就有了真真正正的全貌。
|
||||
|
||||
我们博览群书,为的就是不拘于一隅,串联我们自身的知识体系,形成我们的认知框架。
|
||||
|
||||
信息科技的整体架构,与我们的应用软件架构息息相关。架构分基础架构和应用架构。选择基础架构也是构建业务竞争优势的重要组成部分。
|
||||
|
||||
从技能来说,我们可能把架构师能力去归结为:
|
||||
|
||||
|
||||
理需求的能力;
|
||||
读代码的能力;
|
||||
抽象系统的能力。
|
||||
|
||||
|
||||
但架构师的成长之旅,首先是心性修炼之旅。这包括:
|
||||
|
||||
|
||||
同理心的修炼,认同他人的能力;
|
||||
全局观的修炼,保持好奇心和学习的韧性;
|
||||
迭代能力的修炼,学会反思,学会在自我否定中不断成长。
|
||||
|
||||
|
||||
业务的正交分解
|
||||
|
||||
|
||||
架构就是业务的正交分解。每个模块都有它自己的业务。
|
||||
|
||||
这里我们说的模块是一种泛指,它包括:函数、类、接口、包、子系统、网络服务程序、桌面程序等等。
|
||||
|
||||
|
||||
这句话看似很简单,但是它太重要了,它是一切架构动作的基础。
|
||||
|
||||
架构行为的三步曲:“需求分析”、“概要设计”、模块的 “详细设计”,背后都直指业务的正交分解,只是逐步递进,一步步从模糊到越来越强的确定性,直至最终形成业务设计的完整的、精确无歧义的解决方案。
|
||||
|
||||
对业务进行分解得到的每一个模块来说,最重要的是模块边界,我们通常称之为 “接口”。
|
||||
|
||||
接口是业务的抽象,同时也是它与使用方的耦合方式。在业务分解的过程中,我们需要反复地审视模块的接口,发现其中 “过度的(或多余的)” 约束条件,把它提高到足够通用的、普适的场景来看。
|
||||
|
||||
在架构分解过程中有两大难题。
|
||||
|
||||
其一,需求的交织,不同需求混杂在一起。这是因为存在我们说的全局性功能。其二,需求的易变。不同客户,不同场景下需求看起来很不一样,场景呈发散趋势。
|
||||
|
||||
但无论如何,我们需要坚持作为一名架构师的信仰:
|
||||
|
||||
|
||||
任何功能都是可以正交分解的,即使我目前还没有找到方法,那也是因为我还没有透彻理解需求。
|
||||
|
||||
|
||||
怎么做业务分解?
|
||||
|
||||
业务分解就是最小化的核心系统,加上多个正交分解的周边系统。核心系统一定要最小化,要稳定。坚持不要往核心系统中增加新功能,这样你的业务架构就不可能有臭味。
|
||||
|
||||
所以业务做正交分解的第一件事情,就是要分出哪些是核心系统,哪些是周边子系统。核心系统构成了业务的最小功能集,而后通过不断增加新的周边功能,而演变成功能强大的复杂系统。
|
||||
|
||||
这里有一个周边功能对核心系统总伤害的经验公式:
|
||||
\[ \\sum_ {对每一处修改} log_2(修改行数+1)\]同一个周边功能相邻的代码行算作一处修改。不同周边功能的修改哪怕相邻也算作多处。
|
||||
|
||||
这个公式核心想表达的含义是:修改处数越多,伤害越大。对于每一处修改,鼓励尽可能减少到只修改一行,更多代码放到周边模块自己那里去。
|
||||
|
||||
在 “[62 | 重新认识开闭原则 (OCP)]” 这一讲我们介绍了开闭原则。它非常非常重要,可以说是整个架构课的灵魂。总结来说,开闭原则包含以下两层含义:
|
||||
|
||||
第一,模块的业务要稳定。模块的业务遵循 “只读” 设计,如果需要变化不如把它归档,放弃掉。这种模块业务只读的思想,是很好的架构治理的基础哲学。
|
||||
|
||||
这告诉我们,软件是可以以 “搭积木” 的方式搭出来的。核心的一点是,我们如何形成更多的 “积木”,即一个个业务只读、接口稳定、易于组合的模块。
|
||||
|
||||
我平常和小伙伴们探讨架构时,也经常说这样一句话:
|
||||
|
||||
|
||||
每一个模块都应该是可完成的。
|
||||
|
||||
|
||||
这实际上是开闭原则业务范畴 “只读” 的架构治理思想的另一种表述方式。
|
||||
|
||||
要坚持不断地探索各类需求的架构分解方法。这样的思考多了,我们就逐步形成了各种各样的架构范式。这些架构范式,并不仅仅是一些架构思维,而是 “一个个业务只读、接口稳定、易于组合的模块 + 组合的方法论”,它们才是架构师真正的武器库。
|
||||
|
||||
第二,模块的业务变化点,简单一点的,通过回调函数或者接口开放出去,交给其他的业务模块。复杂一点的,通过引入插件机制把系统分解为 “最小化的核心系统+多个彼此正交的周边系统”。回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。
|
||||
|
||||
领域理解
|
||||
|
||||
|
||||
应对业务需求的变化,最好的结构就是: 最小化的核心系统+多个彼此正交的周边系统。
|
||||
|
||||
|
||||
但是光理解了这一点,并不足以根本性地改变你的架构能力,因为这里面最难的是领域理解。所以需求分析很关键。怎么做需求分析?这一点要讲透真的很难。
|
||||
|
||||
我们用的是笨方法。把整个信息科技的演进史讲了一遍。
|
||||
|
||||
我们用穷举的方式来讲信息科技的半部演进史。为什么我说是半部?整个信息科技的发展,我们把它分为程序驱动和数据驱动两个阶段。
|
||||
|
||||
程序驱动的本质,是自动化的极致。以前,自动化是非常机械的,要完成自动化需要极大的难度。但是,软件的出现让自动化成为一种普惠价值,这是信息科技的上半部演进史带来的核心收益。
|
||||
|
||||
但到了数据驱动,事情就变了。我们甚至有了新的专有名词,比如 “智能时代”,或者 “DT 时代”。很多人想到智能,想到的是深度学习,想到的是机器视觉。但其实这非常片面。马云把上半场叫 IT,下半场叫 DT(数据科技),非常形象而且深刻。
|
||||
|
||||
我们的架构课,把话题收敛到了 “如何把软件跑起来,并保证它持续健康运行” 这件事情上。
|
||||
|
||||
但从企业的业务运营角度来说,这还远不是全部。“[54 | 业务的可支持性与持续运营]” 我们稍稍展开了一下这个话题。但要谈透这个话题,它会是另一本书,内容主题将会是 “数据治理与业务运营体系构建”。
|
||||
|
||||
我希望有一天能够完成它,但这可能要很久之后的事情了。
|
||||
|
||||
结语
|
||||
|
||||
今天我们对本章内容做了概要的回顾,“架构思维篇” 到此就结束了。理解了本章的内容,对于如何构建一个高度可扩展的软件架构你就有了基本的认知。
|
||||
|
||||
但不要让自己仅仅停留在认知上,需要多多实践。
|
||||
|
||||
架构的功夫全在平常。
|
||||
|
||||
无论是在我们架构范式的不断完善上,还是应对架构老化的经验积累上,都是在日常工作过程中见功夫。我们不能指望有一天架构水平会突飞猛进。架构能力提升全靠平常一点一滴地不断反思与打磨得来。
|
||||
|
||||
在应对架构老化这件事情上,不要轻率地选择进行全局性的重构。要把功夫花在平常,让重构在润物细无声中发生。
|
||||
|
||||
从难度来说,全局性的重构比一个全新业务的架构过程要难得多。重构,不只是一个架构的合理性问题。它包含了架构合理性的考量,因为我们需要知道未来在哪里,我们迭代方向在哪里。
|
||||
|
||||
但重构的挑战远不只是这些。这是一个集架构设计(未来架构应该是什么样的)、资源规划与调度(与新功能开发的优先级怎么排)、阶段规划(如何把大任务变小,降低内部的抵触情绪和项目风险)以及持久战所需的韧性与毅力的庞大工程。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们正式开始进入第六章:软件工程篇。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
98
专栏/许式伟的架构课/68软件工程的宏观视角.md
Normal file
98
专栏/许式伟的架构课/68软件工程的宏观视角.md
Normal file
@ -0,0 +1,98 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
68 软件工程的宏观视角
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
软件工程
|
||||
|
||||
今天开始,我们进入第六章,谈谈软件工程。
|
||||
|
||||
我理解的架构师的职责其实是从软件工程出发的。也许大家都学过软件工程,但如果我们把软件工程这门课重新看待,这门学科到底谈的是什么?是软件项目管理的方法论?
|
||||
|
||||
无论如何,软件工程是一门最年轻的学科,相比其他动辄跨世纪的自然科学而言,软件工程只有 50 年的历史。这门学科的实践太少了,任何一门学科的实践时间短的话,都很难沉淀出真正高效的经验总结,因为这些总结通常都是需要很多代人共同推动来完成的。
|
||||
|
||||
为什么说它只有 50 年时间呢?
|
||||
|
||||
我们先来看看 C 语言,一般意义上来说,我们可能认为它是现代语言的开始。C 语言诞生于 1970 年,到现在是 49 年。再看 Fortran,它被认定为是第一个高级语言,诞生于 1954 年,那时候主要面向的领域是科学计算。Fortran 的程序代码量普遍都还不大,量不大的时候谈不上工程的概念。
|
||||
|
||||
这也是我为什么说软件工程这门学科很年轻,它只有 50 岁。对于这样一个年轻的学科,我们对它的认知肯定还是非常肤浅的。
|
||||
|
||||
我在这个架构课的序言 “[开篇词 | 怎样成长为优秀的软件架构师?]” 一上来就做了软件工程和建筑工程的对比。通过对比我们可以发现,二者有非常大的区别,具体在于两点:
|
||||
|
||||
其一,不确定性。为什么软件工程有很大的不确定性?大部分大型的软件系统都有几千甚至几万人的规模,而这几千几万人中,却没有两个人的工作是重复的。
|
||||
|
||||
虽然大家都在编程,但是编程的内容是不一样的。每个人昨天和今天的工作也是不一样的,没有人会写一模一样的代码,我们总是不停地写新的东西,做新的工作。这些东西是非常不同的,软件工程从事的是创造性的工作。
|
||||
|
||||
大家都知道创造是很难的,创造意味着会有大量的试错,因为我们没有做过。大部分软件的形成都是一项极其复杂的工程,它们远比传统的工程复杂得多,无论是涉及的人力、时间还是业务的变数都要多很多。这些都会导致软件工程有非常大的不确定性。
|
||||
|
||||
其二,快速变化。建筑工程在完工以后就结束了,基本上很少会进行变更。但在软件工程里,软件生产出来只是开始。只要软件还在服务客户中,程序员们的创造过程就不会停止,软件系统仍然持续迭代更新,以便形成更好的市场竞争力。
|
||||
|
||||
这些都与传统建筑工程的模式大相径庭。一幢建筑自它完成之后,所有的变化便主要集中在一些软装的细节上,很少会再发生剧烈的变动,更不会持续地发生变动。但软件却不是这样,它从诞生之初到其生命周期结束,自始至终都在迭代变化,从未停止。
|
||||
|
||||
以上这两点都会导致软件工程区别于传统意义上的所有工程,有非常强的管理难度。过去那么多年,工业界有非常多的工程实践,但是所有的工程实践对软件工程来说都是不适用的,因为二者有很大的不一样。
|
||||
|
||||
今天如果我们站在管理的视角再看软件工程的话,我们知道管理学谈的是确定性。管理学本身的目的之一就是要抑制不确定性,产生确定性。
|
||||
|
||||
比如,开发工期、时间成本是否能确定。比如,人力成本、研发成本以及后期运维的成本是否能确定。
|
||||
|
||||
所以,软件项目的管理又期望达到确定性。但软件工程本身是快速变化的,是不确定的。这就是软件工程本身的矛盾。我们的目标是在大量的不确定性中找到确定性,这其实就是软件工程最核心的点。
|
||||
|
||||
架构师的职责
|
||||
|
||||
如果用 “瀑布模型” 的方式来表达,现代软件工程的全过程大体如下:
|
||||
|
||||
|
||||
|
||||
从开始的需求与历史版本缺陷,到新版本的产品设计,到架构设计,到编码与测试,到最终的产品发布,到线上服务的持续维护。
|
||||
|
||||
贯穿整个工程始终的,还有不变的团队分工与协同,以及不变的质量管理。
|
||||
|
||||
更为重要的是,这个过程并不是只发生一遍,而是终其生命周期过程中,反复迭代演进。
|
||||
|
||||
它是一个生命周期往往以数年甚至数十年计的工程。对于传统工程,我们往往也把一个工程称为项目,项目工程。但软件工程不同,虽然我们平常也有项目的概念,但软件工程并不是一个项目,而是无数个项目。每个项目只是软件工程的一个里程碑(Milestone)。
|
||||
|
||||
所以,光靠把控软件工程师的水平,依赖他们自觉保障工程质量,是远远不够的。软件工程是一项非常复杂的系统工程,它需要依赖一个能够掌控整个工程全局的团队,来规划和引导整个系统的演变过程。这个团队就是架构师团队。
|
||||
|
||||
软件架构师的职责,并不单单是我们通常理解的,对软件系统进行边界划分和模块规格的定义。从根本目标来说,软件架构师要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。
|
||||
|
||||
因此,虽然架构师的确是一个技术岗,但是架构师干的事情,并不是那么纯技术。
|
||||
|
||||
首先是用户需求的解读。怎么提升需求分析能力,尤其是需求演进的预判能力?它无关技术,关键是心态,心里得装着用户。除了需要 “在心里对需求反复推敲” 的严谨态度外,对用户反馈的尊重之心也至关重要。
|
||||
|
||||
其次是产品设计。产品边界的确立过程虽然是产品经理主导,但是架构师理应深度参与其中。原因在于,产品功能的开放性设计不是一个纯粹的用户需求问题,它通常涉及技术方案的探讨。因此,产品边界的确立不是一个纯需求,也不是一个纯技术,而是两者合而为一的过程。
|
||||
|
||||
以上两点,是架构本身的专业性带来的,在前面五章中已经谈过很多,我们这里不再展开。在本章中,我们更多是从工程本身出发。这些话题是因软件工程的工程性而来,属于工程管理的范畴,但它们却又通常和架构师的工作密不可分。
|
||||
|
||||
这里面最为突出但也非常基础的,是贯穿软件工程始终的 “团队分工与协同” 问题、“软件的质量管理” 问题。从 “团队分工与协同” 来说,话题可以是团队的目标共识,也可以是做事方式的默契,各类规范的制定。从 “软件的质量管理” 来说,话题可能涉及软件的版本发布,质量保障的过程体系等等。
|
||||
|
||||
从更宏观的视角看,我们还涉及人力资源规划的问题。什么东西应该外包出去,包给谁?软件版本的计划是什么样的,哪些功能先做,哪些功能后做?
|
||||
|
||||
看起来,这些似乎和架构师的 “本职工作” 不那么直接相关。但是如果你认同架构师的职责是 “对软件工程的执行结果负责”,那么就能够理解为什么你需要去关注这些内容。
|
||||
|
||||
结语
|
||||
|
||||
软件工程本身是一个非常新兴、非常复杂的话题。可能需要再花费 50 年这样漫长的时间才能形成更清晰的认知(例如,我们第四章 “服务治理篇” 专门探讨了现代软件工程全过程最后一个环节 “线上服务管理” 这个话题)。
|
||||
|
||||
作为架构课的一部分,这一章我们将主要精选部分与架构师的工作关系密切的话题来进行讨论,主要包括:
|
||||
|
||||
|
||||
团队的共识管理;
|
||||
如何阅读别人的代码;
|
||||
怎么写设计文档;
|
||||
发布单元与版本管理;
|
||||
软件质量管理:单元测试、持续构建与发布;
|
||||
开源、云服务与外包管理;
|
||||
软件版本迭代的规划;
|
||||
软件工程的未来。
|
||||
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “团队的共识管理”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
120
专栏/许式伟的架构课/69 团队的共识管理.md
Normal file
120
专栏/许式伟的架构课/69 团队的共识管理.md
Normal file
@ -0,0 +1,120 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
69 团队的共识管理
|
||||
69 | 团队的共识管理你好,我是七牛云许式伟。
|
||||
|
||||
软件工程是一项团体活动,大家有分工更有协同。不同的个体因为能力差别,可以形成十倍以上的生产力差距。而不同团体更是如此,他们的差距往往可以用天壤之别来形容。
|
||||
|
||||
这差距背后的原因,关乎的是协同的科学。
|
||||
|
||||
团队共识
|
||||
|
||||
有的团体像一盘散沙,充其量可以叫团伙。有的团体则有极强的凝聚力,整个团队上下同心,拧成一股绳,这种团体才是高效率组织,是真正意义上的团队。
|
||||
|
||||
团队靠什么上下同心?靠的是共识。
|
||||
|
||||
那么,什么是团队的共识?
|
||||
|
||||
团队的共识分很多层次。其一,团队是不是有共同的目标。其二,团队是不是有共同的行事做人的准则。其三,对产品与市场的要与不要,以及为什么要或为什么不要,是否已达成一致。其四,对执行路径有没有共同的认知。其五,有没有团队默契,是否日常沟通交流很多地方不必赘述,沟通上一点即透。
|
||||
|
||||
一个团体如果缺乏共同的目标,那么它最多能够算得上是一个团伙,而不能称之为团队。
|
||||
|
||||
团队的目标也分很多层次。为什么很多企业都会谈他们的使命和愿景,是因为它是这个企业作为一个团队存在的意义,是企业所有人共同的长远目标。
|
||||
|
||||
人是愿景型动物,需要看到未来。越高级的人才越在乎团队存在的意义。所以高科技公司的人才通常只能去影响,而不是像一些人心中理解的那样,认为管理是去控制。
|
||||
|
||||
愿景是一种心力。人有很强的主观能动性。一旦人相信企业的使命与愿景,员工就变得有很强烈的使命感,有强烈的原动力。员工的行为方式也就会潜移默化发生变化。
|
||||
|
||||
不过,有共同的远景目标的团队仍然有可能走向分裂。
|
||||
|
||||
中国有句古话说得好:“道不同,不相为谋”。团队有没有相同的价值观,有没有相同的行事做人的准则,这些更根本性的基础共识,极有可能会成为压垮团队的稻草。
|
||||
|
||||
共识大于能力。如果一个人有很强的个人能力,但是却和团队没有共同的愿景,或者没有共同的价值观,那么能力越大产生的破坏性也就越大。
|
||||
|
||||
怎么达成共识?
|
||||
|
||||
团队有了共同的使命、愿景与价值观,就有了共同努力把一件事情干成的最大基础。然而,这并不代表这个团队就不会遇到共识问题。
|
||||
|
||||
团队仅有远期的目标是不够的,还要有中短期的目标。企业的使命和愿景需要由一个个的战略行动来落地。我们的产品定位怎么样,选择哪些细分市场去切入,这些同样需要团队达成共识。
|
||||
|
||||
怎么去达成共识?
|
||||
|
||||
越 “聪明” 的团队负责人,往往越容易忽视达成共识的难度。他们通常会召开会议,然后把自己的想法说给大家听。半个小时后,兄弟们迷茫地回去了。
|
||||
|
||||
在团队还小的时候,这种简单共识的方式很可能是可以奏效的,尤其是当团队负责人还能够一一去检查每个人的工作内容时,所有的理解偏差都能够得到比较及时的纠正。
|
||||
|
||||
但是团队规模稍微变大一些,这种简单共识突然就失效了。“我明明已经告诉他们要做什么了。” 负责人有时候困惑于团队成员为什么并没有理解他的话。
|
||||
|
||||
这是因为他还并不理解真正的共识意味着什么。也没有对达成共识的难度有足够的认知。
|
||||
|
||||
让更多人参与到决策形成的过程现场,是更好的共识达成的方式。通过同步足够充分的信息,通过共创而非传达决策的方式让结论自然产生。
|
||||
|
||||
这个共创过程不必团队所有人都参与,但要确保所有影响落地的关键角色都在,并确保参与这个过程的人都能够产生思想的碰撞,而非做个吃西瓜群众。
|
||||
|
||||
契约与共识效率
|
||||
|
||||
目标与执行路径达成了共识,这还不够。我们还需要把共识表达出来,形成文字。
|
||||
|
||||
为什么这很重要?
|
||||
|
||||
因为共识之所以为共识,是因为它不是空中楼阁,不是口号,而是指导我们做战略选择的依据,指导我们平常行为的依据。
|
||||
|
||||
所以,共识就是团队协作的契约。契约的表达越是精确而无歧义,团队协作中主观能动性就越高,执行的效率也就越高。
|
||||
|
||||
对于架构过程同样如此。
|
||||
|
||||
架构过程实际上是团队共识形成与确认的过程。架构设计需要回答两个基本的问题:
|
||||
|
||||
|
||||
系统要做成什么样?
|
||||
怎么做?
|
||||
|
||||
|
||||
架构设计为什么叫架构设计,是因为架构师的工作中除了架构,还有设计。设计其实谈的就是 “系统要做成什么样”。
|
||||
|
||||
|
||||
设计高于架构。
|
||||
|
||||
|
||||
设计强调规格,架构强调实现。规格设计是架构过程的最高共识。所以,规格高于实现。我们用架构的全局性和系统性思维去做设计。
|
||||
|
||||
一些架构师乐衷于画架构图,把它当作是架构师最重要的工作内容。但架构图在共识的表达上并不太好。因为共识是需要精确的、无歧义的。而架构图显然并不精确。
|
||||
|
||||
对于一个工程团队来说,没有精确的共识很可怕。它可能导致不同模块的工作牛头不对马嘴,完全无法连接起来,但是这个风险没有被暴露,直到最后一刻里程碑时间要到了,要出版本了,大家才匆匆忙忙联调,临时解决因为架构不到位产生的 “锅”。
|
||||
|
||||
这时候人们的动作通常会走形。追求的不再是架构设计的好坏,而是打补丁,怎么把里程碑的目标实现了,别影响了团队绩效。
|
||||
|
||||
我们作个类比,这种不精确的架构,就好比建筑工程中,设计师画了一个效果图,没有任何尺寸和关键细节的确认,然后大家就分头开工了。最后放在一起拼接(联调),发现彼此完全没法对上,只能临时修修改改,拼接得上就谢天谢地了。是不是能够和当初效果图匹配?让老天爷决定吧。
|
||||
|
||||
更精确描述架构的方法是定义每个模块的接口。接口可以用代码表达,这种表达是精确的、无歧义的。架构图则只是辅助模块接口,用于说明模块接口之间的关联。
|
||||
|
||||
尊重契约,尊重共识精确的、无歧义的表达,非常非常重要。
|
||||
|
||||
绝大部分哪怕是非常优秀的架构师,在系统设计(也叫概要设计)阶段通常也只会形成系统的概貌,把子系统的划分谈清楚,把子系统的接口规格谈清楚。
|
||||
|
||||
但实际上概要设计阶段最好的状态并不是只有设计文档。
|
||||
|
||||
为了降低风险,系统设计阶段也应该有代码产出。
|
||||
|
||||
这样做有两个方面的目的。其一,系统的初始框架代码。也就是说,系统的大体架子已经搭建起来了。其二,原型性的代码来验证。一些核心子系统在这个阶段提供了 mock 的系统。
|
||||
|
||||
这样做的好处是,一上来我们就关注了全局系统性风险的消除,并且给了每个子系统或模块的负责人一个更具象且确定性的认知。
|
||||
|
||||
代码即文档。代码是理解一致性更强的文档。
|
||||
|
||||
结语
|
||||
|
||||
这一讲我们谈的是协同的科学。为什么有的团队效率极高,有的团队却进展缓慢,从背后的协同效率来说,共识管理是根因中的根因。
|
||||
|
||||
共识有非常多的层次。不同层次的共识处于完全不同的维度。它们都极其重要,且相互不可替代。当某个层次的共识出问题的时候,我们需要在相应的层次去解决它。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “怎么写设计文档”。原计划我们下一讲是 “如何阅读别人的代码”,但是我想先顺着共识这个话题谈问题谈清楚。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
152
专栏/许式伟的架构课/70怎么写设计文档?.md
Normal file
152
专栏/许式伟的架构课/70怎么写设计文档?.md
Normal file
@ -0,0 +1,152 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
70 怎么写设计文档?
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
在 “[68 | 软件工程的宏观视角]” 一讲中,我们用最基本的 “瀑布模型” 来描述现代软件工程的全过程,大体如下:
|
||||
|
||||
|
||||
|
||||
在这个过程中,有两个阶段非常关键:一个是 “产品设计”,一个是 “架构设计”。产品设计由产品经理主导,关注的是 “如何以产品特性来系统化地满足用户需求”。架构设计由架构师主导,关注的是 “业务系统如何系统化地进行分解与交付”。
|
||||
|
||||
“设计” 一词非常精妙。无论是 “产品设计”,还是 “架构设计”,其实谈的都是 “需求如何被满足” 这件事情的共识。无论是 “产品文档”,还是 “架构文档”,它们都是设计文档的一种,都有团队内及团队间的协同价值。
|
||||
|
||||
上一讲 “[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 的语法,如下:
|
||||
|
||||
# 请求
|
||||
post /v1/foo/bar json {...}
|
||||
|
||||
# 返回
|
||||
ret json {...}
|
||||
|
||||
|
||||
类似地,对于 MongoDB,我们可以直接用 MongoDB 的 JavaScript 脚本文法。对于 MySQL,则可以直接基于 SQL 语法。等等。
|
||||
|
||||
结语
|
||||
|
||||
前面在 “[45 | 架构:怎么做详细设计?]” 我们实际上已经大体介绍了模块级的设计文档怎么写。所以这一讲我们主要较为全面地补充了各类设计文档,包括产品设计、系统的概要设计等在细节上与模块设计文档的异同。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “如何阅读别人的代码”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
132
专栏/许式伟的架构课/71 如何阅读别人的代码?.md
Normal file
132
专栏/许式伟的架构课/71 如何阅读别人的代码?.md
Normal file
@ -0,0 +1,132 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
71 如何阅读别人的代码?
|
||||
71 | 如何阅读别人的代码?你好,我是七牛云许式伟。今天聊聊如何阅读别人的代码。
|
||||
|
||||
为何要读别人的代码?
|
||||
|
||||
我们去阅读别人的代码,通常会带有一定的目的性。完整把一个系统的代码 “读懂” 需要极大的精力。所以明确阅读代码的目标很重要,因为它决定了你最终能够为这事付出多大的精力,或者说成本。
|
||||
|
||||
大体来说,我们可以把目标分为这样几种类型:
|
||||
|
||||
|
||||
我要评估是否引入某个第三方模块;
|
||||
我要给某个模块局部修改一个 Bug(可能是因为使用的第三方模块遇到了一个问题,或者可能是你的上级临时指定了一个模块的 Bug 给你);
|
||||
我要以某个开源模块为榜样去学习;
|
||||
我要接手并长期维护某个模块。
|
||||
|
||||
|
||||
为什么要把我们的目标搞清楚?
|
||||
|
||||
因为读懂源代码真的很难,它其实是架构的反向过程。它类似于反编译,但是并不是指令级的反编译,而是需要根据指令反推更高维的思想。
|
||||
|
||||
我们知道反编译软件能够将精确软件反编译为汇编,因为这个过程信息是无损的,只是一种等价变换。但是要让反编译软件能够精确还原出高级语言的代码,这就比较难。因为编译过程是有损的,大部分软件实体的名字已经在编译过程中被去除了。当然,大部分编译器在编译时会同时生成符号文件。它主要用于 debug 用途。否则我们在单步跟踪时,debug 软件就没法显示变量的名字。
|
||||
|
||||
即使我们能够拿到符号文件,精确还原出原始的高级语言的代码仍然非常难。它需要带一定的模型推理在里面,通过识别出这里面我们熟悉的 “套路”,然后按照套路进行还原。
|
||||
|
||||
我们可以想像一下,“一个精确还原的智能反编译器” 是怎么工作的。
|
||||
|
||||
第一步,它需要识别出所采用的编程语言和编译器。这通常相对容易,一个非常粗陋的分类器就可以完成。尤其是很多编译器都有 “署名”,也就是在编程出的软件中带上自己签名的习惯。如果假设所有软件都有署名,那么这一步甚至不需要训练与学习。
|
||||
|
||||
第二步,通过软件的二进制,结合可选的符号文件(没有符号文件的结果是很多软件实体,比如类或函数的名字,会是一个随机分配的符号),加上它对该编译器的套路理解,就可以进行反编译了。
|
||||
|
||||
编译器的套路,就如同一个人的行为,持续进行观察学习,是可以形成总结的。这只需要反编译程序持续地学习足够多的该编译器所产生的样本。
|
||||
|
||||
我之所以拿反编译过程来类比,是希望我们能够理解,阅读源代码过程一方面是很难的,另一方面来说,也是需要有产出的。
|
||||
|
||||
有产出的学习过程,才是最好的学习方式。
|
||||
|
||||
|
||||
|
||||
那么阅读源代码的产出应该是什么?答案是,构建这个程序的思路,也就是架构设计。
|
||||
|
||||
理解架构的核心脉络
|
||||
|
||||
怎么做到?
|
||||
|
||||
首先,有文档,一定要先看文档。如果原本就已经有写过架构设计的文档,我们还要坚持自己通过代码一步步去反向进行理解,那就太傻了。
|
||||
|
||||
但是,一定要记住文档和代码很容易发生脱节。所以我们看到的很可能是上一版本的,甚至是最初版本的设计。
|
||||
|
||||
就算已经发生过变化,阅读过时的架构设计思想对我们理解源代码也会有极大的帮助作用。在这个基础上,我们再看源代码,就可以相互进行印证。当然如果发生了冲突,我们需要及时修改文档到与代码一致的版本。
|
||||
|
||||
看源代码,我们首先要做到的是理解系统的概要设计。概要设计的关注点是各个软件实体的业务范畴,以及它们之间的关系。有了这些,我们就能够理解这个系统的架构设计的核心脉络。
|
||||
|
||||
具体来说,看源码的步骤应该是怎样的呢?
|
||||
|
||||
首先,把公开的软件实体(模块、类、函数、常量、全局变量等)的规格整理出来。
|
||||
|
||||
这一步往往有一些现成的工具。例如,对 Go 语言来说,运行 go doc 就可以帮忙整理出一个自动生成的版本。一些开源工具例如 doxygen 也能够做到类似的事情,而且它支持几乎所有的主流语言。
|
||||
|
||||
当然这一步只能让我们找到有哪些软件实体,以及它们的规格是什么样的。但是这些软件实体各自的业务范畴是什么,它们之间有什么关系?需要进一步分析。
|
||||
|
||||
一般来说,下一步我会先看 example、unit test 等。这些属于我们研究对象的客户,也就是使用方。它们能够辅助我们理解各个软件实体的语义。
|
||||
|
||||
通过软件实体的规格、说明文档、example、unit test 等信息,我们根据这些已知信息,甚至包括软件实体的名字本身背后隐含的语义理解,我们可以初步推测出各个软件实体的业务范畴,以及它们之间的关系。
|
||||
|
||||
接下来,我们需要进一步证实或证伪我们的结论。如果证伪了,我们需要重新梳理各个软件实体之间的关系。怎么去证实或证伪?我们选重点的类或函数,通过看它们的源代码来理解其业务流程,以此印证我们的猜测。
|
||||
|
||||
当然,如果你能够找到之前做过这块业务的人,不要犹豫,尽可能找到他们并且争取一个小时左右的交流机会,并提前准备好自己遇到迷惑的问题列表。这会大幅缩短你理解整个系统的过程。
|
||||
|
||||
最后,确保我们正确理解了系统,就需要将结论写下来,形成文档。这样,下一次有其他同学接手这个系统的时候,就不至于需要重新再来一次 “反编译”。
|
||||
|
||||
理解业务的实现机制
|
||||
|
||||
业务系统的概要设计、接口理清楚后,通常来说,我们对这个系统就初步有谱了。如果我们是评估第三方模块要不要采纳等相对轻的目标,那么到此基本就可以告一段落了。
|
||||
|
||||
只有在必要的情况下,我们才研究实现机制。刚才我们谈到系统架构梳理过程中,我们也部分涉及了源代码理解。但是,需要明确的是,前面我们研究部分核心代码的实现,其目的还是为了确认我们对业务划分猜测的正确性,而不是为了实现机制本身。
|
||||
|
||||
研究实现是非常费时的,毕竟系统的 UserStory 数量上就有很多。把一个个 UserStory 的具体业务流程都研究清楚写下来,是非常耗时的。如果这个业务系统不是我们接下来重点投入的方向,就没必要在这方面去过度投入。
|
||||
|
||||
这时候目标就很重要。
|
||||
|
||||
如果我们只是顺带解决一下遇到的 Bug,无论是用第三方代码遇到的,还是上级随手安排的临时任务,我们自然把关注点放在要解决的 Bug 本身相关的业务流程上。
|
||||
|
||||
如果我们是接手一个新的业务系统,我们也没有精力立刻把所有细节都搞清楚。这时候我们需要梳理的是关键业务流程。
|
||||
|
||||
怎么搞清楚业务流程?
|
||||
|
||||
|
||||
程序 = 数据结构 + 算法
|
||||
|
||||
|
||||
还是这个基础的公式。要搞清楚业务流程,接下来要做的事情是,把这些业务流程相关的数据结构先理清楚。
|
||||
|
||||
数据结构是容易梳理的,类的成员变量、数据库的表结构,通常都有快速提取的方式。除了 MongoDB 可能会难一些,因为弱 schema 的原因,我们需要通过阅读代码的方式去理解 schema。更麻烦的是,我们不确定历史上经历过多少轮的 schema 变更,这通过最新版本的源代码很可能看不出来。一个不小心,我们就可能会处理到非预期 schema 的数据。
|
||||
|
||||
理清楚数据结构,事情就解决了大半。
|
||||
|
||||
剩下来就是理各个 UserStory 的业务流程,并给这些业务流程画出它的 UML 时序图。这个过程随时可以补充。所以我们挑选对我们当前工作最为相关的来做就好了。
|
||||
|
||||
最后,还是同样地,我们要及时把我们整理的结论写下来,变成架构文档的一部分。这样随着越来越多人去补充完整架构设计文档,才有可能把我们的项目从混沌状态解脱出来。
|
||||
|
||||
结语
|
||||
|
||||
对于任何一个项目团队来说,阅读代码的能力都极其重要。哪怕你觉得你的团队共识管理很好,团队很默契,大家的工程习惯也很好,也都很乐意写文档,但这些都替代不了阅读代码这个基础活动。
|
||||
|
||||
阅读代码是不可或缺的能力。
|
||||
|
||||
为什么这么说?因为:代码即文档,代码是理解一致性更强的文档。
|
||||
|
||||
另外,作为一个小补充,我们需要指出的一点是:阅读代码的结果,有时不一定仅仅是架构设计文档的补充与完善。我们有时也会顺手修改几行代码。
|
||||
|
||||
这是正常现象,而且应该被鼓励。为什么鼓励改代码?是因为我们鼓励随时随地消除臭味。改几行明显风格不太好的代码,是非常好的一件事情。
|
||||
|
||||
但是我们也要有原则。
|
||||
|
||||
其一,不做大的改动,比如限定单个函数内的改动不能超过 10 行。
|
||||
|
||||
其二,确保改动前后的语义完全一致。这种一致需要包括所有 corner case 上的语义一致,例如错误码,条件语句的边界等。
|
||||
|
||||
其三,不管多自信,有改动就需要补全相关的单元测试,确保修改代码的条件边界都被覆盖。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “发布单元与版本管理”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
164
专栏/许式伟的架构课/72 发布单元与版本管理.md
Normal file
164
专栏/许式伟的架构课/72 发布单元与版本管理.md
Normal file
@ -0,0 +1,164 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
72 发布单元与版本管理
|
||||
72 | 发布单元与版本管理你好,我是七牛云许式伟。
|
||||
|
||||
前面我们在 “[68 | 软件工程的宏观视角]” 一讲中谈到:一个软件工程往往是生命周期以数年甚至数十年计的工程。对于传统工程,我们往往把一个工程同时也称之为项目,项目工程。但软件工程不同,虽然我们平常也有项目的概念,但软件工程并不是一个项目,而是无数个项目。每个项目只是软件工程中的一个里程碑(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 | 软件工程的宏观视角]” 一讲中我们提到:
|
||||
|
||||
|
||||
软件项目的管理期望达到确定性。但软件工程本身是快速变化的,是不确定的。这就是软件工程本身的矛盾。我们的目标是在大量的不确定性中找到确定性,这其实就是软件工程最核心的点。
|
||||
|
||||
|
||||
只读设计提升了软件工程的确定性,所以只读思想被广泛运用。前面我们说开闭原则背后的架构治理哲学,也是模块,或者说软件实体,其业务范畴只读。在业务只读,接口稳定的预期下,模块与模块之间就可以自由组合,构建越来越复杂的系统。
|
||||
|
||||
往小里说,我们开发的时候,有时候会倾向于变量只读,以提高内心对确定性的预期。我并没有去用严谨的方式实证过变量只读的收益究竟有多大,但它的确成为了很重要的一种编程流派,即函数式编程。
|
||||
|
||||
函数式编程从编程范式来说比较小众,但是其只读思想被广泛借鉴。
|
||||
|
||||
这里面最典型的就是大数据领域的 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 分派的网关来做到。
|
||||
|
||||
这样两个版本服务并行,就不需要重构时做太细节的行为兼容。但应当注意,这也是不得已的办法,如果能够兼容,还是鼓励尽可能去兼容。毕竟客户端在升级版本之后,不兼容的地方越多,修改的心智负担就越大。
|
||||
|
||||
结语
|
||||
|
||||
今天我们聊的是怎么做版本管理。一个复杂的软件,总可以被分割为若干个独立迭代的发布单元,以便分而治之。发布单元的切割不宜过细,应该以一个小团队负责起来比较舒服为宜,不太小但也不太大。
|
||||
|
||||
版本的只读设计提高了系统的确定性预期,这是非常非常好的收益。但我们也应注意版本兼容上带来的坑。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “软件质量管理:单元测试、持续构建与发布”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
121
专栏/许式伟的架构课/73软件质量管理:单元测试、持续构建与发布.md
Normal file
121
专栏/许式伟的架构课/73软件质量管理:单元测试、持续构建与发布.md
Normal file
@ -0,0 +1,121 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
73 软件质量管理:单元测试、持续构建与发布
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
上一讲 “72 | 发布单元与版本管理” 我们聊了版本管理中,只读思想给软件工程带来的确定性价值,它在软件工程质量管理中也是很核心的一点。
|
||||
|
||||
软件质量管理
|
||||
|
||||
今天我们聊聊软件工程中,我们在质量管理上其他方面的一些思考。事实上,软件质量管理横跨了整个软件工程完整的生命周期。
|
||||
|
||||
|
||||
|
||||
软件工程与传统工程非常不同。它快速变化,充满不确定性。不仅如此,一个软件工程往往是生命周期以数年甚至数十年计的工程。对于传统工程,我们往往把一个工程同时也称之为项目,项目工程。但软件工程不同,虽然我们平常也有项目的概念,但软件工程并不是一个项目,而是无数个项目。每个项目只是软件工程中的一个里程碑(Milestone)。
|
||||
|
||||
这些都决定了软件工程质量管理的思想与传统工程截然不同。在传统工程中,设计的工作往往占比极少,重复性的工作占据其生命周期的绝大部分时间。所以传统工程有极大的确定性。检查清单(Check List)很可能就已经可以很好地实现其工程质量的管理。
|
||||
|
||||
但对于软件工程来说,设计工作在整个工程中持续发生。哪怕是非设计工作,比如编码实现,也仍然依赖个体的创造力,同样存在较强的不确定性。显然,检查清单(Check List)完全无法满足软件工程的质量管理需要。
|
||||
|
||||
那么,到底应该怎么管理软件工程的质量?每次谈软件工程质量保障的时候,我总会先画下面这张图:
|
||||
|
||||
|
||||
|
||||
它谈的是软件的生命周期,或者也可以理解为软件中某项功能的生命周期。我们把软件或软件的某项功能生命周期分成两个大的阶段,一个阶段是开发期,一个阶段是维护期。开发期与维护期是相对而言的,只是在表征上,开发期有更强的设计属性。维护期虽然也持续会有设计工作,但是工作量会小一个数量级以上。
|
||||
|
||||
为什么划分出开发期与维护期是重要的?
|
||||
|
||||
因为开发期的时间跨度虽然可能不长,但是它的影响太大了,基本决定了后期维护期的成本有多高。
|
||||
|
||||
这也意味着软件工程是需要有极强预见性的工程。我们在开发期恰如其分地多投入一分精力,后面在维护期就有十倍甚至百倍以上的回报。
|
||||
|
||||
设计工作的质量至关重要。但是它执行上又不太有复制性,可复制的只是设计范式和设计思维。
|
||||
|
||||
我们只能在这种执行的不确定性中找工程上的确定性。
|
||||
|
||||
如何做到?
|
||||
|
||||
单元测试
|
||||
|
||||
首先,做好自动化测试。自动化测试对软件工程的重要性是不言而喻的。如果是一项一次性的工程,我们可以基于常规的手工测试。但常规测试的缺点在于:
|
||||
|
||||
其一,一般常规测试是基于手工的,不具备可回归性。因此,常规测试的效率不高,一次完整的测试集跑下来可能需要几天甚至一周之久。
|
||||
|
||||
其二,易于缺乏效率,所以往往为了赶工会导致测试仅仅针对典型数据,测试的覆盖率往往也很低。
|
||||
|
||||
软件工程的生命周期往往几年甚至几十年之久,我们必然关注单次测试的效率。所以自动化测试的核心价值就在于可回归性与提高测试的覆盖率。
|
||||
|
||||
自动化测试与常规测试相比,风格上有很明显的不一样,它有如下重要特征。
|
||||
|
||||
|
||||
自动化、可回归性。
|
||||
静默(Quiet)。没有发生错误的时候,就不说话。
|
||||
案例执行的安全受控。某个案例执行的失败,不会影响其他案例的正常运行。
|
||||
|
||||
|
||||
从分类来说,一般自动化测试我们分两个层次:一个是模块级的单元测试,一个是系统级的集成测试。
|
||||
|
||||
无论从什么角度来看,模块的单元测试都是重中之重的大事。原因是,单元测试的成本是最低的。
|
||||
|
||||
关于测试成本,我们可以从两个维度看。
|
||||
|
||||
其一,单元测试的实施成本低,最容易去做。不少高级语言比如 Go 语言甚至在语言内建的工具链上就直接支持。而集成测试虽然也有自动化的方法和支持工具,但是往往需要更高额的代价。
|
||||
|
||||
其二,减少问题发现的周期,进而降低问题的修复成本。单元测试将问题发现周期缩短,基本上在问题现场就发现问题,这降低了Bug的修复成本。如果问题在系统的集成测试阶段发现,那么从问题定位,到回忆当初实现这段代码时候的思路,到最终去解决掉它,必然需要多花费几倍甚至几十倍的时间。
|
||||
|
||||
因此,我们鼓励更严格的单元测试要求,更高的单元测试覆盖率,以尽可能把发现问题做到前头。
|
||||
|
||||
但仍然有不少公司在推广单元测试上遇到了不小的麻烦,推不起来。
|
||||
|
||||
对于这一点,我们认为首先要改变的是对推广单元测试这件事情的认知。我们不把推广单元测试看作是让大家去多做一件额外的事情,而是规范大家做单元测试的方法。
|
||||
|
||||
为什么这么说?因为实际上单元测试大家都会去做,很少有人会不经验证就直接交付。但是验证方式上可能有各种 “土” 方法,比如用 print,用可视化的界面做输入测试,用调试工具做单步跟踪等等。
|
||||
|
||||
但是这些方法代价其实一样不低,但是却不可回归,正确与否还需要人脑临时去判断。
|
||||
|
||||
更重要的是,这些方法最大的问题是没有办法去固化已知的 Bug,最大程度保留下来我们的测试案例。
|
||||
|
||||
这其实才是最核心的一个认知问题:我们应当重视我们的测试代码,它同样也是我们的开发成果,理应获得和模块的功能代码同等重要的地位,理应被保留下来。
|
||||
|
||||
解决了这个认知上的共识问题,自动化测试就能够被很好地推动起来。当前这方面的工具链已经非常完善,不至于会在工具上遇到太大的障碍。
|
||||
|
||||
持续构建,持续发布
|
||||
|
||||
其次,我们降低软件工程不确定性的方法是:持续构建,持续发布。
|
||||
|
||||
我们鼓励更小的发布。我们鼓励更短的发布周期,更高的发布频率。这能够让发布的负担降低到最低。
|
||||
|
||||
这种极度高频交付的机制与传统工程的质量管理机制迥异。但是它被证明是应对软件工程不确定性的最佳方式。为什么会这样?
|
||||
|
||||
其一,交付的功能越少,因为错误而发生回滚的代价越低,影响面越小。如果我们同时发布了数十个功能,却因为某一个功能不达标而影响整体交付,这其实是降低了软件的功能交付效率。更好的方式显然是把这个出问题的功能回滚,把其他所有功能都放行。
|
||||
|
||||
其二,交付频率越高,我们对交付过程的训练越频繁,过程的熟练度越高,执行效率也越高。当交付成为一个自然习惯后,我们会把交付看作功能开发的一部分,而不是以前大家对研发的理解,认为做完功能就完事,后续上不上线与我无关。我们会鼓励更多把研发的绩效与功能线上的表现关联起来,面向客户价值,而非仅仅面向功能开发。
|
||||
|
||||
当然这种极度高频交付的机制,意味着它对软件工程的系统化建设有更高的要求。
|
||||
|
||||
当然,除了日构建与发布平台外,我们也需要在其中加入各种质量管理的抓手。比如:
|
||||
|
||||
|
||||
自动化运行单元测试案例(unit test);
|
||||
单元测试覆盖率检查(code coverage);
|
||||
静态代码质量检查(lint);
|
||||
人工的代码互审(code review);
|
||||
灰度发布(gray release);
|
||||
A/B 测试(A/B testing);
|
||||
……
|
||||
|
||||
|
||||
结语
|
||||
|
||||
今天我们更加完整地探讨了软件工程的质量管理。整体来说,软件工程与传统工程在质量管理上的理念是迥异的,甚至往往是反其道而行之的。究其原因,还是因为软件工程的核心在于如何在高度的不确定性中找到确定性。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “开源、云服务与外包管理”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
153
专栏/许式伟的架构课/74 开源、云服务与外包管理.md
Normal file
153
专栏/许式伟的架构课/74 开源、云服务与外包管理.md
Normal file
@ -0,0 +1,153 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
74 开源、云服务与外包管理
|
||||
74 | 开源、云服务与外包管理你好,我是七牛云许式伟。今天我们聊的话题是有关于分工的。
|
||||
|
||||
在这一讲之前,我们涉及到分工这个话题,基本上都局限于企业内部,且大多数情况下主要在同一个团队内部。但今天我们聊的是更大的分工:跨组织的分工与协作。
|
||||
|
||||
外包及其理想模型
|
||||
|
||||
在软件工程中,我们第一个接触的外部分工毫无疑问是外包。所谓外包,就是将我们软件的全部或部分模块的实现职能交给外部团队来做。
|
||||
|
||||
但是,软件工程项目的外包实际上成功率非常低。这背后有其必然性,它主要表现在以下这些方面。
|
||||
|
||||
其一,任务表达的模糊,双方容易扯皮。期望需求方能够把需求边界说清楚,把产品原型画清楚,把业务流程讲清楚,这非常难。有这样专业的需求表达能力的,通常软件工程水平不低,遇到这样的需求方,绝对应该谢天谢地。这种专业型的甲方,它大部分情况下只发生在项目交付型外包,而非产品功能外包。更多的产品外包,一般是甲方不太懂技术,需要有团队替自己把事情干了,他好拿着产品去运营。
|
||||
|
||||
其二,交付的代码质量低下,长期维护的代价高。软件工程不是项目,它都需要长长久久地运行下去。但是接包方的选择相当重要。因为接包方的质量相当参差不齐,遇上搬砖的概率远高于设计能力优良的团队。事实上,有良好设计能力的团队,多数情况下也不甘于长期做外包。
|
||||
|
||||
其三,项目交接困难,知识传承效率很低。软件工程并非普通的工程,就算交付的结果理想,项目交接也非常困难。所以外包项目第一期结束后,如果运营得好,往往项目还继续会有第二期、第三期。这里的原因是你只能找同一拨人做,如果换一波人接着做,考虑到知识传承效率低下,往往需要很长的一个交接周期。
|
||||
|
||||
那么,外包的理想模型是什么?
|
||||
|
||||
上面我们已经说到,外包在通常情况下,专业的甲方需要说清楚需求,这样双方就没有分歧。但是,更好的做法其实不是外包需求,而是外包实现。
|
||||
|
||||
也就是说,作为专业的甲方,我自己做好需求分析,做好系统的概要设计。进一步,我们把每个模块的业务范畴与接口细化下来。我们以此作为外包边界。假设分了N个模块,我们可以把它们平均分给若干个接包方。
|
||||
|
||||
这种方式的外包,甲方相当于只留了架构师团队,实现完全交给了别人。但是它与普通的外包完全不同,因为根本不担心知识传承的问题。每个模块的接包方对甲方来说就真的只是干活的。
|
||||
|
||||
接包方拿到的是模块的规格说明书。他要做的是模块的详细设计的实现部分,其中最为核心的是数据结构设计。对于服务端,甲方可以规定所采用的数据库是什么,但是把表结构的设计交出去。
|
||||
|
||||
进一步,如果模块的外包说明书中还规定了单元测试的案例需要包含哪些,那么这个模块发生设计偏离的可能性就很低。
|
||||
|
||||
外包的验收需要包含模块的实现设计文档,里面描述了数据结构+算法。另外,单元测试部分,每个测试场景,也填上对应的测试函数的名称。
|
||||
|
||||
实际会有人这样去外包么?
|
||||
|
||||
我不确定。但我们可以把它看作一种分工的假想实验。这个假想实验可以充分说明架构师团队的重要性。有了一个好的架构师团队,他们设计合适的系统架构,对每个模块的规格都做了相应的定义,他们验收模块的实现。
|
||||
|
||||
这样,项目就可以有条不紊地展开。甚至,研发进度可以自如控制。嫌项目进展太慢?找一倍的接包方,就可以让工程加速一倍。
|
||||
|
||||
所以,这个外包假想实验也说明了一点:我们的平常项目之所以进度无法达到预期,无他,团队缺乏优秀的架构师而已。
|
||||
|
||||
让我们把软件工程看作一门科学。我们以[工程师思维]的严谨态度来看它。我们减少项目中的随意性,把架构设计的核心,模块规格,也就是接口,牢牢把控住。这样,项目的执行风险就完全消除了。
|
||||
|
||||
哦不,还有一个最大的执行风险没有消除。我怎么证明这个系统架构的分解是对的?不会出现每个模块做好了,但是最终却拼不起来?
|
||||
|
||||
我们前面在 “[架构:系统的概要设计]” 这一讲中实际上已经谈过这事的解决方法:系统设计的产出要有源代码,它是项目的原型。关键模块有 mock 的实现,业务系统的关键 UserStory 都串了一遍,确保系统设计的正确性。
|
||||
|
||||
这个假想实验是有趣的,它可以让你想明白很多事情。甚至可以把它看作理解这个专栏的架构思维核心思想的钥匙。
|
||||
|
||||
我希望,它不只是一个假想实验。
|
||||
|
||||
开源与众包
|
||||
|
||||
我们把话题拉回到跨组织的分工。
|
||||
|
||||
除了传统的外包外,在软件工程中出现的第二类外包是众包,它以开源这样一个形态出现。
|
||||
|
||||
从分工角度,开源的核心思想是让全社会的程序员共同来完成一个业务系统。
|
||||
|
||||
开源的优势非常明显。对于一个热门的开源项目,它的迭代进度是非常惊人的,因为它撬动的资源太大了。
|
||||
|
||||
但不是开源了就能够获得这样的好处。
|
||||
|
||||
虽然成功的开源项目风风火火,但是我们也应该意识到,对于那些并没有得到关注的开源项目,它们的迭代速度完全无法保障。最终,你可能还是只能靠自己的团队来完成它的演进。
|
||||
|
||||
从这个意义上看,开源是一种商业选择。你得持续经营它。没有经营的开源项目不会成功。你需要宣传它,你自己也得持续迭代它,你还要为它拉客户。有客户的开源项目自然就有了生命力。
|
||||
|
||||
另外,开源这种形态,注定了它只能做大众市场。如果一个业务系统它的受众很少,就比较难通过开源获得足够的外部支持。
|
||||
|
||||
所以绝大部分成功的开源项目,都属于基础设施性质的业务系统,有极其广泛适用的场景。例如,语言、操作系统、基础库、编程框架、浏览器、应用网关、各类中间件等等。我们这个架构课重点介绍的内容,大部分都有相应的开源实现。
|
||||
|
||||
开源对信息科技的影响极其巨大,它极大地加速了信息科技前进的进程,是全球共同精诚协作的典范。
|
||||
|
||||
没有参与过开源的程序员是需要心有遗憾的。开源沉淀下来的协同方法与工作流,今天被无数公司所借鉴。
|
||||
|
||||
没有开源,我们无法想象这件事情:那么多形形色色的企业,今天其中绝大部分,它们的软件工程协同方法与业务流竟然如此相似。
|
||||
|
||||
这是开源带来的另一种无形资产。
|
||||
|
||||
如果大家没有忘记的话,可能能够回忆起来,在谈完软件工程的宏观视角之后,我首先聊的是 “[团队的共识管理]”。为什么这很重要?因为它是团队协作效率的最大基础。如果连对协作的工作流都没有共识,那团队真的是一盘散沙了。
|
||||
|
||||
今天我们几乎不会遇到工作方式上的问题,不是别的原因,是开源给予我们的礼物。它让全球的程序员、全球的科技企业,都养成了一模一样的工程习惯。
|
||||
|
||||
云计算与服务外包
|
||||
|
||||
云服务是新的跨组织分工的形态。无论是传统的外包,还是开源的众包,它们都属于源代码外包。这类外包的共同特点是,它们不对结果负责。
|
||||
|
||||
对于传统外包,项目验收结束,双方一手交钱一手交货,至于用得好不好,那是甲方自己的事情。
|
||||
|
||||
对于开源软件来说,那更是完全免责,你爱用不用,用了有什么问题责任自负。当然有很多公司会购买开源软件的商业支持,这不难理解,除了有人能够帮助我一起完成项目上线外,最重要的是要有人能够给我分担出问题的责任。
|
||||
|
||||
互联网为跨组织协同带来了新的机会。我可以24小时为另一个组织服务,而无需跑到对方的办公室,和他们团队物理上处在一起。
|
||||
|
||||
这就是云计算。云计算从跨组织协同的角度来看,不过是一种新的交付方式。我们不再是源代码交付,而是服务交付。所以,你也可以把云计算看着一种外包,我们称之为服务外包。
|
||||
|
||||
大部分的基础设施,都可以以服务外包的方式进行交付。这中间释放的生产力是惊人的。
|
||||
|
||||
一方面,云计算与传统外包不同,它对结果负责,有服务 SLA 承诺。一旦出问题,问题也可以由云服务提供方自己解决,而无需业务方介入,这极大降低了双方的耦合,大家各司其职。
|
||||
|
||||
另一方面,它简化了业务方的业务系统,让它得以能专注自己真正的核心竞争力的构建。
|
||||
|
||||
站在生产效率角度看,不难理解为什么我们会坚信云服务是未来必然的方向。
|
||||
|
||||
外包方式的选择
|
||||
|
||||
任何企业都存在于社会生态之中,我们无法避开组织外部的分工协同问题。
|
||||
|
||||
怎么选择跨组织的协同方式?
|
||||
|
||||
在七牛,自成立以来我们就一直有一句话谈我们对跨组织协同的看法:
|
||||
|
||||
|
||||
我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。
|
||||
|
||||
在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。
|
||||
|
||||
|
||||
这句话有它一定的道理,但也有它模糊的地方。
|
||||
|
||||
首先是关于 “核心竞争力相关”。我们并没有太清晰地去定义什么样的东西是我们核心竞争力相关,什么不相关。
|
||||
|
||||
一些程序员对此理解可能会比较 “技术化”,认为业务系统的核心模块就是核心竞争力。与它相关的东西就是核心竞争力相关。
|
||||
|
||||
但更合理的视角不是技术视角,而是业务视角。我们每一家企业都是因为服务客户而存在。所以,与服务客户的业务流越相关,越不能外包,而是要自己迭代优化,建立服务质量与效率的竞争优势。
|
||||
|
||||
另外,外包的选择需要非常谨慎。很多开发人员都有随意引用开源项目的习惯,这一定程度上给项目带来了不确定的风险。
|
||||
|
||||
我一直认为,开源项目的引入需要严格把关。严谨来说,开源项目引入大部分情况下是属于我说的 “基础架构” 选择的范畴,这同样是架构师团队需要承担的重要职责,一定要有正规的评估流程。
|
||||
|
||||
结语
|
||||
|
||||
今天我们聊的话题是跨组织的分工与协同。在形态上,我们可以分为:传统外包、开源与云服务。当然还有就是我们今天没有讨论的使用外部商业软件。
|
||||
|
||||
从形态来说,商业软件很接近传统外包,但是从它的边界来说,因为商业软件往往有明确的业务边界,所以在品质上会远高于外包。当然定制过于严重的商业软件例外,它在某种程度上来说退化为了传统外包。
|
||||
|
||||
在外包方式的选择上,我们的建议是:
|
||||
|
||||
|
||||
我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。
|
||||
|
||||
在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。
|
||||
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “软件版本迭代的规划”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
175
专栏/许式伟的架构课/75 软件版本迭代的规划.md
Normal file
175
专栏/许式伟的架构课/75 软件版本迭代的规划.md
Normal file
@ -0,0 +1,175 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
75 软件版本迭代的规划
|
||||
75 | 软件版本迭代的规划你好,我是七牛云许式伟。
|
||||
|
||||
到今天为止,我们专栏的话题主要集中在软件工程的质量与效率上。我们在专栏的开篇中就已经明确:
|
||||
|
||||
|
||||
从根本目标来说,软件架构师要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。
|
||||
|
||||
|
||||
但是今天,我们将探讨一个更高维的话题:软件版本迭代的规划。后续我们简称为 “版本规划”。简单说,就是下一步的重点应该放在哪里,到底哪些东西应该先做,哪些东西应该放到后面做。
|
||||
|
||||
这是一个极其关键的话题。它可以影响到一个业务的成败,一个企业的生死存亡。方向正确,并不代表能够走到最后,执行路径和方向同等重要。
|
||||
|
||||
那么,版本规划的套路是什么?
|
||||
|
||||
探讨这个问题前,我想先看一个实际的案例。这个案例大家很熟悉:Go 语言的版本迭代。
|
||||
|
||||
我们从 Go 语言的演进,一起来看看 Go 团队是如何做软件版本迭代规划的。这有点长,但是细致地琢磨对我们理解版本规划背后的逻辑是极其有益的。
|
||||
|
||||
Go 版本的演进历史
|
||||
|
||||
Go 语言的版本迭代有明确的周期,大体是每半年发布一个版本。
|
||||
|
||||
Go 1.0 发布于 2012 年 3 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1。它是 Go 语言发展的一个里程碑。
|
||||
|
||||
在这个版本,Go 官方发布了兼容性文档: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。这个版本主要专注于语言内在机制的改善和性能提升(编译器、垃圾回收、map、goroutine调度)。改善后的效果如下:
|
||||
|
||||
|
||||
|
||||
这个版本还发布了一个竞态探测器(race detector),它对 Go 这种以高并发著称的语言显然是重要的。详细可参考 Go 官方博客文章:https://blog.golang.org/race-detector。
|
||||
|
||||
Go 1.2 发布于 2013 年 12 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.2。这个版本发布了单元测试覆盖率检查工具:go tool cover。详细可参考 Go 官方博客文章:https://blog.golang.org/cover。
|
||||
|
||||
Go 1.3 发布于 2014 年 6 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.3。这个版本栈的内存分配引入了连续段(contiguous segment)的分配模式,以提升执行效率。之前的分页式的栈分配方式(segment stack)存在频繁地分配/释放栈段导致栈内存分配耗时不稳定且效率较低。引入新机制后,分配稳定性和性能都有较大改善。
|
||||
|
||||
Go 1.3 还引入了 sync.Pool,即内存池组件,以减少内存分配的次数。标准库中的 encoding/json、net/http 等都受益于它带来的内存分配效率提升。另外,Go 还对 channel 进行了性能优化:
|
||||
|
||||
|
||||
|
||||
Go 1.4 发布于 2014 年 12 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.4。从功能来说,这个版本最大的一件事情是增加了 Android/iOS 支持(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。
|
||||
|
||||
Go 1.5 发布于 2015 年 8 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.5。这个版本让 Go 实现了自举。这让GC 效率优化成为可能。所以在这个版本中,GC 被全面重构。由于引入并发垃圾回收,回收阶段带来的延迟降低了一个数量级。
|
||||
|
||||
这个版本还有一个很重要的尝试,是引入了 vendor 机制以试图解决 Go 模块的版本管理问题。自从 Go 解决了 GC 效率后,Go 版本管理就成了老大难问题。下图是 Go 社区对 Go 面临的最大挑战的看法:
|
||||
|
||||
|
||||
|
||||
当然后来事实证明 vendor 机制并不成功。
|
||||
|
||||
另外,Go 1.5 引入了 go tool trace,通过该命令我们可以实现执行器的跟踪(trace)。详细参考 https://golang.org/cmd/trace/。
|
||||
|
||||
Go 1.6 发布于 2016 年 2 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.6。垃圾回收器的延迟在这个版本中进一步降低。如下:
|
||||
|
||||
|
||||
|
||||
从功能上来说,这个版本支持了 HTTP/2。
|
||||
|
||||
Go 1.7 发布于 2016 年 8 月,详细 ReleaseNote 见 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。GC 延迟在这个版本中进一步得到改善,延迟时间降到毫秒级别以下。
|
||||
|
||||
另外,这个版本还大幅提升了defer的性能。如下:
|
||||
|
||||
|
||||
|
||||
Go 1.9 发布于 2017 年 8 月,详细 ReleaseNote 见 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。在这个版本中,go test 引入了一个新的缓存机制,所有通过测试的结果都将被缓存下来。当 test 没有变化时,重复执行 test 会节省大量时间。类似地,go build 也维护了一个已构建的包的缓存以加速构建效率。
|
||||
|
||||
Go 1.11 发布于 2018 年 8 月,详细 ReleaseNote 见 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。这个版本的 go vet 命令基于 analysis 包进行了重写,使得 go vet 更为灵活并支持 Gopher 编写自己的 checker。详细参考 “How to Build Your Own Analyzer” 一文。
|
||||
|
||||
Go 1.13 发布于 2019 年 8 月,详细 ReleaseNote 见 https://tip.golang.org/doc/go1.13。这个版本的 sync.Pool 性能得到进一步的改善。当 GC 时,Pool 中对象不会被完全清理掉。它引入了一个 cache,用于在两次 GC 之前清理 Pool 中未使用的对象实例。
|
||||
|
||||
另外,这个版本的逃逸分析(escape analysis)被重新实现了,这让 Go 更少地在堆上分配内存。下图是新旧逃逸分析的基准测试对比:
|
||||
|
||||
|
||||
|
||||
另外,Go modules 引入的 GOPROXY 变量的默认值被改为:
|
||||
|
||||
GOPROXY=https://proxy.golang.org,direct
|
||||
|
||||
|
||||
但在国内无法访问 Go 官方提供的 proxy.golang.org 站点。建议改为:
|
||||
|
||||
export GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
|
||||
这里 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 阶段的方法论,在少量客户上先做灰度。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们谈谈 “软件工程的未来”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
86
专栏/许式伟的架构课/76 软件工程的未来.md
Normal file
86
专栏/许式伟的架构课/76 软件工程的未来.md
Normal file
@ -0,0 +1,86 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
76 软件工程的未来
|
||||
76 | 软件工程的未来你好,我是七牛云许式伟。现在正值中国年,我在这里祝大家新年快乐。开开心心过大年的同时,注意安全第一,出门记得戴上口罩,少去人员聚集的地方。
|
||||
|
||||
好,那我们开始今天的学习,今天我们想聊聊软件工程的未来。
|
||||
|
||||
软件工程是一门非常年轻的学科,相比其他动辄跨世纪的自然科学而言,软件工程只有 50 年的历史。只有如此短暂实践的科学,今天我们来探讨它的未来,条件其实还并不算太充分。
|
||||
|
||||
但是我们的宗旨就是要每个领域都应该谈清楚过去(历史)与未来(趋势判断),所以今天不妨也理性来探讨一下。
|
||||
|
||||
在 “[软件工程的宏观视角]” 一讲中,我们引入了下图来表达软件工程的瀑布模型:
|
||||
|
||||
|
||||
|
||||
在这样一个模型里面,涉及的角色分工已经非常多:
|
||||
|
||||
|
||||
产品经理;
|
||||
架构师;
|
||||
开发工程师;
|
||||
质量保障(QA)工程师;
|
||||
网站可靠性工程师(SRE);
|
||||
……
|
||||
|
||||
|
||||
但这还只是常规描述的工种。实际的分工要细致很多。更不要说对特殊的领域,比如企业服务,也就是大家常说的 2B 行业,它的基本过程是这样的:
|
||||
|
||||
|
||||
|
||||
比之纯粹的产品研发上线过程,它多了单个客户的跟进与落地实施过程,也由此引入更多的角色分工,比如:售前工程师、交付(实施)工程师、售后工程师、项目经理等。
|
||||
|
||||
未来软件工程会走向何方?
|
||||
|
||||
首先 “快速变化” 是软件工程的自然属性,其 “不确定性” 也只能抑制而无法消除。
|
||||
|
||||
但显而易见的是,软件工程的问题最终还是由软件解决。事实上今天很多问题已经解决得很好,比如源代码的管理。我们经历了 cvs、svn,最终到今天的 git。基本上开发人员的协同问题已经形成非常约定俗成的方法论,并以软件或云服务的方式被固化下来。
|
||||
|
||||
今天,线上服务管理正如火如荼的发展。假以时日,不需要多久之后,一个全新的时代开启,我们中大部分人不必再为线上服务的稳定性操心。关于这块更详细的讨论,可以参考第四章 “服务治理篇”。
|
||||
|
||||
需求管理与测试这块也已经得到很好的解决。唯一比较遗憾就是是界面(UI)相关的测试虽然也有相关的工具链,但当前的普及率仍然极低。
|
||||
|
||||
这可能与大部分公司都较难保证界面的稳定性有关。如果我们经常变动界面,这就如同我们经常调整一个模块对外的接口规格一样,必然导致相关的测试案例编译通不过,或者测试通不过。这会让人沮丧,进而丧失对实现界面(UI)测试自动化的信心。自动化测试极其依赖被测模块接口的稳定性,这是我们今天常规自动化测试方法的限制。
|
||||
|
||||
当然另一方面,这也与界面测试相对高维,大部分公司的质量保障水平都还没有到达这个级别有关。从现实来看,虽然单元测试方法论已经极其成熟,但是仍然有不少企业在推行中遇到不少障碍。
|
||||
|
||||
可以预期,随着企业的平均工程水平逐步提升,最终会形成越来越多的有效的界面测试最佳实践的方法论,并得以大范围的推广。
|
||||
|
||||
从全局来看,今天软件工程已经形成较为成熟的分工。但各类分工的最佳实践与软件系统,仍然是相对孤立的。
|
||||
|
||||
这一定程度上也与软件工程还很年轻有关。从软件工程的软件系统发展来说,可以预期的是,未来一定会形成更加一体化的系统,上一道 “工序” 的输出就是下一道 “工序” 的输入。
|
||||
|
||||
但是今天一些 “工序” 的输出仍然是人肉进行传递,甚至没有标准化的仓库管理它。例如,产品经理输出的产品界面设计原型、架构师输出的架构设计文档,其传递过程仍然有极大的随意性。
|
||||
|
||||
但是,软件工程的最大不确定性就来源于 “设计” 类工作,包括产品设计与软件的架构设计。今天虽然产品设计和架构设计也都有一些独立的工具,但普及度与刚才说的开发与测试类工程实践相比完全是小巫见大巫。
|
||||
|
||||
这是可以理解的,产品经理与架构师在软件工程中属于小众群体,其培养难度极高,很多经验也很难形成传统意义上的 “知识点” 来传递。所以真正意义上合格的产品经理与架构师是比较少的,和程序员(软件开发工程师)的规模完全无法相比。
|
||||
|
||||
就拿架构师这个岗位举例。架构师的职责是什么,架构师工作的方法论是什么、培养架构师的方法论又是怎样的,这些今天并没有一个被广泛接受的实践。
|
||||
|
||||
为什么我会写这个架构课专栏,以及为什么成立七牛大学开启线下的架构师实战训练营,也是希望能够在一定程度上找到这些问题的最佳答案。
|
||||
|
||||
而事实上,产品经理的培养有更高的难度。严格意义上来说,成为产品经理前,首先应该成为架构师。我这个观念可能与大部分人的常识相悖,但是我个人对此深信不疑。
|
||||
|
||||
软件工程的未来发展会怎样,细节上很难给出确定性的判断。但是,我们相信,软件工程极大成熟的标志,是一体化的软件工程支撑系统,和高效的人才培养体系。包括今天仍然极为稚嫩的架构师培养体系,和产品经理培养体系,都应该得到了极大的完善。
|
||||
|
||||
到那个时候,软件工程就成为了一门真正成熟的科学。
|
||||
|
||||
结语
|
||||
|
||||
软件工程项目迭代快速、充满变化、充满不确定性。这使得软件工程成为一门极其独特魅力的科学。今天这门科学仍然还非常年轻,其发展只能以日新月异来形容。
|
||||
|
||||
软件工程的未来,它的成熟不单单是工程方法论和业务系统软件的成熟,也需要包括人才培养体系的成熟。因为,软件工程的不确定性与它充满设计与创造有关,人的主观能动性是它的优势,但也意味着不确定性无法得到彻底的消除。
|
||||
|
||||
我们要做的,只能说在大量的不确定性中,找到尽可能多的确定性。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。至此,本章 “软件工程篇” 已经到尾声阶段,下一讲我们将对本章的内容进行回顾与总结。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
116
专栏/许式伟的架构课/77 软件工程篇:回顾与总结.md
Normal file
116
专栏/许式伟的架构课/77 软件工程篇:回顾与总结.md
Normal file
@ -0,0 +1,116 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
77 软件工程篇:回顾与总结
|
||||
77 | 软件工程篇:回顾与总结你好,我是七牛云许式伟。
|
||||
|
||||
我们架构课的最后一章软件工程篇到此就要结束了。今天我们就本章的内容进行回顾与总结。
|
||||
|
||||
架构师并不是一个纯技术岗位。我们从软件工程的视角来看,架构师的职责就是要对软件工程的执行结果负责,这包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。
|
||||
|
||||
|
||||
|
||||
软件工程所覆盖的范畴非常广泛。从开始的需求与历史版本缺陷,到新版本的产品设计,到架构设计,到编码与测试,到最终的产品发布,到线上服务的持续维护。
|
||||
|
||||
还有贯穿整个工程始终的,是不变的团队分工与协同,以及不变的质量管理。
|
||||
|
||||
我们这个专栏并没有打算站在完整的软件工程角度去谈,更多还是从架构师与软件工程的关联入手。
|
||||
|
||||
本章的内容大体如下图所示。
|
||||
|
||||
|
||||
|
||||
软件工程是一项团体活动,大家有分工更有协同。不同的个体因为能力差别,可以形成十倍以上的生产力差距。而不同的团体更是如此,他们的差距可能更上一个数量级,达到百倍以上的生产力差距。
|
||||
|
||||
百倍以上的差距是什么概念?这就是说,一个团队只需要三四天做出来的东西,另一个团队可能需要一年才能做出来。两者之间的差距之大,只能用天壤之别来形容。
|
||||
|
||||
个人与个人的差距,你可以认为是技术上的能力差距的反映。但团队与团队的差距,不是简单的技术上的能力差距,而是有着更为深刻的原因。
|
||||
|
||||
高效团队的效率,核心体现在以下两个方面:
|
||||
|
||||
|
||||
团队开发一个新功能的效率。它体现的是架构的老化程度。
|
||||
团队新人的融入效率。新人多快的速度可以融入到团队,理解业务系统的现状及团队的做事方式。
|
||||
|
||||
|
||||
开发新功能的效率,主要取决于架构的优劣。这初听起来是一项纯技术上的事情。但如果我们站在时间维度上长达数年甚至数十年的软件工程的角度看,能够维持架构设计的持续优异,这绝非某个人的技术能力可以做到的事情,而是要靠团队共同的坚持。
|
||||
|
||||
而从新人融入效率看,更非技术能力所能够简单囊括,而是仰仗团队对业务传承的坚持。
|
||||
|
||||
这些东西的背后,关乎的都是有关于协同的科学。
|
||||
|
||||
有的团体像一盘散沙,充其量可以叫团伙。有的团体则有极强的凝聚力,整个团队上下同心,拧成一股绳,这种团体才是高效率组织,是真正意义上的团队。
|
||||
|
||||
共识是团队效率的基础。
|
||||
|
||||
从软件工程角度来说,产品设计和架构设计是团队最大的共识。架构过程就是一次团队共识确认的过程,从项目的混沌之初,到团队形成越来越清晰且一致的视图( Picture)。
|
||||
|
||||
高效团队往往还有极高的团队默契,这让他们无论是维护老项目还是做什么新项目都如鱼得水。团队默契可以包含很多东西,比如:
|
||||
|
||||
|
||||
共同的目标;
|
||||
团队的做事态度与价值观;
|
||||
编码规范;
|
||||
架构设计文档的模板;
|
||||
软件工程的方法论;
|
||||
基础架构及技术选型;
|
||||
……
|
||||
|
||||
|
||||
对于一个团队新人来说,融入一个团队或一个项目的基础过程就是阅读别人写的源代码。既有的文档越清楚,新人阅读代码的障碍就越小,融入的速度就越快。
|
||||
|
||||
文档要怎样才能把问题说清楚?
|
||||
|
||||
文档传递的是思维方式。大多数程序员不善于写文档,甚至讨厌写文档。这背后的根源不在于文档本身,而在于有效的思维表达方式,这需要长期的训练。
|
||||
|
||||
软件工程的各个环节都有其交付物。理想情况下,上一个环节的输出是下一环节的输入。软件系统的质量管理一般从这些交付物的管理入手。例如:交付物的版本管理、单元测试、持续构建,灰度发布,等等。
|
||||
|
||||
从更宏观的视角看,我们还涉及人力资源规划的问题。什么东西应该外包出去,包给谁?软件版本的计划是什么样的,哪些功能先做,哪些功能后做?
|
||||
|
||||
这些选择非常非常重要。因为他们属于业务架构的顶层设计。
|
||||
|
||||
除了传统意义上的外包外,外包方式还有:开源(众包)、云服务(服务外包)、商业软件(产品外包)。在外包方式的选择上,我们的建议是:
|
||||
|
||||
|
||||
我们尽可能不要做太多事情。非核心竞争力相关的,能够外包的我们尽可能外包。
|
||||
|
||||
在外包选择上,我们优先选择云服务,次选开源,最后才考虑传统的外包。
|
||||
|
||||
|
||||
当然,哪些事情是非核心竞争力相关,这一点不同公司可能判断不尽相同。但基本的判断逻辑是,越与我们面向用户所提供的业务流程相关,越靠近企业的核心竞争力,也就越不能外包。
|
||||
|
||||
软件版本迭代的规划需要根据业务的发展阶段而定。在不同阶段,版本迭代的侧重点会有极大的不同。
|
||||
|
||||
从 0 到 1 阶段,我们验证的是用户使用姿势,也就是产品设计的规格。这时性能并不是第一位的。
|
||||
|
||||
但是进入扩张阶段,产品竞争力就是一些用户关心的关键指标。这时候我们迭代的不再是用户使用姿势,它已经非常稳定。我们迭代的往往是看不见的非功能性需求,是那些用户真正最在乎的部分。
|
||||
|
||||
而遇到会对产品产生巨大冲击的功能需求,头脑别发热,谨慎处理。回到从 0 到 1 阶段的方法论,在少量客户上先做灰度。
|
||||
|
||||
结语
|
||||
|
||||
软件工程还很年轻,只有 50 年的历史。有关于软件工程的系统与方法论都仍然在快速演化与迭代中。
|
||||
|
||||
这意味着意我们不必墨守成规。要勇于探索,勇于打破固有的惯例,去建立新的方法论,新的惯例。
|
||||
|
||||
但需要强调的是,打破惯例不是胡闹,不是要做不尊重科学的 “野蛮人”。今天仍然有那么一批工程师,人数还不在少数,他们随心所欲、任性而为,不喜欢写架构设计文档,不喜欢写单元测试,不喜欢代码互审(code review)。
|
||||
|
||||
我们首先需要尊重团队协同的科学,在尊重的基础上去探索新的更高效的协同方法论。
|
||||
|
||||
很早之前我说过以下这段话,它很长一段时间里,被贴在某家公司墙上:
|
||||
|
||||
|
||||
严谨并非创新的对立面,而是创新的重要基础。每个人都有灵光乍现的时刻,但是唯有那些拥有严谨的科学态度的人才能抓住它,把它变成现实。
|
||||
|
||||
|
||||
我想,它非常适合作为软件工程篇的结束语。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。至此,本章 “软件工程篇” 结束,下一讲将结束本专栏的内容。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
178
专栏/许式伟的架构课/加餐 怎么保障发布的效率与质量?.md
Normal file
178
专栏/许式伟的架构课/加餐 怎么保障发布的效率与质量?.md
Normal file
@ -0,0 +1,178 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
加餐 怎么保障发布的效率与质量?
|
||||
加餐 | 怎么保障发布的效率与质量?你好,我是七牛云许式伟。
|
||||
|
||||
为什么要有发布流程?
|
||||
|
||||
在 “[49 | 发布、升级与版本管理]” 一讲中我们提到过:
|
||||
|
||||
|
||||
变更是故障之源。
|
||||
|
||||
|
||||
这种由于业务需要而主动发起的软硬件升级与各类配置变更,我们可以统一称之为发布。例如:
|
||||
|
||||
|
||||
更换交换机的类型,或升级版本。
|
||||
更换所依赖的基础软件,或升级版本。基础软件包括操作系统、负载均衡、数据库等等。
|
||||
升级业务软件本身。
|
||||
调整软硬件环境的配置项。
|
||||
|
||||
|
||||
特殊地,如果集群的服务对扩容缩容有很好的自动化支持,可以非常便捷地增加或减少服务器,那么这种情况虽然发生了集群的变化,我们可以不把它看作变更,不走发布相关的流程。尤其在硬件已经完全池化时,增加、减少服务器可能是个非常标准化且低成本的操作。
|
||||
|
||||
我们通常说的 “版本发布”,往往侧重点是在升级业务软件的版本,这是发布中最常发生的情况,当然也是我们最为关注的。
|
||||
|
||||
传统的软件公司的发布周期往往很长,有几个月甚至有的是按年来计算。而互联网公司的发布周期则非常不同。之所以快速发布、快速迭代变得简单的原因是,它们仅仅需要在服务器端发布,而不需要发布到每个使用者的电脑上。
|
||||
|
||||
一个每三年发布一次新产品的公司不需要详细的发布流程。因为发布的频率太低了,发布流程的优化能够带来的收益太小。
|
||||
|
||||
但是如果我们每天都在发布,甚至每天发布很多次,那么如此高频的发布速度,就要求我们创建和维护一个效率与质量都能够兼顾的精简的发布流程。
|
||||
|
||||
一个精简的发布流程,通常需要有发布平台这样的基础设施,把发布过程中反复遇到的问题对应的解决方案固化到系统中。
|
||||
|
||||
但是系统并不能解决所有的发布问题。变更终究是存在未知的新东西,需要人工进行检查判断。为此,SRE 部门往往还建立了一个专门的团队负责发布,即发布协调小组。团队成员称为 “发布协调工程师(Launch Coordination Engineering,LCE)”。
|
||||
|
||||
发布协调小组会针对每个业务,维护一个该业务的 “发布检查列表”,包括针对每次发布需要检查的常见问题,以及避免常见问题发生的手段。只有在发布检查表中的检查点都得到了确认,才会给版本发布放行。
|
||||
|
||||
这个列表在实践中被证实,它是保障发布可靠性的重要工具。
|
||||
|
||||
建立在系统之上的灰度发布
|
||||
|
||||
除了 “发布检查列表”,我们还有一个至关重要的保障发布质量的做法:灰度发布。
|
||||
|
||||
不管你如何小心,发布检查做得多全面,仍然只是在尽可能减少发布的风险,而不是消除。任何改动都具有一定的危险性,而任何危险性都应该被最小化,这样才能保障系统的可靠性。
|
||||
|
||||
在小型的测试环境上测试成功的变更,不见得在生产环境就没有问题,更何况从 SRE 的角度,测试的覆盖率也是不能假设的。
|
||||
|
||||
任何发布都应该灰度进行,并且在整个过程中还需要穿插必要的校验步骤。刚开始,新的服务可能会在某个数据中心的一台或几台机器上安装,并且被严密监控一段时间。如果没有发现异常,新版本会在更多台机器上安装并再次监控,直至最后完成整个发布过程。
|
||||
|
||||
发布的第一阶段通常被称为 “金丝雀”。这和煤矿工人带金丝雀下矿井检测有毒气体类似,通过使用这些“金丝雀” 服务线上流量,我们可以观察任何异常现象的发生。
|
||||
|
||||
“金丝雀” 测试适用于正常的软件版本发布,也适用于配置项的变更。负责配置变更的工具通常都会对新启动的程序监控一段时间,保证服务没有崩溃或者返回异常。如果在校验期间出现问题,系统会自动回退。
|
||||
|
||||
灰度式发布的理念甚至并不局限于软件和服务的发布。例如,我们商业上的高成本的运营活动,往往会先选择一到两个地区先做实验,然后再把成功经验复制到全国各地。
|
||||
|
||||
所以灰度发布思想的一个自然延伸是做功能开关,也就是大家熟悉的 AB 测试。很多东西在测试环境中无法模拟时,或者在真实环境中仍然存在不可预知的情况时,灰度机制就非常有用了。
|
||||
|
||||
不是所有的改动都可以一样对待。有时我们仅仅是想检查某个界面上的改动是否能提升用户感受。这样的小改动不需要几千行的程序或者非常重量级的发布流程。我们可能希望同时测试很多这方面的改动。
|
||||
|
||||
有时候我们只是想要知道是否有足够多的用户会喜欢使用某个新功能,就通过发布一个简单的原型给他们测试。这样我们就不用花费数个月的时间来优化一个没人想要使用的功能。
|
||||
|
||||
通常来说,这类 AB 测试框架需要满足以下几个要求:
|
||||
|
||||
|
||||
可以同时发布多个变更,每个变更仅针对一部分服务器或用户起作用。
|
||||
变更可以灰度发布给一定数量的服务器或用户,比如 1%。
|
||||
在严重Bug发生,或者有其他负面影响时,可以迅速单独屏蔽某个变更。
|
||||
用数据来度量每个变更对用户体验的提升。
|
||||
|
||||
|
||||
LCE 的职责
|
||||
|
||||
LCE 团队负责管理发布流程,以确保整个发布过程做到又快又好。LCE 有如下这些职责:
|
||||
|
||||
|
||||
审核新产品及相关的内部服务,确保它们的可靠性标准达到要求。如果不达预期,提供一些具体的建议来提升可靠性。
|
||||
在发布过程中作为多个团队之间的联系纽带。
|
||||
负责跟进发布系统相关的所有技术问题。
|
||||
作为整个发布过程中的一个守门人,决定某次发布是否是 “安全的”。
|
||||
|
||||
|
||||
整体来说,LCE 的要求其实是相当高的。LCE 的技术要求与其他的 SRE 成员一样,但这个岗位打交道的外部团队很多,需要有很强的沟通和领导能力。他需要将分散的团队聚合在一起达成一个共同目标,同时还需要偶尔处理冲突问题,还要能够为软件开发工程师提供建议和指导。
|
||||
|
||||
发布检查列表
|
||||
|
||||
我们前面已经提过,发布检查列表可以用来保障发布质量,它是可靠发布产品与服务的重要组成部分。一个完备的检查列表通常包含以下这些方面的内容。
|
||||
|
||||
其一,架构与依赖相关。针对系统架构的评审可以确定该服务是否正确使用了某类基础设施,并且确保这些基础设施的负责人加入到发布流程中来。为什么要引入基础设施的负责人,是因为需要确认相关依赖的服务都有足够的容量。
|
||||
|
||||
一些典型的问题有:
|
||||
|
||||
|
||||
从用户到前端再到后端,请求流的顺序是什么样的?
|
||||
是否已经将非用户请求与用户请求进行隔离?
|
||||
预计的请求数量是多少?单个页面请求可能会造成后端多个请求。
|
||||
|
||||
|
||||
其二,集成和公司最佳实践相关。很多公司的对外服务都要运行在一个内部生态系统中,这些系统为如何建立新服务器、配置新服务、设置监控、与负载均衡集成,以及设置 DNS 配置等提供了指导。
|
||||
|
||||
其三,容量规划相关。新功能通常会在发布之初带来临时的用量增长,在几天后会趋于平稳。这种尖峰式的负载或流量分布可能与稳定状态下有显著区别,之前内部的压力测试可能失效。
|
||||
|
||||
公众的兴趣是很难预测的,有时甚至需要为预计容量提供 15 倍以上的发布容量。这种情况下灰度发布会有助于建立大规模发布时的数据依据与信心。
|
||||
|
||||
一些典型的问题有:
|
||||
|
||||
|
||||
本次发布是否与新闻发布会、广告、博客文章或者其他类型的推广活动有关?
|
||||
发布过程中以及发布之后预计的流量和增速是多少?
|
||||
是否已经获取到该服务需要的全部计算资源?
|
||||
|
||||
|
||||
其四,故障模式相关。针对服务进行系统性的故障模式分析可以确保发布时服务的可靠性。
|
||||
|
||||
在检查列表的这一部分中,我们可以检查每个组件以及每个组件的依赖组件来确定当它们发生故障时的影响范围。
|
||||
|
||||
一些典型的问题有:
|
||||
|
||||
|
||||
该服务是否能够承受单独物理机故障?单数据中心故障?网络故障?
|
||||
如何应对无效或者恶意输入,是否有针对拒绝服务攻击(DoS)的保护?
|
||||
是否已经支持过载保护?
|
||||
如果某个依赖组件发生故障,该服务是否能够在降级模式下继续工作?
|
||||
该服务在启动时能否应对某个依赖组件不可用的情况?在运行时能否处理依赖不可用和自动恢复情况?
|
||||
|
||||
|
||||
其五,客户端行为相关。最常见的客户端滥发请求的行为,是配置更新间隔的设置问题。比如,一个每 60s 同步一次的新客户端,会比600s 同步一次的旧客户端造成10倍的负载。
|
||||
|
||||
重试逻辑也有一些常见问题会影响到用户触发的行为,或者客户端自动触发的行为。假设我们有一个处于过载状态的服务,该服务由于过载,某些请求会处理失败。如果客户端重试这些失败请求,会对已经过载的服务造成更大负载,于是会造成更多的重试,更多的负载。客户端这时应该降低重试的频率,一般需要增加指数型增长的重试延迟,同时仔细考虑哪些错误值得重试。例如,网络错误通常值得重试,但是 4xx 错误(这一般意味着客户端侧请求有问题)一般不应该重试。
|
||||
|
||||
自动请求的同步性往往还会造成惊群效应。例如,某个手机 APP 开发者可能认为夜里2点是下载更新的好时候,因为用户这时可能在睡觉,不会被下载影响。然而,这样的设计会造成夜里 2 点时有大量请求发往下载服务器,每天晚上都是如此,而其他时间没有任何请求。这种情况下,每个客户端应该引入一定随机性。
|
||||
|
||||
其他的一些周期性过程中也需要引入随机性。回到之前说的那个重试场景下:某个客户端发送了一个请求,当遇到故障时,1s 之后重试,接下来是 2s、4s 等。没有随机性的话,短暂的请求峰值可能会造成错误比例升高,这个周期会一直循环。为了避免这种同步性,每个延迟都需要一定的抖动,也就是加入一定的随机性。
|
||||
|
||||
一些典型的问题有:
|
||||
|
||||
|
||||
客户端在请求失败之后,是否按指数型增加重试延时?
|
||||
是否在自动请求中实现随机延时抖动?
|
||||
|
||||
|
||||
其六,流程与自动化相关。虽然我们鼓励自动化,但是对于发布这件事情来说,完全自动化是灾难性的。为了保障可靠性,我们应该尽量减少发布流程中的单点故障源,包括人在内。
|
||||
|
||||
这些流程应该在发布之前文档化,确保在工程师还记得各种细节的时候就完全转移到文档中,这样才能在紧急情况下派上用场。流程文档应该做到能使任何一个团队成员都可以在紧急事故中处理问题。
|
||||
|
||||
一些典型的问题有:
|
||||
|
||||
|
||||
是否已将所有需要手动执行的流程文档化?
|
||||
是否已将构建和发布新版本的流程自动化?
|
||||
|
||||
|
||||
其七,外部依赖相关。有时候某个发布过程依赖于某个不受公司控制的因素。尽早确认这些因素的存在可以使我们为它们的不确定性做好准备。
|
||||
|
||||
例如,服务依赖于第三方维护的一个类库,或者另外一个公司提供的服务或者数据。当第三方提供商出现故障、Bug、系统性的错误、安全问题,或者未预料到的扩展性问题时,尽早计划可以使我们有办法避免影响到直接用户。
|
||||
|
||||
一些典型的问题有:
|
||||
|
||||
|
||||
这次发布依赖哪些第三方代码、数据、服务,或者事件?
|
||||
是否有任何合作伙伴依赖于你的服务?发布时是否需要通知他们?
|
||||
当我们或者第三方提供商无法在指定截止日期前完成工作时,会发生什么?
|
||||
|
||||
|
||||
结语
|
||||
|
||||
今天我们探讨 “发布与升级” 的实践,如何既保证质量,又能够兼顾效率。正确的做法当然不是为了快而去忽略流程,而是在不断的发布经历中总结经验教训,把每个环节干得更快更有效率。
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们聊聊 “故障域与故障预案”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
231
专栏/许式伟的架构课/加餐如何做HTTP服务的测试?.md
Normal file
231
专栏/许式伟的架构课/加餐如何做HTTP服务的测试?.md
Normal file
@ -0,0 +1,231 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
加餐 如何做HTTP服务的测试?
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
基于 HTTP 协议提供服务的好处是显然的。除了 HTTP 服务有很多现成的客户端、服务端框架可以直接使用外,在 HTTP 服务的调试、测试、监控、负载均衡等领域都有现成的相关工具支撑。
|
||||
|
||||
在七牛,我们绝大部分的服务,包括内部服务,都是基于 HTTP 协议来提供服务。所以我们需要思考如何更有效地进行 HTTP 服务的测试。
|
||||
|
||||
七牛早期 HTTP 服务的测试方法很朴素:第一步先写好服务端,然后写一个客户端 SDK,再基于这个客户端 SDK 写测试案例。
|
||||
|
||||
这种方法多多少少会遇到一些问题。首先,客户端 SDK 的修改可能会导致测试案例编不过。其次,客户端 SDK 通常是使用方友好,而不是测试方友好。服务端开发过程和客户端 SDK 的耦合容易过早地陷入“客户端 SDK 如何抽象更合理” 的细节,而不能专注于测试服务逻辑本身。
|
||||
|
||||
我的核心诉求是对服务端开发过程和客户端开发过程进行解耦。在网络协议定好了以后,整个系统原则上就可以编写测试案例,而不用等客户端 SDK的成熟。
|
||||
|
||||
不写客户端 SDK 而直接做 HTTP 测试,一个直观的思路是直接基于 http.Client 类来写测试案例。这种方式的问题是代码比较冗长,而且它的业务逻辑表达不直观,很难一眼就看出这句话想干什么。虽然可以写一些辅助函数来改观,但做多了就会逐渐有写测试专用 SDK 的倾向。这种写法看起来也不是很可取,毕竟为测试写一个专门的 SDK,看起来成本有些高了。
|
||||
|
||||
七牛当前的做法是引入一种 httptest DSL 文法。这是七牛为 HTTP 测试而写的领域专用语言。这个 httptest 工具当前已经开源,项目主页为:
|
||||
|
||||
|
||||
https://github.com/qiniu/httptest(httptest 框架)
|
||||
https://github.com/qiniu/qiniutest(支持七牛帐号与授权机制的 qiniutest 工具)
|
||||
|
||||
|
||||
httptest 基础文法
|
||||
|
||||
这个语言的文法大概在 2012 年就已经被加入到七牛的代码库,后来有个同事根据这个 DSL 文法写了第一版本 qiniutest 程序。在决定推广用这个 DSL 来进行测试的过程中,我们对 DSL 不断地进行了调整和加强。虽然总体思路没有变化,但最终定稿的 DSL 与最初版本有较大的差异。目前来说,我已经可以十分确定地说,这个DSL可以满足 90% 以上的测试需求。它被推荐做为七牛内部的首选测试方案。
|
||||
|
||||
|
||||
|
||||
上图是这套 DSL 的 “hello world” 程序。它的执行预期是:下载 www.qiniu.com 首页,要求返回的 HTTP 状态码为 200。如果返回非 200,测试失败;否则测试通过,输出返回包的正文内容(resp.body 变量)。输出 resp.body 的内容通常是调试需要,而不是测试需要。自动化测试是不需要向屏幕去输出什么的。
|
||||
|
||||
|
||||
|
||||
我们再看该 DSL 的一个 “quick start(快速入门)” 样例。以 # 开始的内容是程序的注释部分。这里有一个很长很长的注释,描述了一个基本的 HTTP 请求测试的构成。后面我们会对这部分内容进行详细展开,这里暂时跳过。
|
||||
|
||||
这段代码的第一句话是定义了一个 auth 别名叫 qiniutest,这只是为了让后面具体的 HTTP 请求中授权语句更简短。紧接着是发起一个 POST 请求,创建一个内容为 {“a”: “value1”, “b”: 1} 的对象,并将返回的对象 id 赋值给一个名为 id1 的变量。后面我们会详细解释这个赋值过程是如何进行的。
|
||||
|
||||
接着我们发起一个获取对象内容的 GET 请求,需要注意的是 GET 的 URL 中引用了 id1 变量的值,这意味着我们不是要取别的对象的内容,而是取刚刚创建成功的对象的内容,并且我们期望返回的对象内容和刚才POST上去的一样,也是 {“a”: “value1”, “b”: 1}。这就是一个最基础的 HTTP 测试,它创建了一个对象,确认创建成功,并且尝试去取回这个对象,确认内容与我们期望的一致。这里上下两个请求是通过 id1 这个变量来建立关联的。
|
||||
|
||||
对这套DSL文法有了一个大概的印象后,我们开始来解剖它。先来看看它的语法结构。首先这套 httptest DSL 基于命令行文法:
|
||||
|
||||
command switch1 switch2 … arg1 arg2 …
|
||||
|
||||
|
||||
整个命令行先是一个命令,然后紧接着是一个个开关(可选),最后是一个个的命令参数。和大家熟悉的命令行比如 Linux Shell 一样,它也会有一些参数需要转义,如果参数包含空格或其他特殊字符,则可以用 \ 前缀来进行转义。比如 ‘\ ’ 表示 “(空格),‘\t’表示 TAB 等。另外,我们也支持用 ‘…’ 或者 “…” 去传递一个参数,比如 json 格式的多行文本。同 Linux Shell 类似,’…’ 里面的内容没有转义,‘\ ’ 就是 ‘\ ’,‘\t’就是 ‘\t’,而不是 TAB。而 “…” 则支持转义。
|
||||
|
||||
和 Linux Shell 不同的是,我们的 httptest DSL 虽然基于命令行文法,但是它的每一个参数都是有类型的,也就是说这个语言有类型系统,而不像 Linux Shell 命令行参数只有字符串。我们的 httptest DSL 支持且仅支持所有 json 支持的数据类型,包括:
|
||||
|
||||
|
||||
string(如:”a”、application/json 等,在不引起歧义的情况下,可以省略双引号)
|
||||
number(如:3.14159)
|
||||
boolean(如:true、false)
|
||||
array(如:[“a”, 200, {“b”: 2}])
|
||||
object/dictionary(如:{“a”: 1, “b”: 2})
|
||||
|
||||
|
||||
另外,我们的 httptest DSL 也有子命令的概念,它相当于一个函数,可以返回任意类型的数据。比如 qiniu f2weae23e6c9f jg35fae526kbce返回一个 auth object,这是用常规字符串无法表达的。
|
||||
|
||||
理解了 httptest DSL 后,我们来看看如何表达一个 HTTP 请求。它的基本形式如下:
|
||||
|
||||
req <http-method> <url>
|
||||
header <key1> <val11> <val12>
|
||||
header <key2> <val21> <val22>
|
||||
auth <authorization>
|
||||
body <content-type> <body-data>
|
||||
|
||||
|
||||
第一句是 req 指令,带两个参数: 一个是 http method,即 HTTP 请求的方法,如 GET、POST 等。另一个是要请求的 URL。
|
||||
|
||||
接着是一个个自定义的 header(可选),每个 header 指令后面跟一个 key(键)和一个或多个 value(值)。
|
||||
|
||||
然后是一个可选的 auth 指令,用来指示这个请求的授权方式。如果没有 auth 语句,那么这个 HTTP 请求是匿名的,否则这就是一个带授权的请求。
|
||||
|
||||
最后一句是 body 指令,顾名思义它用来指定 HTTP 请求的正文。body 指令也有两个参数,一个是 content-type(内容格式),另一个是 body-data(请求正文)。
|
||||
|
||||
这样说比较抽象,我们看下实际的例子:
|
||||
|
||||
无授权的 GET 请求:
|
||||
|
||||
req GET http://www.qiniu.com/
|
||||
|
||||
|
||||
带授权的 POST 请求:
|
||||
|
||||
req POST http://foo.com/objects
|
||||
auth `qiniu f2weae23e6c9fjg35fae526kbce`
|
||||
body application/json '{
|
||||
"a": "hello1",
|
||||
"b":2
|
||||
}'
|
||||
|
||||
|
||||
也可以简写成:
|
||||
|
||||
无授权的GET请求:
|
||||
|
||||
get http://www.qiniu.com/
|
||||
|
||||
|
||||
带授权的Post请求:
|
||||
|
||||
post http://foo.com/objects
|
||||
auth `qiniu f2weae23e6c9fjg35fae526kbce`
|
||||
json '{
|
||||
"a": "hello1",
|
||||
"b":2
|
||||
}'
|
||||
|
||||
|
||||
发起了 HTTP 请求后,我们就可以收到 HTTP 返回包并对内容进行匹配。HTTP 返回包匹配的基本形式如下:
|
||||
|
||||
ret <expected-status-code>
|
||||
header <key1> <expected-val11><expected-val12>
|
||||
header <key2> <expected-val21><expected-val22>
|
||||
body <expected-content-type><expected-body-data>
|
||||
|
||||
|
||||
我们先看 ret 指令。实际上,请求发出去的时间是在 ret 指令执行的时候。前面 req、header、auth、body 指令仅仅表达了 HTTP 请求。如果没有调用 ret 指令,那么系统什么也不会发生。
|
||||
|
||||
ret 指令可以不带参数。不带参数的 ret 指令,其含义是发起 HTTP 请求,并将返回的 HTTP 返回包解析并存储到 resp 的变量中。而对于带参数的 ret 指令:
|
||||
|
||||
ret <expected-status-code>
|
||||
|
||||
|
||||
它等价于:
|
||||
|
||||
ret
|
||||
match <expected-status-code> $(resp.code)
|
||||
|
||||
|
||||
match 指令
|
||||
|
||||
这里我们引入了一个新的指令:match 指令。
|
||||
|
||||
|
||||
|
||||
七牛所有 HTTP 返回包匹配的匹配文法,都可以用这个 match 来表达:
|
||||
|
||||
|
||||
|
||||
所以本质上来说,我们只需要一个不带参数的 ret,加上 match 指令,就可以搞定所有的返回包匹配过程。这也是我们为什么说 match 指令是这套 DSL 中最核心的概念的原因。
|
||||
|
||||
和其他自动化测试框架类似,这套 DSL 也提供了断言文法。它类似于 CppUnit 或 JUnit 之类的测试框架提供 assertEqual。具体如下:
|
||||
|
||||
equal <expected> <source>
|
||||
|
||||
|
||||
|
||||
与 match 不同,这里 <expected>、<source>中都不允许出现未绑定的变量。
|
||||
|
||||
与 match 不同,equal 要求<expected>、<source>的值精确相等。
|
||||
|
||||
equalSet
|
||||
|
||||
这里 SET 是指集合的意思。
|
||||
|
||||
与 equal 不同,equalSet 要求 <expected>、<source>都是array,并且对 array 的元素进行排序后判断两者是否精确相等。
|
||||
|
||||
equalSet 的典型使用场景是测试 list 类的 API,比如列出一个目录下的所有文件,你可能预期这个目录下有哪些文件,但是不能预期他们会以什么样的次序返回。
|
||||
|
||||
|
||||
以上介绍基本上就是这套 DSL 最核心的内容了。内容非常精简,但满足了绝大部分测试场景的需求。
|
||||
|
||||
测试环境的参数化
|
||||
|
||||
下面我们谈谈最后一个话题:测试环境的参数化。
|
||||
|
||||
为了让测试案例更加通用,我们需要对测试依赖的环境进行参数化。比如,为了让测试脚本能够同时用于 stage 环境和 product 环境,我们需要把服务的 Host 信息参数化。另外,为了方便测试脚本入口,我们通常还需要把 用户名/密码、AK/SK 等敏感性信息参数化,避免直接硬编码到测试案例中。
|
||||
|
||||
为了把服务器的 Host 信息(也就是服务器的位置)参数化,我们引入了 host 指令。例如:
|
||||
|
||||
host foo.com 127.0.0.1:8888
|
||||
get http://foo.com/objects/a325gea2kgfd
|
||||
auth qiniutest
|
||||
ret 200
|
||||
json '{
|
||||
"a": "hello1",
|
||||
"b":2
|
||||
}'
|
||||
|
||||
|
||||
这样,后文所有出现请求 foo.com 地方,都会把请求发送到 127.0.0.1:8888 这样一个服务器地址。要想让脚本测试另外的服务器实例,我们只需要调整 host 语句,将 127.0.0.1:8888 调整成其他即可。
|
||||
|
||||
除了服务器 Host 需要参数化外,其他常见的参数化需求是 用户名/密码、AK/SK 等。AK/SK 这样的信息非常敏感,如果在测试脚本里面硬编码这些信息,将不利于测试脚本代码的入库。一个典型的测试环境参数化后的测试脚本样例如下:
|
||||
|
||||
|
||||
|
||||
其中,env 指令用于取环境变量对应的值(返回值类型是 string),envdecode 指令则是先取得环境变量对应的值,然后对值进行 json decode 得到相应的 object/dictionary。有了$(env) 这个对象(object),就可以通过它获得各种测试环境参数,比如 $(env.FooHost)、$(env.AK)、$(env.SK) 等。
|
||||
|
||||
写好了测试脚本后,在执行测试脚本之前,我们需要先配置测试环境:
|
||||
|
||||
export QiniuTestEnv_stage='{
|
||||
"FooHost": "192.168.1.10:8888",
|
||||
"AK": "…",
|
||||
"SK": "…"
|
||||
}'
|
||||
|
||||
export QiniuTestEnv_product='{
|
||||
"FooHost": "foo.com",
|
||||
"AK": "…",
|
||||
"SK": "…"
|
||||
}'
|
||||
|
||||
|
||||
这样我们就可以执行测试脚本了:
|
||||
|
||||
测试 stage 环境:
|
||||
|
||||
QiniuTestEnv=stage qiniutest ./testfoo.qtf
|
||||
|
||||
|
||||
测试 product 环境:
|
||||
|
||||
QiniuTestEnv=product qiniutest ./testfoo.qtf
|
||||
|
||||
|
||||
结语
|
||||
|
||||
测试是软件质量保障至关重要的一环。一个好的测试工具对提高开发效率的作用巨大。如果能够让开发人员的开发时间从一小时减少到半小时,那么日积月累就会得到惊人的效果。
|
||||
|
||||
去关注开发人员日常工作过程中的不爽和低效率是非常有必要的。任何开发效率提升相关的工作,其收益都是指数级的。这也是我们所推崇的做事风格。如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
184
专栏/许式伟的架构课/加餐实战:“画图程序”的整体架构.md
Normal file
184
专栏/许式伟的架构课/加餐实战:“画图程序”的整体架构.md
Normal file
@ -0,0 +1,184 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
加餐 实战:“画图程序” 的整体架构
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
我们先回顾一下 “架构思维篇” 前面几讲的内容:
|
||||
|
||||
|
||||
[57 | 心性:架构师的修炼之道]
|
||||
[58 | 如何判断架构设计的优劣?]
|
||||
[59 | 少谈点框架,多谈点业务]
|
||||
[60 | 架构分解:边界,不断重新审视边界]
|
||||
|
||||
|
||||
我们先谈了怎么才能修炼成为一个好的架构师,其中最核心的一点是修心。这听起来好像一点都不像是在谈一门有关于工程的学科,但这又的的确确是产生优秀架构师最重要的基础。
|
||||
|
||||
接下来几篇,我们核心围绕着这样几个话题:
|
||||
|
||||
|
||||
什么是好的架构?
|
||||
架构的本质是业务的正交分解,分解后的每个模块业务上仍然是自洽的。
|
||||
|
||||
|
||||
我们反复在强调 “业务” 一词。可以这样说,关注每个模块的业务属性,是架构的最高准则。
|
||||
|
||||
不同模块的重要程度不同,由此我们会区分出核心模块和周边模块。对于任何一个业务,它总可以分解出一个核心系统,和多个周边系统。不同周边系统相互正交。即使他们可能会发生关联,也是通过与核心系统打交道来建立彼此的间接联系。
|
||||
|
||||
今天我们将通过第二章 “桌面开发篇” 的实战案例 “画图程序” 来验证下我们这些想法。我们以最后一次迭代的版本 v44 为基础:
|
||||
|
||||
|
||||
https://github.com/qiniu/qpaint/tree/v44
|
||||
|
||||
|
||||
整体结构
|
||||
|
||||
我们先来分析整个 “画图” 程序的整体结构。除了 index.htm 作为总控的入口外,我们把其他的文件分为以下四类:
|
||||
|
||||
|
||||
核心系统(棕色):这些文件隶属于整个画图程序的业务核心,不可或缺;
|
||||
周边系统(黄色):这些文件属于业务的可选组件;
|
||||
通用控件(绿色):这些文件与画图程序的业务无关,属于通用的界面元素,由画图程序的周边系统所引用;
|
||||
基础框架(紫色):这些文件与画图程序的业务无关,属于第三方代码,或者更基础的底层框架。
|
||||
|
||||
|
||||
我们可以有如下文件级别的系统组织结构:
|
||||
|
||||
|
||||
|
||||
通过这个图我们可以看出,这个画图程序的 “内核” 是非常小的,就三个文件:index.htm、view.js、dom.js。为了让你看到每个文件的复杂度,我把各个文件的代码规模也在图中标了出来。如果我们把所有的周边系统以及它们的依赖代码去除,整个程序仍然是可以工作的,只不过我们得到的是一个只读的画图程序的查看器(QPaintViewer)。
|
||||
|
||||
这很有意思,因为我们把所有的 Controllers 都做成了彼此完全正交的可选组件。
|
||||
|
||||
有了这个图,我们对各个文件之间的关系就很清楚了。接下来,正如我们在 “ [58 | 如何判断架构设计的优劣?]” 中说的那样,我们最关心的还是周边系统,也就是这些 Controller 对核心系统的伤害是什么样的。
|
||||
|
||||
我们先把所有引用关系列出来:
|
||||
|
||||
|
||||
|
||||
我们先看 creator/rect.js 模块。它对 View 层,主要是 QPaintView 类的引用是 10 处,对 Model 层,主要是 QPaintDoc、Shape、QShapeStyle 这三者的引用是 6 处。每处引用都是 1 行代码,直接调用 View 层或 Model 层对外提供的接口方法。
|
||||
|
||||
单就 creator/rect.js 模块而言,它对核心系统的伤害值为 10 + 6 = 16。但是实际上这些接口方法绝大部分并不是专门提供给 creator/rect.js 模块的,这意味着所有周边模块应该共担这个伤害值。比如某个接口方法被 N 个周边模块引用,那么每个周边模块分担的伤害值为 1/N。
|
||||
|
||||
这个逻辑初听起来有点奇怪,我新增一个和我互不相关的周边模块,怎么会导致一个既有周边模块对核心系统的伤害值降低?
|
||||
|
||||
这是因为,我们的伤害值是工程测量值。我们往极端来说,如果有无穷多个周边模块都会引用某个接口方法,那么对于其中某个周边模块来说,它为此造成的伤害值为 0,因为这个接口太稳定了。这也证明,抽象出共性的业务方法,比给某个周边模块单独开绿灯要好。我们定义业务的接口要尽可能追求自然。
|
||||
|
||||
但是现实中,被无数个周边模块引用的接口是不存在的。你可能主观判断我这个接口是很通用的,但是它需要实证的依据。每增加一个引用方,这个实证就被加强一次。这也是为什么增加一个新周边模块会导致既有周边模块伤害值降低的原因,因为它证实了一些接口方法的确是通用的。
|
||||
|
||||
有一些接口当前只有 creator/rect.js 引用的,这些接口的引用代码在表格中我把它们标为红色,它们是:
|
||||
|
||||
|
||||
new QLine
|
||||
new QRect
|
||||
new QEllipse
|
||||
shape.onpaint
|
||||
|
||||
|
||||
我们一眼看过去就很清楚,这些接口确实是非常通用的接口。之所以它们只有 creator/rect.js 引用,是因为这个 “画图” 程序当前的规模还比较小,随着越来越多的周边模块加入,逐步也会有更多人分担伤害值。
|
||||
|
||||
当前系统有 5 个周边模块。考虑多个周边模块共担伤害值的情况,creator/rect.js 模块对核心系统的伤害值是多少?
|
||||
|
||||
我们做个近似,只要某个接口已经被超过一个周边模块引用,就认为它的引用次数是 5,而不是一一去统计它。这样算的话,creator/rect.js 模块对核心系统的伤害值约 12⁄5 + 4 = 6.4。
|
||||
|
||||
类似地,我们可以计算其他周边模块对核心系统的伤害值,具体如下:
|
||||
|
||||
|
||||
creator/path.js 模块,伤害值约 12⁄5 + 1 = 3.4。
|
||||
creator/freepath.js 模块,伤害值约 13⁄5 = 2.6。
|
||||
accel/select.js 模块,伤害值约 10⁄5 + 6 = 8。
|
||||
accel/menu.js 模块,伤害值约 5⁄5 + 6 = 7。
|
||||
|
||||
|
||||
如果我们把所有周边模块看作整体,它和核心系统的关系如下:
|
||||
|
||||
|
||||
|
||||
可以看出,整个周边系统对核心系统的引用是 31 处,也就是说它带来的伤害值为 31。这和上面我们近似计算得到的所有周边系统伤害值之和 6.4 + 3.4 + 2.6 + 8 + 7 = 27.4 不同。这中间的差异主要由于我们没有去实际统计接口方法的引用次数而直接统一用 5,所以估算的伤害值比实际会小一点。
|
||||
|
||||
Model 层
|
||||
|
||||
看完了整体,我们把关注点放到 Model 层。
|
||||
|
||||
对于这个画图程序,代码量最多的就是 Model 层,即 dom.js 文件,大约 850 多行代码。所以我们决定进一步分解它,得到如下结构:
|
||||
|
||||
|
||||
|
||||
当我们把 Model 层看作一个完整的业务时,它内部仍然可以分解出一个核心系统,和多个周边系统。并且同样地,我们把代码分为四类:
|
||||
|
||||
|
||||
核心系统:隶属于整个画图程序的业务核心,不可或缺,我们标记为棕色或白色;
|
||||
周边系统:属于业务的可选组件,主要是各类图形;
|
||||
操作系统相关的辅助函数:与业务无关,但是和平台相关,我们标记为绿色;
|
||||
纯算法的辅助函数:与业务无关,与操作系统也无关,我们标记为紫色。
|
||||
|
||||
|
||||
上图的核心系统中,标记为棕色的模块与白色的模块的区别在于,标棕色的模块会被周边系统所引用,属于核心系统的 “接口级” 模块。标白色的模块只被核心系统内部所引用,不把它们画出来也是可以的。
|
||||
|
||||
另外,图中 Shape 接口因为 JavaScript 是弱类型语言,它在代码中并没有显式体现出来。这里我们将它用 Go 语法表达如下:
|
||||
|
||||
type number = float64
|
||||
type any = interface{}
|
||||
|
||||
type HitResult struct {
|
||||
hitCode number
|
||||
hitShape Shape
|
||||
}
|
||||
|
||||
type Shape interface {
|
||||
style QShapeStyle
|
||||
onpaint(ctx CanvasRenderingContext2D)
|
||||
hitTest(pt Point) HitResult
|
||||
bound() Rect
|
||||
setProp(parent any, key string, val any)
|
||||
move(parent any, dx, dy number)
|
||||
toJSON() any
|
||||
}
|
||||
|
||||
|
||||
当然,和分析整个画图程序一样,我们最关心的还是周边系统对核心系统的伤害是什么样的。
|
||||
|
||||
我们先把所有引用关系列出来:
|
||||
|
||||
|
||||
|
||||
对于 Model 层来说,目前我们需求的开放性主要体现在图形(Shape)的种类。未来是否要支持图片,是否要支持艺术字等等,这些存在很大的变数。所以我们当前的周边模块,基本上都是某种图形。
|
||||
|
||||
通过这个表格我们可以看出,不同的图形对核心系统的需求完全一模一样。我们很容易计算得到,整个周边系统对核心系统的伤害值为 4,平均每一种图形的伤害值为 1。
|
||||
|
||||
通用控件库
|
||||
|
||||
聊了文件级别的组织结构,也聊了 Model 层,我们画图程序的整体脉络也就出来了。这里我再补充一个虽然和业务无关,但是也是一个不小的体系设计:通用控件库子系统。
|
||||
|
||||
控件的种类是无穷的,我们自然而然得去考虑怎么适应未来的需求。出于开放性架构的考虑,你会发现它也可以基于核心系统和周边系统来拆分,如下:
|
||||
|
||||
|
||||
|
||||
同样地,我们最关心的还是周边系统对核心系统的伤害是什么样的。
|
||||
|
||||
我们先把所有引用关系列出来:
|
||||
|
||||
|
||||
|
||||
通过这个表格我们可以看出,这些控件的实现本身和核心系统,即控件框架没什么关系,它们只是把自己注册到控件框架中。所有控件对核心系统的需求完全一模一样。我们很容易计算得到,整个周边系统对核心系统的伤害值为 1,平均每一种控件的伤害值为 1/3。
|
||||
|
||||
结语
|
||||
|
||||
这一讲我们通过前面实战的画图程序作为例子,来剖析架构设计过程业务是如何被分解的。
|
||||
|
||||
对于复杂系统,一定要理清核心系统和周边系统的边界,让整个程序的内核最小化。
|
||||
|
||||
另外,我们也实际分析了画图程序中,周边模块对核心系统的伤害值。这个数据可以很好地评判不同架构方案的好坏。
|
||||
|
||||
如果你自己也实现了一个 “画图程序”,可以根据这几讲的内容,对比一下我们给出的样例代码,和自己写的有哪些架构思想上的不同,这些不同之处的得失是什么?
|
||||
|
||||
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “全局性功能的架构设计”。
|
||||
|
||||
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。
|
||||
|
||||
|
||||
|
||||
|
222
专栏/许式伟的架构课/热点观察我看Facebook发币(上):区块链、比特币与Libra币.md
Normal file
222
专栏/许式伟的架构课/热点观察我看Facebook发币(上):区块链、比特币与Libra币.md
Normal file
@ -0,0 +1,222 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
热点观察 我看Facebook发币(上):区块链、比特币与Libra币
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
Facebook(脸书)于6月18日发布了其加密数字货币项目白皮书。该数字货币被命名为 Libra(天秤座),象征着平衡与公正。
|
||||
|
||||
此前,BBC 报道说这个数字货币叫 GlobalCoin(全球币),但后来被纠正说这只是Facebook员工在内部对其的昵称。
|
||||
|
||||
不管是叫 Libra,还是 GlobalCoin,其实都透露出了Facebook的雄心壮志。
|
||||
|
||||
今天的Facebook 坐拥 27 亿的活跃用户,相当于全球1/3的人都在用 Facebook,这是一个比微信大得多的数字社群(微信月活为 11 亿,差不多全中国人民都在用微信)。
|
||||
|
||||
我之所以叫它“数字社群”,而不叫“数字王国”,是因为用科学的态度来谈,“国家” 这样的称呼没法随便用,“国家”得符合国家需要具备的特征。
|
||||
|
||||
国家有哪些特征?
|
||||
|
||||
武装是大家能够很快想得到的,国家得有自己的军队,但是数字世界毕竟是个虚拟世界,军队似乎没啥意义。
|
||||
|
||||
发行货币是国家另一个至关重要的权力。现在,拥有 27 亿人口的 Facebook 要发币,这意味着它离真正意义上的 “数字王国” 又靠近了一步,至关重要的一步。
|
||||
|
||||
整个世界将因此发生翻天覆地的变化。
|
||||
|
||||
今天我想借此机会,谈谈区块链和数字货币背后的社会需求动因、逻辑以及它们将产生的巨大影响。
|
||||
|
||||
我们的内容将会分为上下两部分,如下:
|
||||
|
||||
|
||||
我看 FB 发币(上):区块链、比特币与 Libra 币;
|
||||
我看 FB 发币(下):深入浅出理解 Libra 币。
|
||||
|
||||
|
||||
区块链
|
||||
|
||||
我们先从区块链谈起。
|
||||
|
||||
介绍区块链技术的文章已经有很多了,我们今天不谈技术实现的细节。简单说,它是一个分散式防篡改的数字账本。
|
||||
|
||||
但是,区块链到底有什么用?它想解决什么样的问题?
|
||||
|
||||
有人说,区块链是为了“去中心化”。
|
||||
|
||||
那么“去中心化”是趋势么?互联网的趋势是“中心化”,而不是“去中心化”。
|
||||
|
||||
效率为先。如果一个事情可以两个人干更好,那么最终的结果就是应该两个人去干,而不会是十个人去干,这是最朴素的经济学原理。
|
||||
|
||||
而微信和 Facebook 的成功,也证明了中心化是趋势。人们之所以向往着去中心化,是因为人天然对垄断有抗拒之心,被人生杀予夺的感觉怎么想都不太美妙。
|
||||
|
||||
人们因为效率而中心化,因为垄断而去中心化。所以是一个中心还是几个中心,这是自然平衡的结果。
|
||||
|
||||
历史告诉我们,它选择的“去中心化”是开辟疆土。前有哥伦布发现新大陆,今有“钢铁侠”马斯克寻求火星殖民。这才是真正的“去中心化”,找到全新的盐碱地去开拓。
|
||||
|
||||
地球相比于整个宇宙,只不过是一粒尘埃。这样的“中心化”,又算得了什么呢?
|
||||
|
||||
当然也有人说,区块链是为了“去中介”。
|
||||
|
||||
那么“去中介化”是互联网的趋势么?互联网会减少尽可能多的中间环节,但是不会“去中介”。
|
||||
|
||||
每个人有他自己的专长。把自己不擅长的事情交给中介,这是特别自然的一件事情,我们为中介的专业性买单。
|
||||
|
||||
互联网让中介可以 24 小时为你服务,远程为你服务。这些都是以前没法完成的。这会导致什么呢?一些中介会茁壮成长,变成行业中的关键节点。它也有可能会顺带合并掉上下游的一些分工,让服务链条更短。
|
||||
|
||||
那么,区块链价值到底是什么?
|
||||
|
||||
我认为,区块链本质上是一种“共识机制”,或者说“契约机制”。 分散式防篡改的数字账本,保障的核心是事实的不可抵赖。这对双方形成共识是极大的效率提升。在大部分情况下,没有共识往往是因为争议的双方对事实的认定不同。
|
||||
|
||||
从这个认知来看,一些鼓吹“区块链是下一个互联网基础设施”的人们可以洗洗睡了。
|
||||
|
||||
区块链不会重造一个新互联网底层,但它最有可能重塑金融与供应链。
|
||||
|
||||
比特币
|
||||
|
||||
比特币,数字货币的鼻祖,区块链技术的第一个杀手级应用。
|
||||
|
||||
为什么会出现比特币?它希望解决什么样的需求?
|
||||
|
||||
其一,希望能够解决政府动不动就发币,让你手头货币凭空贬值的问题。
|
||||
|
||||
这应该是比特币创始人中本聪的本意。所以比特币发币的机制是挖矿,谁挖到就算谁的。成本是购买计算机的成本和运行计算机挖矿所消耗的电费。
|
||||
|
||||
从这个角度看,比特币不能类比法币,它没有法币的发行者,国家信用作为背书。它更像是黄金,基于资源本身的稀缺性作为背书。
|
||||
|
||||
其二,希望能够解决经济全球化带来的货币跨境流通问题。
|
||||
|
||||
这是数字货币的价值投资者们的共同期望。
|
||||
|
||||
随着互联网技术在全球范围的不断普及,越来越多的生意被放到了网上。旧的商业文明可以一言以蔽之:一手交钱,一手交货。而建立在互联网之上的新商业文明,我们一手下单付款,一手收钱发货,足不出户,货物就通过便捷的物流服务送到了你手上。
|
||||
|
||||
但建立在互联网之上的新商业文明,遭遇了国家与国家之间的边界挑战。这体现在以下两点。
|
||||
|
||||
其一,汇率。 我们都知道,法币与法币的兑换是有损的。你把钱从 A 币换成 B 币,再把 B 币换回 A 币,钱就少了。
|
||||
|
||||
其二,关税。 一个商品从一个国家买到另一个国家,成本就变高了。刨除物流成本带来的影响因素外,最大的额外代价就是关税。
|
||||
|
||||
物流成本是可以解决的。贸易全球化带来的结果是生产全球化。
|
||||
|
||||
以前国与国之间的贸易,主要成分在成品贸易,这个成品的原料和加工的零件都来自于同一个国家,我生产出成品后销往世界各地。
|
||||
|
||||
但是现在国与国之间的贸易,主要成分在原料与零部件的交易。这意味着供应链已经越来越全球化,成品的生产很可能就在销售地,或者靠近销售地。
|
||||
|
||||
这样的好处,一方面当然是产品品质的需要,可以全球寻求优质供应商。另一方面可以极大程度地降低物流成本。原材料的运输相比成品来说,更易于利用规模化效应降低单位运输成本。
|
||||
|
||||
关税问题,也是可以国家之间协调解决的。超低关税,乃至最终零关税是历史发展的必然。
|
||||
|
||||
那么汇率问题呢?
|
||||
|
||||
这个问题不好解决。提供兑换货币服务的金融机构不可能是免费服务,他们自身的成本也在那里。只要有跨境交易,就有“用什么结算货币”的问题,随之而来的就有结汇的问题。
|
||||
|
||||
除非,有一种世界货币,它能够流通于各个国家,被各个国家的市场所接受。
|
||||
|
||||
解决汇率问题最好的办法当然是不用换汇。
|
||||
|
||||
比特币能够满足这个需求么?
|
||||
|
||||
不能。比特币有这样一些软肋,我列在了下面。
|
||||
|
||||
其一,没法按需增发,价格波动大。 货币发行量最理想的情况是按市场需求来。如果市场对货币的需求量暴增,但是又没法增加货币发行量来调节,必然会导致货币增值。
|
||||
|
||||
比特币就属于这种情况,它只能靠挖矿来增加,而没有其他手段。那么一旦人们对它的需求增加速度超过挖矿速度,就会出现价格暴涨。
|
||||
|
||||
投机性的行为可能会助长这一点。但是从更长远的维度看,比特币的价值与“市场需求量/比特币流通量”成正比。
|
||||
|
||||
其二,性能低。 比特币基于区块链技术,参与的节点众多。节点多对比特币是有极强的正面价值的,因为参与的节点越多,其被人把控的概率也就越低。
|
||||
|
||||
但是,节点越多,比特币交易的性能也就越低。那么现实中,比特币交易到底有多慢?平均每秒2-3笔交易。
|
||||
|
||||
这意味着,比特币完全无法满足支持世界货币所需的交易频次。
|
||||
|
||||
Libra 币
|
||||
|
||||
虽然比特币并不满足需求,但数字化的世界货币一定会诞生,只不过是谁的问题,这是贸易全球化决定的。
|
||||
|
||||
Libra 币就是冲着成为世界货币去的。在 Facebook 最近发布的《加密货币项目 Libra 白皮书》中提到:
|
||||
|
||||
|
||||
我们的世界真正需要一套可靠的数字货币和金融基础设施,两者结合起来必须能兑现“货币互联网”的承诺。
|
||||
|
||||
|
||||
可见,Libra 币其实不属于 Facebook,只不过是由Facebook 发起的。就像互联网一样,它期望的是一种开放式的架构。
|
||||
|
||||
“互联网”连接了世界上的所有人,但是国与国之间边界导致的“汇率”和“关税”问题,让贸易无法做到真正意义的全球化。
|
||||
|
||||
Libra 币旨在构建“货币互联网”,让贸易真正无国界,它如何做到?我们将在《我看 FB 发币(下):深入浅出理解 Libra 币》这篇文章中进行详细的介绍。
|
||||
|
||||
今天我们重点聊的是 Libra 币将带来什么。
|
||||
|
||||
第一个问题:Libra 币会不会成功?
|
||||
|
||||
我认为它会成功。唯一阻碍它成功的因素是美国政府。毕竟它给世界上任何一个国家的人们都带来了一个巨大的变化:
|
||||
|
||||
|
||||
除了本国的法币,还有一个货币能够用于人们之间的日常交易。
|
||||
|
||||
|
||||
虽然此前已经有比特币也能够做到这一点,但是比特币的低吞吐能力决定了它必然无法成为世界货币。
|
||||
|
||||
Libra 币最直接挑战的是美元的地位。某种意义上来说,在它之前,美元在承担着世界货币的作用。
|
||||
|
||||
但是我认为美国政府会支持 Libra 币。有两个重要的理由支持他们这样做。
|
||||
|
||||
其一,世界货币一定会产生。 与其让它发生在其他国家,不如发生在美国,由美国人来主导这件事情的发生。
|
||||
|
||||
其二,美元的物理属性,决定了美元对交易的渗透在非美元区(法币不是美元的地区)只能在线下,无法支持线上交易。
|
||||
|
||||
但线上交易的比重越来越大,意味着美国需要一个数字货币能够渗透到其他国家。Libra 币刚好满足了这个需求,所以,Libra 币很容易与美元达成结盟。
|
||||
|
||||
一旦美国支持 Libra 币,这件事情就成功了一半。欧洲本来就有欧元,相当于已经有一个自己小范围的世界货币了,货币发行本来就已经和国家政权解绑了。改用 Libra 币带来的冲击并不大。
|
||||
|
||||
搞定了欧美,Libra 币基本上就算成功了。其他地区的国家很难真正去反抗这一潮流。
|
||||
|
||||
为什么?
|
||||
|
||||
不拥抱 Libra 币,意味着放弃由“互联网+Libra币”共同构建的世界自由贸易体系。这对任何一个国家来说,都意味着闭关锁国,放弃经济增长。
|
||||
|
||||
另外搞一个世界货币与之对抗?
|
||||
|
||||
难。货币有很强的网络效应。如果一个国家或地区已经被 Libra 币渗透,另一个货币进来就难了。
|
||||
|
||||
所以如果非要干,最佳的时机是现在。
|
||||
|
||||
假设,另一个世界货币干起来了,全球被分成了两半,一东一西。
|
||||
|
||||
它们之间没有贸易么?贸易用哪个世界币?这带来了新的汇率问题。
|
||||
|
||||
如果可以让企业自由选择,那么这会是大鱼吃小鱼的故事。因此,第二个世界货币很难真正干起来。
|
||||
|
||||
第二个问题:如果 Libra 币成功的话,它会带来什么影响?
|
||||
|
||||
最大的变化,是货币的大一统。就像欧洲已经发生的那样,国家政权和货币脱钩,政府再也无法用货币杠杆来宏观调控市场。
|
||||
|
||||
第二个大变化,是交易的透明化。链上的支撑节点都可以看到全球所有的交易往来。虽然 Libra 币是匿名的身份,但是开通 Libra 账户过程在很多国家很可能是实名的。所以某种意义上来说,交易还是会和现实身份对应起来。
|
||||
|
||||
第三个问题:中国应该怎么应对?
|
||||
|
||||
这个问题,有点像是中国该不该加入世贸组织(WTO)一样。我的答案是:应该认认真真考虑下如何加入,什么时候加入,加入前应该做好哪些准备。
|
||||
|
||||
结语
|
||||
|
||||
总结一下我对 Facebook 发币这件事情的看法。
|
||||
|
||||
|
||||
世界货币一定会诞生,只不过是谁的问题。这是贸易全球化决定的。
|
||||
以前大家看好比特币,但是比特币有软肋。其一是波动大,其二是性能低。
|
||||
Libra 币不属于 Facebook,只不过是由 Facebook 发起而已。它就像互联网一样,是开放式的架构。这也是其他巨头一拍即合,毫不犹豫地去支持的原因。
|
||||
Libra 币会得到美国政府的支持,并逐步渗透到世界各个地区。它不只是流行于 Facebook 的 27 亿活跃用户,也会流行于其他如 PayPal、Booking 等主流服务。
|
||||
“互联网 + Libra 币” 将共同构建全新的世界自由贸易体系。
|
||||
|
||||
|
||||
站在未来看现在,Libra 币将会是极其重大的一个历史节点。我倾向于这样来描绘它的影响:
|
||||
|
||||
|
||||
计算机 => 互联网 => Libra 币
|
||||
|
||||
|
||||
货币有着巨大的网络效应,留给后来者的时间窗口极短。我想,我们都应该思考怎么面对这件事情了。
|
||||
|
||||
|
||||
|
||||
|
133
专栏/许式伟的架构课/热点观察我看Facebook发币(下):深入浅出理解Libra币.md
Normal file
133
专栏/许式伟的架构课/热点观察我看Facebook发币(下):深入浅出理解Libra币.md
Normal file
@ -0,0 +1,133 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
热点观察 我看Facebook发币(下):深入浅出理解 Libra 币
|
||||
你好,我是七牛云许式伟。今天我们接着聊由 Facebook 推动的 Libra 币。
|
||||
|
||||
听到一个陌生概念后,人们往往习惯于把它和自己熟悉的东西做一个类比,方便和他人沟通的时候,用以表达自己的理解。
|
||||
|
||||
那么,和 Libra 最像的东西是什么呢?
|
||||
|
||||
货币储备
|
||||
|
||||
有人说,Libra 不就是腾讯的 Q 币么?但是,这种理解仅仅停留在非常表面的层次。两者共同之处,仅仅是因为两者都是一种虚拟币(甚至不能说它们都是数字货币,因为 Q 币是不能称之为数字货币的)。
|
||||
|
||||
首先,Q 币可以用货币购买,但是没法反向兑换现金。最关键的是,腾讯可以自由发行 Q 币,它说有多少就可以有多少,不需要被监管。所以如果 Q 币可以以稳定的价格兑换现金的话,相当于腾讯可以自由印钞票,这显然不会被政府所允许。
|
||||
|
||||
但是 Libra 并不是这样。Libra 可以自由兑换,另外 Facebook 也不能够自由发行 Libra 币。发行新的 Libra 币时,有两大核心约束。
|
||||
|
||||
其一,发行新的 Libra 币需要保值的资产做为抵押,我们称之为 “Libra 储备”。 什么样的资产可以作为 Libra 储备?被 Libra 协会认可的,稳定且信誉良好的中央银行发行的货币,或者政府货币证券。也就是说,Libra 币锚定的是它认可的“一篮子货币”,而不是某一种货币。
|
||||
|
||||
其二,由 Libra 协会来发行或销毁 Libra 币。 目前 Libra 协会执行这个动作完全是被动的。Libra 生态中,会有一些被授权的经销商,他们负责 Libra 币的买卖。
|
||||
|
||||
这些授权经销商会按照一定的汇率,以某种被 Libra 协会认可的货币,向 Libra 协会买入 Libra 币。这时,这些用于买入的货币就会成为 Libra 储备。
|
||||
|
||||
当然授权经销商也可能卖出 Libra 币,换成他所期望的某种货币。在这种情况下,被卖出的 Libra 币就会被销毁,相应地, Libra 储备就会减少。
|
||||
|
||||
所以你可以看到,Libra 并没有自己的货币政策,总共有多少 Libra 币完全是由市场的供需决定的。
|
||||
|
||||
关于货币储备更详细的内容,请参阅 “Libra 储备”。
|
||||
|
||||
稳定币
|
||||
|
||||
既然 Libra 不像 Q 币,那么它像什么?比特币吗?
|
||||
|
||||
相比 Q 币来说,Libra 与比特币的确更有比较意义,毕竟它们都是一种数字货币。但是我们在 “我看Facebook发币(上):区块链、比特币与 Libra 币” 中,也已经谈到过两者一些根本上的不同。
|
||||
|
||||
如果我们忽略所有的技术细节,单从货币角度来看,比特币和 Libra 最大的区别是价格的波动性。
|
||||
|
||||
通过上面我们对 “Libra 储备” 的介绍,我们很容易知道 Libra 是一种稳定币,它看起来更像是由一些保值资产抵押所形成的 “债券”。而在币圈,大家可能都经常会听到一个词,叫做 “炒币”。
|
||||
|
||||
如果我们细想一下这背后的逻辑,就会知道这个词其实挺神奇的。
|
||||
|
||||
货币的核心价值是什么?
|
||||
|
||||
货币的价值显然不是增值,而是流通。而流通用的货币,最重要的是价格稳定。而币圈则不同,这些各式各样存在的币,现在它们的核心价值变成了 “炒”,这就完全被扭曲了。
|
||||
|
||||
我这么说当然不是认为比特币不好。只不过如果我们真的对比特币好,就应该把关注点放在比特币流通价值的构建上;而不是“喜看币涨”,涨了就奔走相告。
|
||||
|
||||
世界货币
|
||||
|
||||
那么,究竟 Libra 最像什么呢?
|
||||
|
||||
我个人认为,虽然目前的实现机制有所不同,但是 Libra 最像的是欧元。欧元的出现,对欧盟国家之间的自由贸易往来发挥了巨大的贡献。
|
||||
|
||||
欧元和 Libra 的初衷是一样的,都是为了构建跨国的自由经济贸易体系。两者不同之处在于,当前 Libra 还没有央行,不能凭空去发行货币。
|
||||
|
||||
凡事都有两面,“世界货币” 同样是有缺陷的。欧债危机导致欧元贬值,这个教训大家还记忆犹新。它告诉我们,一个国家经济出现问题,如果它采用的是独立的主权货币,那么只是导致本国的货币贬值。
|
||||
|
||||
但是 “世界货币” 意味着大锅饭,它会传导到整个经济体,连带整个经济体的货币贬值。
|
||||
|
||||
那么 Libra 会出现自己的央行么?短期当然不会,当前最重要的是流通,是地盘的扩张。但是长远看只要 Libra 成功了,就一定会有。毕竟,没有 Libra 央行,就没有机会解决 “大锅饭” 的问题。
|
||||
|
||||
所以站在更长远的未来看,今天的 Libra 协会,有可能就会是未来的 “Libra 央行”。
|
||||
|
||||
Libra 协会
|
||||
|
||||
Libra 协会总部位于瑞士日内瓦,协会成员包括分布在不同地理区域的各种企业、非营利组织、多边组织和学术机构。初始协会成员共有28家(如下图),未来计划达到100家。
|
||||
|
||||
-
|
||||
加入 Libra 协会,需要支付不低于 1000 万美元来购买 Libra 投资代币,注意,它并不能简单理解为 Libra 币,两者有很大不同,它有很多特殊的权益。
|
||||
|
||||
其一,整个 “Libra 储备” 的处置权。 比如, Libra 储备到底放在哪里会比较安全靠谱?这是由Libra 协会决定的。但是每个成员是不是都是一票?并不是,票数是由这个成员持有的 Libra 投资代币数量决定的。具体规则,我在下面会讨论。
|
||||
|
||||
其二,整个 “Libra 储备” 的利息,会被用来支撑 Libra 协会的日常运转。 比如工资和奖金激励,也会用于派发分红。分红会按照持有的 Libra 投资代币的多少来分配。
|
||||
|
||||
其三,各类事务决策的投票权。 权重按持有的 Libra 投资代币来计算,每 1000 万美元有 1 个投票权,但是为了防止投票权过于集中,任何成员的投票权不能超过总票数的 1%。
|
||||
|
||||
超出部分的投票权将由 Libra 协会的董事会重新分配,比如授予具有社会影响力的合作伙伴(称为 SIP)或研究机构,前提是:他们有能力并致力于验证节点的运作,从而参与治理,并且确实无法作出最低 1000 万美元的投资。
|
||||
|
||||
其四,运行验证节点。 这一点可以被看作义务,但也可以看作权益。义务角度来说,显然这事是需要 IT 成本的,如果连这都做不到,就会被剔除出去。从权益角度来说,那么多的交易数据都被你拿到了,这是多么有价值的数据。
|
||||
|
||||
关于 Libra 协会储备更详细的内容,请参阅 “Libra 协会”。
|
||||
|
||||
发展目标
|
||||
|
||||
当前 Libra 的验证节点是有限制的,是许可型的,也就是所谓的 “联盟链”。但是,未来它会向非许可型治理和共识节点运营转变,降低参与的准入门槛,并减少对创始人的依赖。
|
||||
|
||||
Libra 将逐步进行网络的开放,变成所谓的 “公链”。这时,新成员也能够通过投资 1000万美元来自动获得验证节点的运行许可,并持有 Libra 投资代币,从而分享网络权益(但是我估计投票权之类是没有的,只有投资收益)。这部分的详细政策,或许未来才会逐步明朗。
|
||||
|
||||
Libra 协会致力于尽可能减少协会的干预权。比如完全通过市场机制来调节 Libra 币的供需。所以除了说服更多人加入 Libra 协会,协会最重要的工作是确定 Libra 的技术演进路线。
|
||||
|
||||
但这一点真实情况下显然并不会那么理想。货币互联网和互联网不同的是,它管的毕竟是钱。一旦 Libra 网络发展壮大,一些货币相关的治理问题就会暴露出来。
|
||||
|
||||
降维打击
|
||||
|
||||
Libra 的影响面绝对超乎大部分人的想象。我们先看看一些具备技术背景的知名互联网公司 CEO 是怎么看的。具体参见下面的截图。
|
||||
|
||||
|
||||
|
||||
显然无论王兴还是王小川,都高度重视并反复评估 Libra 带来的影响和自己应该采取的行动。
|
||||
|
||||
Libra 带来的打击是全方位的,某种程度上来说甚至是极难抵御的降维打击。我们不妨从以下这些维度看。
|
||||
|
||||
|
||||
弱小国家的货币主权会不会就此被取代?
|
||||
中国的汇率管控会不会失效?就算没有失效,会不会变成闭关锁国?
|
||||
Libra 是否会成为美国取代美元的新货币霸权?
|
||||
从企业与企业竞争的角度,这会不会成为 Facebook 对微信、支付宝的降维打击?
|
||||
|
||||
|
||||
想完影响面想对策。从对策的角度来说无非两种,一是对抗,一是谋求对等权力下的协作。当然,这两条路都不会太容易。
|
||||
|
||||
结语
|
||||
|
||||
今天,我们从分析 Libra 的运行机制重新去理解 Libra 币。这里面的关键是:理解 Libra 协会究竟如何工作的。
|
||||
|
||||
我们没有分析 Libra 的技术细节,比如它怎么改变区块链的底层实现机制,又为什么要引入 Move 语言。如王兴所说,评判 Libra 的实现技术好不好,就好比评价美元钞票的印刷技术精细不精细。这事本身是有价值的,但不那么关键。
|
||||
|
||||
为什么我会写这两篇文章?
|
||||
|
||||
站在未来看现在,Libra 币将会是极其重大的一个历史节点。我倾向于这样来描绘它的影响:
|
||||
|
||||
|
||||
计算机 => 互联网 => Libra 币
|
||||
|
||||
|
||||
货币有着巨大的网络效应,留给后来者的时间窗口极短。我想,我们都应该思考怎么去面对这件事情了。
|
||||
|
||||
|
||||
|
||||
|
95
专栏/许式伟的架构课/用户故事站在更高的视角看架构.md
Normal file
95
专栏/许式伟的架构课/用户故事站在更高的视角看架构.md
Normal file
@ -0,0 +1,95 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
用户故事 站在更高的视角看架构
|
||||
|
||||
你好,许式伟老师的架构课已经更新了一段时间,不少同学反馈这段时间的学习很有收获,于是我们邀请了Aaron同学来和我们做一次分享,看看他这阶段都是怎样学习课程的,又有哪些收获。今天这节课为加餐,形式和正式的课程内容不同,需要你阅读文稿来学习,好,我们开始吧。
|
||||
|
||||
|
||||
我是 Aaron,一名 Python 软件工程师, 目前主要从事运维开发DevOps工作。
|
||||
|
||||
一般开发的系统是内部使用的运维系统,最近主要的工作就是将产品需要的一些功能嵌入到类似CMDB系统当中,比如SaltStack、ELK(EFK)、Zabbix 等。
|
||||
|
||||
目前,运维自动化的困境是运维研发资源能力的不足,底层自动化的能力可以通过IaaS公有云来解决,但是在OS之上的运维自动化都是通过一些开源工具来解决的。开源工具的引入,加大了维护的难度和复杂度,而且,很多平台的可扩展能力非常薄弱。
|
||||
|
||||
理解这些开源工具的内部实现原理、优缺点,了解可以改善的地方,实现项目的深层次进化,是自己的额外追求。因为这个追求,学习架构设计就很有必要,从架构的思维来看这些迭代了五六年的开源项目,可以看到开源软件开发者的迭代开发流程和思考。发上等愿,结中等缘,享下等福,运维开发在公司偏辅助业务,我希望努力学习基础架构来实现自身价值。
|
||||
|
||||
我为什么要学习架构课?
|
||||
|
||||
极客时间推出《许式伟的架构课》的时候 ,我正在工作时间之外学习操作系统、计算机网络等课程。看了这门课的介绍觉得挺好的,知道作者是七牛云的许式伟老师,因为自己工作方向的原因,经常会接触到关于Go语言布道师(许式伟老师)的一些介绍,对于许老师非常崇拜 ,特别是许老师多年前就预测到 Go 语言会称霸云计算这一点,我特别佩服。
|
||||
|
||||
另外,我也一直希望能借助许老师的视角和高度,来看看架构这件事。
|
||||
|
||||
如何将用户的需求,一步步分解为程序员要实现的功能点,并展现给用户,这是架构师的职责。设计高可用、高性能、高并发的可靠系统需要匠心,悟心,保持谦和求取的心态。架构思维并不难 ,但是成为优秀的架构师却不易。我很想知道在许老师这样的高度上,他对架构的理解是什么样的。
|
||||
|
||||
我也希望可以通过对专栏的学习,能在工作实践中有所成长,成为优秀的架构师。所以我购买了《许式伟的架构课》,并开始学习。
|
||||
|
||||
我是怎样学习专栏的?
|
||||
|
||||
我学习这个专栏没有什么特别之处,无非做到了三点:反复学习、动手实践、留言打卡坚持。
|
||||
|
||||
许老师的课程是每周二、五更新,一般情况下我都会在当天先通读一下老师的文章 ,特别好的文章会通读好几遍 ,周末的时候会再拿出来细看 。 平时上下班路上的细碎时间,也会抽空看一次 、听一次来巩固学习。
|
||||
|
||||
具体算下来,整个学习的过程,基本上也和别的同学分享的类似,基本可以读懂的文章,学习两到三次;不是很精通的,云里雾里的,会至少看到五六次;听一遍不够,再看一遍也不行,那就再多读几遍,查看留言,看看其他同学的留言内容,来检验自己是否理解了,问题是否完全解决了。
|
||||
|
||||
许老师专栏中提到的实战项目, 像JS和Golang 的项目 ,我都要自己仔细敲一遍 ,落实到代码上。因为如果只是浮光掠影的话 ,不会理解到精髓的。知识还是要下苦工夫才能消化。
|
||||
|
||||
另外,我基本一直坚持学。可能很多同学可以看到我在老师课程下坚持留言,因为极客时间的课程我购买了不少,有些跟了一段时间就暂时搁置了,所以我在跟许老师的架构课程之初,就定下了Flag:一定要在老师正式的课程下多留言打卡,虽然留言质量未必很高,但是至少有一种方式可以让我尽量坚持下去。
|
||||
|
||||
专栏中最有收获的文章是哪几篇?
|
||||
|
||||
介绍几篇我个人觉得很有收获的文章:
|
||||
|
||||
|
||||
《[01 | 架构设计的宏观视角]》
|
||||
|
||||
|
||||
很多开发人员对众多系统背后是如何工作的,原理一知半解 ,知其然不知其所以然,成为架构师就是成长为造房子的建筑工程师,需要宏观的全局掌握能力。
|
||||
|
||||
|
||||
《[17 | 需求分析(上)]》和《[18 | 需求分析(下)]》
|
||||
|
||||
|
||||
为什么要做需求分析呢?一是为了满足用户需求,二是满足边界的需要 ,三是架构设计的需要,防止过度设计, 把简单的事情复杂化。
|
||||
|
||||
如果只是被动接受产品需求,以按图索骥的方式做架构师,是不足以成为顶级架构师的,用户需求的深层理解是很难传递的。
|
||||
|
||||
产品设计过程需要架构师的深度参与,而不是单向的信息传递。产品是桥,一端连接用户需求 ,一端连接了先进的技术。
|
||||
|
||||
产品经理和架构师其实是一体两面,都需要关心用户需求和产品定义。架构师需要三分之一的精力(我目前感觉可能会更多一些)在需求分析上。
|
||||
|
||||
许老师介绍的这些点,都很有价值:
|
||||
|
||||
“心态第一,装着用户,刨根究底,找到根源需求,理清需求,对需求进行归纳整理。需求分析,用户的需求反馈到架构师那里,需要对功能进行拆解,对于部分超前的需求,分期进行实现。”
|
||||
|
||||
|
||||
《[22 | 桌面程序的架构建议]》
|
||||
|
||||
|
||||
老师对 MVC 的讲解,当时看完了之后感觉到醍醐灌顶,看到下面留言说 ,这一篇文章就值回“票价”,我也是同样的感受。
|
||||
|
||||
后面的我就不一一举例了。
|
||||
|
||||
有哪些好的学习方法?
|
||||
|
||||
学习的方法无非就是坚持,坚持,坚持 !夯实基础,夯实基础,夯实基础!
|
||||
|
||||
去年的时候看到一位深度学习的大佬 Lan Goodfellow 的访谈, 说他人工智能本科阶段和博士早期阶段曾经多次咨询吴恩达老师寻求建议。 吴恩达建议他彻底精通这些基础知识和技能 (编程、调试、线性代数、概率论等等), 特别是基础数学。
|
||||
|
||||
以前的他特别不理解这个建议,觉得这些实在是很无聊 ,他以为吴恩达能建议他去学习超实数或者类似的东西 ,实践几年后,他发现老师当时给的建议实在是太正确了。
|
||||
|
||||
学习没什么捷径,不存在什么一蹴而就的高超技术,要真的能稳得住啃那些基础,能沉下心坚持再坚持。
|
||||
|
||||
夯实基础,坚持下去,就几个字,做到不易,愿你我共勉。每个开发者都希望能够有银弹,架构设计的其中一种是基础架构,涉及操作系统、分布式系统、嵌入式系统、数据库、计算机网络等,这些都需要扎实的基础作为后盾。研发底层基础设施,这个是自己近期的目标。
|
||||
|
||||
有很多人说 ,第一份工作带给你的能力,眼界,和圈子是非常重要的,一个人的能力决定他的上限,圈子决定了他的下限。
|
||||
|
||||
我对毕业之后的第一份工作没有足够的重视 ,发展不佳,但是非常庆幸参加了许老师的课程, “受益终生”四个字不足以完全表达我的谢意。 希望老师的公司发展蒸蒸日上, 也祝一起在极客时间学习的我们在努力坚持之后会心想事成 !
|
||||
|
||||
平时对外输出文字的机会不多 ,些许胡言,希望能够给大家抛砖引玉,谢谢大家。
|
||||
|
||||
|
||||
|
||||
|
202
专栏/许式伟的架构课/答疑解惑想当架构师,我需要成为“全才”吗?.md
Normal file
202
专栏/许式伟的架构课/答疑解惑想当架构师,我需要成为“全才”吗?.md
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
答疑解惑 想当架构师,我需要成为“全才”吗?
|
||||
你好,这里是极客时间编辑部。
|
||||
|
||||
不知不觉,“许式伟的架构课”专栏已经更新了3个多月,我们的后台收到了同学们数以千计的留言。许式伟老师每天都在实时关注着留言并回答同学们的问题,同时根据同学们的留言不断优化调整课程的设置。
|
||||
|
||||
老师和同学在留言区的互动也十分热闹精彩,今天我们就精选出一批留言,一起来看一看。
|
||||
|
||||
精选问答
|
||||
|
||||
1.老师好,人精力有限,如果什么都懂,那不是不精了?通才还能做架构师吗?还是“一专多能”,先“专”,精通一样;再“多能”,了解其它技术?
|
||||
|
||||
答:挺好的问题。架构师绝对不是要把自己打造为全才。架构师掌控全局的核心思想是打通经络,让自己的内力在全身自然流通,浑然一体。在不影响理解的情况下,你需要放弃很多实现细节的专研,但有一天你需要细节的时候,你能够知道存在这些细节,并且快速钻研进去。
|
||||
|
||||
2.许老师,自己现在已经工作快三年了,想往架构师这个方向走,但现在自己有些迷茫,接触到的技术也算挺多了,但不知道该如何入手架构师,之前您也提到过先广度然后深度,但我想问达到什么算广度够了,怎么进行深度学习?
|
||||
|
||||
答:架构师核心是把知识串起来,构建一个完整的认知,不留疑惑。大部分知识是不需要深入细节的,只在你需要的时候深入,但深入的时候要很深。
|
||||
|
||||
3.如何来确定需求中哪些是稳定的?对架构角度,关注需求到什么层次?
|
||||
|
||||
答:挺好的问题。需求分析的重要性怎么形容都不过分。准确的需求分析是做出良好架构设计的基础。很多优秀的架构师之所以换到一个新领域一上来并不一定能够设计出好的架构,往往需要经过几次迭代才趋于稳定,原因在于新领域的需求理解需要一个过程。除了心里对需求的反复推敲的严谨态度外,对客户反馈的尊重之心也至关重要。
|
||||
|
||||
4.老师好,我有三个问题。
|
||||
|
||||
|
||||
现在运维开发基本上都用Go,Python慢慢变少了,Java也少用了……现在运维开发是要学Go吗?
|
||||
架构师要学数据结构和算法吗?很多都说算法是“内功”,中小公司好像学了都基本用不到。
|
||||
现在不是流行Docker+k8s、微服务、DevOps、AI等,那些主流技术都要了解吗?OpenStack云计算这两年基本不讲了,是否不用学习?
|
||||
|
||||
|
||||
答:关于你的三个问题,我的意见是下面这样的。
|
||||
|
||||
|
||||
学Go挺好,建议学,生产效率很高的开发工具。
|
||||
“算法用不到”其实更准确的说法是“想不到”,或者是已经有人实现了你只需要调用,不需要自己实现。但是只有你知道了背后的道理,你才能明白算法对应的限制在哪里,什么情况下应该用什么算法。
|
||||
高阶的技术可以按需学,按精力学,更根本的还是要打好基础,这也更有助于你判断是否应该深入学习某些技术。
|
||||
|
||||
|
||||
5.“你可以发现,引入了输入输出设备的电脑,不再只能做狭义上的计算(也就是数学意义上的计算),如果我们把交互能力也看做一种计算能力的话,电脑理论上能够解决的计算问题变得无所不包。”
|
||||
|
||||
交互能力也看做一种计算能力吗?这句话应该怎么理解呢?这种交互不就是输入和输出?哪有计算?
|
||||
|
||||
答:广义的计算包含有副作用的函数(有IO的函数)。因为数据交换本身也是计算的需求,否则计算没有办法与现实世界相互作用。任何工具都需要解决现实问题才有用。计算器还有交互按钮呢,并不是只有纯正的计算。
|
||||
|
||||
6.我要做一个最小机器人系统,需要考虑需求的变化点和稳定点。该怎么考虑呢?
|
||||
|
||||
答:挺典型的问题。这个问法是一种典型的需求陈述误区。
|
||||
|
||||
描述需求需要有几个典型的要素:
|
||||
|
||||
|
||||
用户,面向什么人群;
|
||||
他们有什么要解决的问题;
|
||||
我解决这个问题的核心系统。
|
||||
|
||||
|
||||
只有满足这几个要素的需求才能进一步讨论变化点和稳定点。最小机器人可能符合上面的第三点,但是用户人群和要解决的问题没有描述,也就无法进一步去思考到底哪些因素是稳定的,哪些是易变的。
|
||||
|
||||
7. 编程框架和编程范式具体有什么区别呢?感觉它们都具备约束、规范的作用。
|
||||
|
||||
答:最主要的差别是:编程框架通常是领域性的,比如面向消息编程是多核背景下的网络服务器编程框架;编程范式则是普适性的,不管解决什么领域的问题都可以适用。
|
||||
|
||||
8.老师提到了如果需要重修数据结构这门课程,大学里面学的数据结构是不顶用了。那应该学习什么呢,您可以给个建议吗?
|
||||
|
||||
答:这方面的资料不太多。可以给你一个我当年翻过的资料: Purely Functional Data Structures
|
||||
|
||||
|
||||
https://www.cs.cmu.edu/~rwh/theses/okasaki.pdf
|
||||
|
||||
|
||||
你可以参考看看。
|
||||
|
||||
9.老师您好,我不太理解您说的继承是个过度设计的原因,我目前在架构过程中大量使用了继承,而且我也觉得继承功能将我的代码功能高度抽象化,给我带来了很大的方便。我想咨询下您如果不是用继承的话,用什么方法替代继承的功能呢?
|
||||
|
||||
答:建议继承只使用接口继承;正常情况下,优先用组合;当然因为大部分语言的组合功能不够强大,有时候从便捷性的角度继承可以适度使用,但是应当意识到如果过度使用继承对工程来说是有害的。
|
||||
|
||||
10.老师授课知识的角度很有深度,更贴切地说是一种思维方式,这种深度思考,从事情的本质重新推演与复盘的思考方式是很值得学习的。因为我们大部分人应该都没有想过自己去重新设计一个计算机的实现。不知道我这么理解的对不对。
|
||||
|
||||
答:从无到有到万物,我们这个课的脉络之一就是重新从零构建整个信息世界,这一点在开篇词中提到过,这一点非常非常关键。另一个脉络是架构思维的递进,这一章重点是需求分析。这两个脉络相辅相成,交织在一起。
|
||||
|
||||
11.许老师,您好,虽然自己是科班出身,但是对于下面这个问题困扰了我很久。
|
||||
|
||||
|
||||
一般来说程序的运行需要OS的支撑,那么在BIOS之前,选择运行哪个操作系统那一段程序是怎么运行的?再问一句,编译器可以独立于操作系统运行吗?
|
||||
可以用C语言去实现很多其他的语言,比如说Python、Go等,那在C语言之前,这么一直追寻下去,会衍生成鸡生蛋的问题。但是编程语言又具有自举的功能,那自举是怎么实现的?比如目前版本的Go核心实现中,很多是用Go本身实现的,它是怎么做到自己编译自己的?
|
||||
|
||||
|
||||
答:先回答你的第一个问题,程序运行不需要操作系统支持,有BIOS支持就可以(把控制权交给它)。编译器可以独立于操作系统存在,而且它应该先于操作系统产生。
|
||||
|
||||
接下来是第二个问题,语言诞生的过程是这样的:机器码 =>汇编 =>C =>C写的汇编、C写的C(自举)。当然这个过程不需要每次新架构的CPU或操作系统都重新来一遍,因为人是聪明的,发明了交叉编译这样的东西,C =>新平台的C,这样就一下子完成整个语言的进化了。
|
||||
|
||||
12.许老师:选择某种语言无关的接口表示;能举个例吗?
|
||||
|
||||
答:先看看是网络协议层的接口,还是跨语言的二进制接口。
|
||||
|
||||
前者比如protobuf之类就挺好,后者可以了解一下IDL之类的东西,不过我觉得都有点重。如果要跨语言,我的建议在网络协议层跨,或者用操作系统的动态库机制(有点原始但很轻);如果语言内的接口,就别太复杂了,用语言自己的机制挺好的。
|
||||
|
||||
13.请问每个应用的虚拟内存地址是怎么分配的?起始地址都是0吗?函数F可以跨多个虚拟内存页吗?
|
||||
|
||||
答:操作系统会保留一个地址空间,0通常也在保留区间内,因为0开始往往是中断向量表的地址,其他的地址区间怎么分配其实应用自己说了算。函数和数据都可以跨内存页。
|
||||
|
||||
14.老师您好,有两个问题希望解答。
|
||||
|
||||
|
||||
淘汰的内存页数据保存在哪里;是保存在外置存储设备中吗;
|
||||
CPU加载对应程序的代码段到内存中,那么CPU是如何知道这个对应程序的代码段在什么位置的呢?
|
||||
|
||||
|
||||
答:第一个问题:是的,保存在外置存储中。对于unix系的系统往往是swap分区;windows则是一个隐藏属性的.swp文件。-
|
||||
第二个问题:代码段在哪里,是操作系统约定的,因为负责加载的人是操作系统,它设计程序文件的数据格式。
|
||||
|
||||
15.使用Java四年了,看到封装,继承,多态的描述,特别精准,又有了更深刻的理解。不了解Go语言,比如有一个表单的基类,里面有基本的处理,子类继承这个基类,有自己特殊的实现。这种情况,如何用组合实现呢?
|
||||
|
||||
答:这是受继承思维的影响了。其实继承实现了代码复用和多态两个东西,揉在一起。在Go里面,组合实现代码复用,接口实现多态,彼此完全独立,非常清晰。
|
||||
|
||||
16.请问一下CPU是如何检查是否有中断的。是怎么及时知道发生了中断?每执行完一条指令都去检查一次吗?
|
||||
|
||||
答:挺好的问题。硬件中断和软中断不一样。硬件中断你可以理解为总是会定期检查。软中断本身是一条指令,所以不存在检查这样的概念。
|
||||
|
||||
17.交叉编译是什么意思,不是很理解,老师能讲讲吗?
|
||||
|
||||
答:其实理解清楚一个实质:编译器就是把高级语言翻译成为机器码,更抽象说,它其实就是格式转换器。
|
||||
|
||||
目标格式是不是编译器正在运行的环境并不重要,只不过如果目标格式刚好是当前机器的CPU+操作系统,那么目标格式就可以直接执行,否则就编译出一个当前环境下无法执行的目标格式,这种情况就叫交叉编译。
|
||||
|
||||
18.关于外存管理,有个问题从之前就困扰我。
|
||||
|
||||
磁盘的IO是由CPU完成的吗?但之前见到的说法是“CPU只能操作内存”。既然今天又提到了这个问题,文中提到“大量的磁盘 IO 操作,非常占用 CPU 时间”,那这两种说法是否矛盾?
|
||||
|
||||
还想知道磁盘中的数据是怎么被加载到内存上来的呢?另外,更多的文章是说,“CPU的速度远远大于磁盘IO,CPU经常需要‘等待’磁盘IO”,这明显也是一种将CPU和外存割舍开的一种说法,而且按这种说法,CPU不光无需分配很多时间片给IO,而且还有很多“等待”时间。这也和本文中“非常占用CPU时间”相矛盾吧?
|
||||
|
||||
答:所有外设CPU都统一基于数据交换(IO)的方式操作。CPU并不知道数据的含义,但是设备的使用方和设备知道。
|
||||
|
||||
这种情况下你可以简单理解CPU只是一根网线,但是很重要的一点是它让设备使用方和设备可以交互。CPU并不负责磁盘IO,但是它要等它结束以接收数据。这方面当然也有一些新技术出现改善这一点,可以想一想可能的优化路径,这里不表。
|
||||
|
||||
19.有一个疑问:协程属于用户态的线程,它跟线程之间怎么对应呢?协程之间也需要切换,那线程切换的那些成本它一样有啊,没想明白它的优势在哪。
|
||||
|
||||
答:从单位时间成本来说,有一定优势但也不会特别大。主要少掉的代价是从用户态到内核态再回到用户态的成本。
|
||||
|
||||
这种差异类似于系统调用和普通函数调用的差异。因为高性能服务器上io次数实在太多了,所以单位成本上能够少一点,积累起来也是很惊人的。
|
||||
|
||||
20.这种对需求的前瞻性探索挺重要,但同时感觉也是最难的,应该如何培养呢
|
||||
|
||||
答:很多时候是思维方式的转变。首先要尝试去做前瞻,预测错了并不可怕,但可以事后复盘到底是缺失了什么重要的信息让你判断出现了什么偏差。
|
||||
|
||||
21.隐隐感觉到架构的主要难点在于对需求的前瞻性判断,这要求的不仅仅是技术能力。目前几乎所有的架构课程,都是基于确定的需求来讲技术架构,例如秒杀系统怎么做高可用高并发。不知道我这么理解对不对。
|
||||
|
||||
答:架构在于创造,如果你从事的事情总是重复别人,那这个公司又有何价值?即使有所参考,也应该有自己的精气神,这个精气神是需要架构师把它干出来的。
|
||||
|
||||
精选学习留言
|
||||
|
||||
恭喜@有铭和@Enthusiasm 两位同学,你们的留言被选为精选留言,极客时间将送出价值99元的专栏阅码一份。1个工作日之内,工作人员会与你取得联系。
|
||||
|
||||
@有铭 同学留言
|
||||
|
||||
对象范式的原始概念其实根本不包括类和继承,只有1.程序由对象组成,2.对象之间互相发送消息,协作完成任务。
|
||||
|
||||
最初世界上第一个面向对象语言是 Simula-67,第二个面向对象语言是 Smalltalk-71。
|
||||
|
||||
Smalltalk 受到了 Simula-67 的启发,基本出发点相同,但是最大的不同是Smalltalk是通过发消息来实现对象方法调用,而Simula是直接调用目标对象的方法。
|
||||
|
||||
Bjarne Stroustrup 在博士期间深入研究过 Simula,非常欣赏其思想,C++的面向对象思路直接受其影响,因为调用目标对象的方法来“传递消息”需要事先知道这个对象有哪些方法,因此,定义对象本身有哪些方法的“类”和“继承”的概念,一下超越了对象本身,而对象只不过是类这个模子里造出来的东西,反而不重要。
|
||||
|
||||
随着C++的大行其道,继承和封装变成了面向对象世界的核心概念,OOP 至此被扭曲为 COP ( Class Oriented Programming,面向类程序设计)。
|
||||
|
||||
但是COP这套概念本身是有缺陷的:每个程序员似乎都要先成为领域专家,然后成为领域分类学专家,然后构造一个完整的继承树,然后才能 new 出对象,让程序跑起来。
|
||||
|
||||
到了 1990 年代中期,问题已经十分明显。UML 中有一个对象活动图,其描述的就是运行时对象之间相互传递消息的模型。1994 年 Robert C. Martin 在《 Object-Oriented C++ Design Using Booch Method 》中,曾建议面向对象设计从对象活动图入手,而不是从类图入手。
|
||||
|
||||
而 1995 年出版的经典作品《 Design Patterns 》中,建议优先考虑组合而不是继承,这也是尽人皆知的事情。
|
||||
|
||||
这些迹象表明,在那个时候,面向对象社区里的思想领袖们,已经意识到“面向类的设计”并不好用。只可惜他们的革命精神还不够,Delphi 之父在创建.Net Framework 的时候,曾经不想要继承,在微软内部引起了很大的争议,最后是向市场低头,加上了继承。
|
||||
|
||||
2000 年后,工程界明确提出:“组合比继承重要,而且更灵活”,Go和Rust也许是第一批明确的对这种思路进行回应的语言,它们的对象根本不需要类本身来参与,也能完成对象范式的多态组合。
|
||||
|
||||
历史让 C++走上了舞台,历史也终将让 COP 重新回到 OOP 的本来面目
|
||||
|
||||
@Enthusiasm 同学学习笔记
|
||||
|
||||
总结:设计系统架构的前提是用户需求分析,用户需求包括分析出稳定需求点和变化需求点。从功能上看,稳定需求点一般是实现偏核心需求的需求点,变化需求点往往是实现偏扩展性需求的需求点。
|
||||
|
||||
从层次结构上看,稳定需求点往往在系统层次的底层,而变化需求点往往在更加抽象层(上层)。从从属关系上看,稳定点需要提供功能给变化点使用,变化点调用稳定点提供的功能。从时间顺序看,稳定需求往往先现是变化点实现的基础,变化点通过调用已经实现的稳定点提供的功能来实现更为抽象的功能。
|
||||
|
||||
系统架构类似于一个栈的结构,人机交互(变化点)放在栈顶,底层工作(稳定点)置于栈底。
|
||||
|
||||
这节课程让我联想到网络中的OSI 7层模型。大概其也体现了这种软件架构思想。好处就是架构清晰,职责明确,功能规范等等。
|
||||
|
||||
以往我认为的架构设计类似上面的描述,描述起来类似按自顶向下顺序,采用分治思想完成。但许老师的方法又有些巧:架构好比搭积木,许老师是先有了很多积木(需求点),然后把再确定这些积木放在哪一层次的格子里。这简化了架构设计的难度,好比用市场经济代替计划经济,很有趣。
|
||||
|
||||
架构设计博大精深,灵活多变,初学课程的我们,对架构设计的学习,也只能算是盲人摸象。
|
||||
|
||||
|
||||
|
||||
如果你在课程中有看不懂的地方,有想解答的架构问题,或者想分享的实战经验,都可以在文章下留言,如果你的留言被选中作为精选留言,我们将会为你送出价值99元的阅码一份。欢迎留言,与许式伟老师一起交流讨论,教学相长,共同精进。
|
||||
|
||||
|
||||
|
||||
|
138
专栏/许式伟的架构课/结束语放下技术人的身段,用极限思维提升架构能力.md
Normal file
138
专栏/许式伟的架构课/结束语放下技术人的身段,用极限思维提升架构能力.md
Normal file
@ -0,0 +1,138 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
结束语 放下技术人的身段,用极限思维提升架构能力
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
这个专栏从去年 4 月份至今已经有 10 个月左右的时间,到了要和你说再见的时候了。感谢你的一路相伴,感谢你的坚持。也希望这些内容能够对你有所帮助。
|
||||
|
||||
从工程角度来说,架构师的存在几乎是一种必然。传统项目工程也有架构师的角色,只不过软件工程有其特殊性,它快速变化,充满了不确定性,所以架构师的重要性的比重会被进一步放大。
|
||||
|
||||
但是如何才能成为优秀的软件工程架构师?
|
||||
|
||||
传统的架构图书往往从架构思维开始。但是,我认为它们错了。这里面最关键的问题在于:
|
||||
|
||||
|
||||
架构并不是 “知识点”。
|
||||
|
||||
|
||||
架构思维的确非常非常重要。但是,熟读架构思维并不足以让人成为一名优秀的架构师。
|
||||
|
||||
关于这一点,我经常拿中国传统的武学文化做类比。武功招式可以精确传授,是 “知识点”,掌握了就是掌握了,理论上可以做到分毫不差。但是,架构不是武功招式。它更像内功,它不是 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 播放器。
|
||||
|
||||
在代码量非常大的时候,人的脑容量就完全无法把这个实现装到头脑中。这时 “规格重于实现” 背后的意义就完全体现出来了。通过规格串起整个业务系统,以此把业务系统装到脑子里,这就是很朴素的架构 “骨架” 思维。
|
||||
|
||||
这不是一个假想实验。
|
||||
|
||||
它是我的亲身经历。这段经历启发我意识到极限思维对架构能力提升的重要性。
|
||||
|
||||
架构没有最好,只有更好。在极有限的上机时间里,在没有电脑的情况下,我们只能选择把更多的逻辑装进脑子里。
|
||||
|
||||
这个过程还可以更进一步。我们不断训练自己对不同业务领域的架构范式的理解。直至最终,我们头脑中可以装得下整个信息科技的骨架。
|
||||
|
||||
到那时,单就架构能力而言,你就是最顶级的架构师了。
|
||||
|
||||
备注:我在文末准备了一份调研问卷,也欢迎你点击下方的图片参与调研,期待你的反馈。
|
||||
|
||||
](https://jinshuju.net/f/zxjsq8)
|
||||
|
||||
|
||||
|
||||
|
85
专栏/许式伟的架构课/课外阅读从《孙子兵法》看底层的自然法则.md
Normal file
85
专栏/许式伟的架构课/课外阅读从《孙子兵法》看底层的自然法则.md
Normal file
@ -0,0 +1,85 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
课外阅读 从《孙子兵法》看底层的自然法则
|
||||
你好,我是七牛云许式伟。
|
||||
|
||||
近日在读《孙子兵法》,颇有感触。作为我国现存第一部,也是世界最早的军事著作。《孙子兵法》一直为众多政治家、军事家及企业管理者膜拜,被当作军事外交、企业管理的圣典。
|
||||
|
||||
时至今日,新经济、科技和社会结构下,很多人认为我们身处一种复杂社会,需要全新的军事、商业和公共策略。然而,不管远古农耕刀兵还是高科技信息社会竞争,时代、人文环境和科学技术的变迁,并不能改变人类社会生存竞争所依赖的基础系统环境和人性思维的规则。
|
||||
|
||||
英国空军元帅斯莱瑟曾说过:“孙武的思想有惊人之处——把一些词句稍加变换,他的箴言就像是昨天刚写出来的。”
|
||||
|
||||
《孙子兵法》讲的是战争,骨子里包含的却是人类社会生存竞争的底层规则。案例会变迁,背后的道理却很难过时。我们作为架构师,需要做到对世界的认知可宏观、可微观。从对宏观的理解来说,背后所依赖的正是我们对这些底层自然法则的体悟。
|
||||
|
||||
明道
|
||||
|
||||
《孙子兵法》讲的是规则。它通过战争,结合环境和人性探讨竞争中生存的规则;然后,在确定目标下,它又将战争行为分解成为一系列细分的规则。
|
||||
|
||||
《始计篇》开端,全书先讲总规则,就是“道”。“道者,令民与上同意也,故可以与之死,可以与之生,而不畏危。”如同国家有纲领,企业必须明确愿景,使命或是价值观一样。明心方能正道,明道方知力之所指。事实上,明白道之所求,术方能有指引,追随者才能有方向、有激情。对国家、企业而言“道”不仅是方向,还是战斗力和吸附力的来源,明道和传播非常重要。
|
||||
|
||||
“慎战”。“兵者,国之大事,生死之地,存亡之道,不可不察也。”战争和企业竞争涉及系统存亡关系重大,是管理者每日都要考虑的事情。“自古知兵非好战。”这种行为不是个人名利或得失,而是关系到万户千家,或者全体员工和家庭,必须慎之又慎,不可凭个人好恶,更不可好大喜功和拍脑袋。“上兵伐谋”、“不战而屈人之兵”。要牢记初心,保存敬畏和责任感,求胜同时要避免己方生命和资源的巨大消耗。“善战者,无智名,无勇功。”正如“善弈者通盘无妙手”,或是扁鹊治病于未发,“立于不败之地”非常之关键。
|
||||
|
||||
“避害第一”,先考虑失败的后果;“先胜后战”,不打无把握的战争。“因利而战”,考虑成本和结果;“一战而终”,准备要充分,动手要迅捷,毕其功于一役。
|
||||
|
||||
“兵无常势,水无常形。”环境、资源条件,心态、优劣势这些都可能随时变化,不能教条主义,须将策略灵活应用于变化的实际。满口教义,如赵括那般纸上谈兵,一棒子打死算了。“君将士卒皆有其道”。公司内也应各司其职,人人满口战略、创新和文化,“所有人都举着旗帜,他们用哪只手来战斗?”这样的公司很危险。
|
||||
|
||||
庙算
|
||||
|
||||
“先胜后战”,就要“未战先算”。战与棋,一样考的算力,所谓政治经济环境人心都要纳入数据输入。
|
||||
|
||||
庙算,非治一战或局部。而是知己知彼综合所获数据,依据规则全面分析,很像是系统竞争成败的“大案牍数”。
|
||||
|
||||
庙算策略,不是单一事件的静态考量,而是复杂动态的综合考量。五事,道、天、地、将、法;七计,主、将、天地、法令、兵众、士卒、赏罚都要纳入进来。知己还要知彼。内外部环境,天时地形人才人心钱粮规章,所有影响战争胜负的东西,一切可以数据化,能算计的全都计算进去。
|
||||
|
||||
依据庙算可确定“势”,依据“势”决断是否有利并采取相应行动。拥有这样的洞察和决断力,知道了什么时候该打什么时候不该打,真正战争行为才会很简单。战争中要“致人而不致于人”。曾说“朕观诸兵书,无出孙武”的李世民,还有林彪都非常擅长集中优势兵力,通过忍耐和调动对手,“多方以误”,引敌人失误而获取最终胜利。
|
||||
|
||||
恰如“木桶理论”,庙算还可以消灭己方短板,寻找或者调动对方出现短板。当然,《孙子兵》并不是每战必胜,也不是以弱胜强,化不可能为可能的魔法书。不宜“知其不可而为之”。一系列庙算都在规则下,寻找到可能性,获得相对优势来战胜对手。如集中优势兵力,实现局部优势。“以迂为直,以患为利。”利用天时地形和行军,训练扎营做饭都有明确的目标。速战还是持久战,都是依据各种条件资源推演分析的结果。“故知战之地,知战之日,则可千里而会战。”唯有算胜,方可一战。
|
||||
|
||||
庙算为术,战术不能超脱于战略。“将在外君命有所不受”只是指根据实际情况可以灵活战术,正如当年毛主席要求林彪东北战役要服从大局,大战略不容颠覆。不能因一人之利,一地之利,一时之利而改变,只能随整个系统变化而调整。
|
||||
|
||||
感知
|
||||
|
||||
“不可胜在己”,立于不败之地,自己是关键,其他都是辅因;“可胜在敌”,看敌人的软肋和什么时候失误。“内省。知彼,但先要知己。”曾国藩作战,也首先讲明己第一,稳扎稳打立于不败之地,克敌才能实现。胜利也并非都是好事,很多时候当不是自己太强,而是对手太弱或其他因素,造就的胜利只会麻痹自己和埋下根本性失败的伏笔。
|
||||
|
||||
“知己知彼”就是自我和对手的感知。“天时地利人和”是对环境的感知。战争错综复杂风云变化,很多时候考较的都是随机应变的感知力和应对能力。“用兵者,合于利而动,不合于利而止。”对是否进行战争的条件的感知。还有对于细节的感知,“汲而先饮者,渴也。”需要通过经验学习感知获取信息,将规则载入情景化和有效利用。感知,可用于内部观察和防患未然,也可以用于判断对手并拟定对策。
|
||||
|
||||
“攻其无备,出其不意。”需要重视情报和传播。“兵者,诡道也。”情报和传播,等于数据的输入和输出,很多时候决定战争或组织的成败。依据情报奇兵制胜,如邓艾灭蜀;与对手虚假的情报,像蒋干盗书。更典型的是反间计。李牧、范曾罢黜,赵国、项羽败亡。
|
||||
|
||||
法度
|
||||
|
||||
“五事七计”都提到法令和奖惩。楚汉之争刘邦胜,“约法三章”以及陈平的有效奖惩体系都为功不小,胜在乎法令执行和奖惩公平。另一方面,用兵能“破釜沉舟”的项羽,然“印刓敝,忍不能予”是其失败的重要原因。
|
||||
|
||||
“五德,智信仁勇乎。”五德相须,缺一不可。但强调“智”,将人才放第一。而法度是规则,人才必须在法度内。
|
||||
|
||||
“主孰有道,将孰有能。”
|
||||
|
||||
“将之至任,不可不察也。”
|
||||
|
||||
“将听吾计,用之必胜,留之。将不听吾计,用之必败,去之。”
|
||||
|
||||
“令行素。”平时一样重视,战时才能发挥作用。
|
||||
|
||||
对于复杂系统,内部崩溃往往是其失败的主要原因。当然如前所述,这种内因也可由对手或外因推动演化。对于企业来说,领导者素质和人才队伍,中坚力量的支撑很重要,而成为中坚的标准就是一致的价值观和遵循规范。另外,还要建立适应自身、对手和战争形态的管理架构和组织体系,以及有效的规章和奖惩体系。
|
||||
|
||||
辨证
|
||||
|
||||
“以正合,以奇胜。”《孙子兵法》可能是辨证思维最早期的著作和运用。
|
||||
|
||||
《孙子兵法》里,强弱虚实迂直恩仇甚至胜败都是辩证的,是可以对立转化的。
|
||||
|
||||
事实上,无论战争或是组织发展,都不是直线的,而是曲线的,或者周期的,充满了变化。胜负也是辩证和变化的,便是拥有《孙子兵法》,吴国后期数战数胜,开始骄傲自负,百姓疲敝,胜负环境逆转,终致败亡。“水因地而制流,兵因敌而制胜”。所有策略必须依据时机、环境、对手、人心变化而变化。
|
||||
|
||||
兵法不可能写尽所有情况,任何行为和结果也不是简单对应,而是因果交错。战争和系统竞争要处理的是复杂数据。《孙子兵法》注定“学者生,像者死”,必须根据特定时空、自己和对手的具体情况灵活化用。
|
||||
|
||||
《孙子兵法》重规则轻案例,舍事而言理,因而得以破越时空和领域,成为竞争、管理和生存的基础规则。
|
||||
|
||||
《大败局》一书中,吴晓波将中国大多数企业失败归结为缺乏道德感和人文关怀,缺乏对规则和秩序的尊重以及系统的职业精神的缺乏,这些因果在《孙子兵法》中都能够找得到对应;相对的,所有基业长青的公司,又都具备一系列适应变化,可以持续指导成长的一般性规则,这些规则在《孙子兵法》中也都能够找到对应。
|
||||
|
||||
作为一种超乎教材而更类似于宗教的力量,《孙子兵法》的价值在于其提供了实用性同时引导了对规则的思考。跨域2500年时空岁月,它的思想精髓仍如星辰闪耀。而且随着数据、时代演进、新元素的输入还会不断推动其演进和发展,即便再过100年,1000年,只要人类、系统和竞争存在,它就永不会过时。
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user