人才的能力象限

在 AI 时代的浪潮下,我们不应把人当做实现任务的工具。在上一期实训的结营分享中,我用一张图来讲述人才的能力象限。一个人到底强不强,最根本的因素是心性,然后再外延出很多能力——工程能力、设计能力、商业能力。对这部分感兴趣的伙伴,可以阅读之前的文章《许式伟从「人的能力象限」谈技术人「职业发展」》


工程能力,就是如何把事情做成的学问。具备工程能力的人,懂得如何把大事变小,如何拆解复杂问题、控制好节奏来逼近目标、最后成功实现。举个典型例子,马斯克要上火星,这个是非常天马行空的巨大工程,但它可以被不断拆解,最终转化为普通工程师手中的每一个小任务。虽然大家总会习惯性地认为工程就是写代码,但工程与写代码之间并不是画等号的关系。

设计能力,就是做决策的能力。如何设计产品、如何设计架构、如何设计组织……有太多的东西需要设计。比如产品设计,初级产品经理喜欢做加法,通过叠加功能让研发赶工,整个团队忙得团团转却没有收益。但顶级产品经理做的是减法,要减到刚好精准命中用户心智,这是非常难的。再比如架构设计,初级架构师也喜欢做加法,喜欢垒代码。但顶级架构师做的是乘法
怎么做架构,这也是今天想重点分享交流的。

什么是架构设计?

有一个很有意思的现象——其实这个世界上没有几本好的架构书。关于架构这个词,大家都好像知道,但又好像无法精准定义,这说明还有很多模糊的、没有形成共识的地方。

架构可以被很朴素地理解,从手法上来说架构就是模块拆解、是连接和组合的问题。如何理解呢?程序、动态库、包、模块、函数等等都可以统称为实体,而「连接和组合」则是要探讨实体的边界问题,即一个实体要解决什么问题、不解决什么问题、和别的实体之间如何连接与组合。

架构非常依赖深度的经验,因此刚刚提到的架构应该要“做乘法”并不容易理解。在三期实训中,每一批同学都更习惯性地做加法——列好需求(而非真正分析清楚)之后,将页面1交给A,页面2交给B,这是一种“懒惰”且错误的分工方式。

我们应该用做业务的角度来看待模块拆解,即,每个模块都是独立的业务,它有独立的更大的需求边界,独立来看也很有前景。如果从商业的角度来讲,就是它有足够大的价值——价值=需求量*单价(假设先不考虑盈利,假设利润为0,那么单价就是把这个模块做出来的代价)。

从 Go+ 的实现看架构拆解

我们以 Go+ 的拆解作为实例,看看在这个独立业务中,架构是如何体现的。


首先,我们从资产的角度来看,可作为另外一个工序输入的东西就叫做资产。作为每一个模块的输入/输出,资产是耦合度最高、可能被引用最广泛的东西。因此,在任何模块/边界的拆解中,资产都很重要。

虽然“资产”并不是一个软件工程书籍中经常提到、被广泛达成共识的词,但这里要强调,资产不稳定将意味着这个模块是没有价值的,甚至可以说,资产决定一切。比如,在办公软件的构建中,文档就是用户所能感知到的资产,文档必须是稳定的,否则整个办公软件都会大打折扣。在 Go+ 的实现过程中也有很多道资产的变化,源代码、AST(抽象语法树)、SSA(静态单赋值形式) 等都是资产。

然后,Go+ 从宏观上分为2层,一层是 Go+ 的编译器,一层是 Go 的编译器,也就是 LLGo。我们也重点看看具体怎么拆解?

第一层:关于 Go + 本身的拆解思路

第一道工序,是从 Go+ 源代码到 Go+ AST,其目前有三个组件:scanner(词法分析)、parser(语法分析)、ast(抽象语法树)。其实还有另一种切法,Go+ v1.3 中的tpl 也有通用的词法分析器、语法分析器,我们也能通过这个 tpl 来实现 Go+ AST。

AST作为资产是重要且不可被改写的,但我们可以通过换一个通用组件来实现词法分析、语法分析。基于这两种思考逻辑,产生了不同的边界,而有些边界是可改的,有些边界是不能改的。

第二道工序,是从 Go+ AST,到 Go AST,再到 Go源代码,最终到可执行程序。为什么要有三道步骤呢?这里涉及到架构方面的权衡。

这里看似步骤很多,但其实只有从 Go+ AST 到 Go AST 这道工序是我们自己做的。这里涉及到 Go+ 编译器和 gogen 两个模块。gogen 是一个独立于 Go+ 项目的资产,是一个通用的 Go AST 的生成器。gogen以及刚刚提到的 tpl,它们的特点就是可以脱离 Go+ 项目而存在,其本身是一个有独立价值的组件。不像 Go+ 的编译器,基本只服务于 Go+ 这个项目,不太能迁移到别的地方。如果使用 tpl 和 gogen 来构建,实际需要写的代码是很少的。

而且,这也符合我们追求更高的灵活性(而非更高性能)的诉求。Go 源代码是重要资产,Go 语言使用者将复用 Go 源代码,如果我们跳掉中间步骤,一定会获得更差收益——和 Go 世界的连通性变差、无法选择 Go 的编译器等,那么,整个 Go+ 的生态都会变得很难建立。但现在,我们有了自由选择编译器的权利,支持了 Go、LLGo、TinyGo 这三个 Go 的编译器,而且 Go+ 的模块也可以被 Go 所使用。

第二层:关于 LLGo 的拆解思路

如同 Go+ 站在 Go 的巨人肩膀上,LLGo 同时站在了 Go 和 LLVM 这两大巨人的肩膀上。

在第三道工序的举例中,从 Go 源代码到 Go AST,再到 Go SSA。这整个过程的代码几乎都是现成的,我们只写了有限的连接性的代码。其中,主要的组件有 Go 的 scanner、parser、ast以及 Go 的扩展包、Go 的ssa等等,这些都由 Go 的官方团队提供,虽然这些都不由我们自己来写,但这很重要,这关系到要下述第四道工序举例。

我们核心要做的工序是从 Go SSA 到 LLVM SSA,再到可执行程序。也可以说,LLGo 的编译器真正在做的工序其实只有一道,就是 Go SSA 到 LLVM SSA。

我们做了 llgo/ssa 和 LLGo编译器这两个重要的模块。虽然LLGo 编译器一定是和 LLGo这个项目深度绑定的,无法放到其他地方使用,但其实它的代码量非常少,因为在边界划分中它把大量的业务都交给了 llgo/ssa 来干。而 llgo/ssa 其实是实现了 go/ssa 语义到 llvm ssa 语义转换的一个工具包,也就是说,使用这个模块以后,任何人只要知道 go/ssa 的语义,就可以用 llgo/ssa 直接生成可执行程序,这意味着 llgo/ssa 是一个很高阶的模块,可以理解为一个比 LLVM 的 SSA 更高阶的虚拟机,更加“机器友好”,它能够让写编译器变得很简单。

综上,在提到的这么多模块中,Go+ 编译器、 LLGo 编译器的代码量相对都比较小,而抽象出来的公共的 gogen 和 llgo/ssa,它们的代码量会显著大于编译器的代码。这就是刚刚讲的实体的边界问题,体现了实体的连接和组合,其决策逻辑的背后意味着巨大的得失。实体的边界问题其实就是实体的价值问题,因此,我们应该把模块尽量的看成是独立业务。

在 Go+ 的实现过程中,我们广泛地使用现有组件去串联、大量复用既有生态里的东西,而非自己造轮子;我们足够收敛,在一个很大的问题里,拆解出了真正应该花大力气做的事;我们看重资产,将每一个模块都作为资产来看待,即使在事项足够收敛的情况下,我们还拆解出了足够泛化的、和 LLGo 项目无关的 llgo/ssa,从而实现更大业务的范畴。这些,都让我们做成 Go+ 这件事变得更靠谱、更有可能。

这里,我还要分享一个逆直觉的重要结论:模块切分,通常和需求并不形成对应关系。以 Go+ 编译器为例,其需求无非就是 Go+ 的源代码到可执行程序,但整个过程中的那么多道工序并不由需求产生。

做好模块切分非常难,首先一定做好需求分析。很多人会将需求分析混淆为罗列需求,但罗列需求只是需求分析的输入,绝对不等同于需求分析。需求分析需要做好两件事:

第一,对需求未来可能的发展方向进行预判,这是为了防止过度设计。目前,会做需求发展研判的架构师,绝对是凤毛麟角,甚至有人认为这不是一名架构师该做的事情。

第二,更重要的是,要洞察需求的内在逻辑关联。也就是说,当需求被罗列在那里,我们如何做到、怎样做代价最低呢?

所以需求分析是存在于需求列表之后、架构之前的,要透过现象看本质,去洞察、发现需求内在的逻辑关联,并且提炼出实体来描述需求、建立需求之间的关联,为模块切分提供基础。

但我们要知道,需求分析一定是在架构范畴内的,因为需求分析和架构之间的关联非常密切,耦合很强。