You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

183 lines
9.7 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 59 | 少谈点框架,多谈点业务
你好,我是七牛云许式伟。
## 架构是共识确认的过程
对于架构这件事情,有不少让人误解的地方。前面在 “[57 | 心性:架构师的修炼之道](https://time.geekbang.org/column/article/166014)” 一讲中,我们提到过架构师需要掌握的三大技能:
* 理需求的能力;
* 读代码的能力;
* 抽象系统的能力。
这里面除了 “读代码” 这件事可能允许没有什么显性的产出外(其实也应该有,去读代码通常意味着缺架构设计文档,所以按理应该去补文档),其他两类事情要做好都不容易。
就理需求的能力而言,很多架构师一不知道要做需求分析,二不知道需求分析的产出到底应该是什么样的。需求分析可以说是架构师最没有概念的一个环节,尽管它至关重要。这一块领域特征比较明显,课堂上讲师授课的方式,很难有好的成效,更适合以实训的方式来强化。
就抽象系统的能力而言,很多架构师爱画架构图。画完了架构图,他就认为架构做完了,下一步该去编码。
这有什么问题?
首先,架构过程是团队共识形成与确认的过程。共识是需要精确的、无歧义的。而架构图显然并不精确。
团队没有精确的共识很可怕,它可能导致不同模块的工作牛头不对马嘴,完全无法连接起来,但是这个风险没有被暴露,直到最后一刻里程碑时间要到了,要出版本了,大家才匆匆忙忙联调,临时解决因为架构不到位产生的“锅”。
这时候人们的动作通常会走形。追求的不再是架构设计的好坏,而是打补丁,怎么把里程碑的目标实现了,别影响了团队绩效。
我们作个工程师的类比,这种不精确的架构,就好比建筑工程中,设计师画了一个效果图,没有任何尺寸和关键细节的确认,然后大家就分头开工了。最后放在一起拼接(联调),发现彼此完全没法对上,只能临时修修改改,拼接得上就谢天谢地了。是不是能够和当初效果图匹配?让老天爷决定吧。
更精确描述架构的方法是定义每个模块的接口。接口可以用代码表达,这种表达是精确的、无歧义的。架构图则是辅助模块接口,用于说明模块接口之间的关联。
为了证明接口的有效性,架构师还应该过一遍所有的用户故事,以伪代码或流程图的方式,把所有用户故事过一遍,确认模块之间的接口串起来是可以正常工作的。
实际上更有效的方法是在概要设计(也叫系统设计)阶段就把框架代码写出来,真真正正用代码,而不是伪代码,把用户故事串一遍。
代码即文档。代码是理解一致性更强的文档。
这样做的好处是,我们把联调工作做到了前头,工程的最大风险就得到了管理。剩下来的就是每个模块自身的好坏,这就和组织能力无关,只取决于我们招聘的工程师个体素质了。
所以模块的接口,是架构设计的核心。
## 别让框架绑架业务
接口代表什么?接口代表业务。架构图代表什么?架构图代表框架。
不要让框架绑架业务。
在架构的两侧,一边是用户需求,一边是技术。接口代表用户需求,代表业务。框架代表技术,是我们满足需求的方法。
框架它是重要的。但是不要让框架反客为主,溢出模块边界。在系统迭代的过程中,框架会经受变化,以适应需求的演进过程。
抓住稳定的东西,比追逐变化更重要。
框架,体现的是需求泛化的能力。从架构思维角度上来说,它是通过抽象出需求模板,把多个需求场景中变化的部分抽离出来,形成相对稳定的泛化需求。
框架的抽象能力不是一蹴而就的,它既依赖我们抽象系统的能力,也依赖我们对领域需求的理解程度。所以框架会随着时间而迭代,逐步向最理想的状态逼近。
如果框架不能满足需求,但我们不迭代框架,而是硬生生去添加这样的功能需求,会发生什么?
结果是,代码逃逸出框架,把系统搅得支离破碎。这时候你可能能够嗅到一丝危险的气息。但是你可能说没办法,里程碑的截止时间就在那里,没办法。
这实际上是大框架面临的最大挑战。它最好能够提前预测所有可能的需求,以此抑制潜在代码逃逸的风险。
但这很难。
所以我们应该换一个角度看这个问题。在如何持续保证系统洁净的这件事情上,我个人给的建议是:
> 连接性的代码越少越好。
什么是连接性的代码?就是把两个子的业务系统连接,构成一个大业务场景的代码。如果有大业务场景,应该抽象出新的更大范畴的业务系统。
这样我们的焦点就始终在业务上。
**每个模块都是一个业务。**这里我们说的模块是一种泛指,它包括:函数、类、接口、包、子系统、网络服务程序、桌面程序等等。
抽象出符合业务自然语义的接口,远比抽象出泛需求的框架要容易得多。因为,业务语义是稳定的。
关注业务接口的定义,我们自然就把焦点转向关注业务如何由相互正交的子业务组合而来。
我们举例来说明关注业务与关注框架这两种思维方式的差异性。
我们知道,在 IO 系统中读取磁盘文件中的数据有两种常见的模型SAX 和 DOM 模型。
SAX 模型是一种基于事件的读盘机制。在读完一个完整的数据单元时,就发送一个读到某数据单元的事件。比如在 XML 中,它的事件接口看起来是这样的:
```
type ContentHandler interface {
StartDocument()
StartElement(element string, attrs Attributes)
Characters(chars []byte)
EndElement(element string)
EndDocument()
}
```
DOM 模型则基于对象的组织模型来提供数据读取的能力。细分来说,它又有两种不同的选择。一种是基于抽象的 DOM 树,它看起来是这样的:
```
type Nodes interface {
Len() int
Elem(i int) Node
}
type Node interface {
Childs() Nodes
Name() string
Type() NodeType
Text() []byte
Attributes() Attributes
}
```
另一种是基于更具体的业务逻辑,与具体的领域相关,比如对于 Word/WPS 这样的字处理软件看起来是这样的:
```
type Span interface {
Text() []byte
Attributes() SpanAttrs
}
type Spans interface {
Len() int
Elem(i int) Span
}
type Paragraph interface {
Spans() Spans
Attributes() ParagraphAttrs
}
type Paragraphs interface {
Len() int
Elem(i int) Paragraph
}
type Document interface {
Paragraphs() Paragraphs
Attributes() DocumentAttrs
}
```
基于 SAX 模型是非常典型的框架思维。它的确足够的通用,但它有两个问题。
一方面,基于事件模型是一个非常简陋的编程框架,与大部分 IO 子系统的需求方的诉求并不那么匹配。关于这一点我们在下一讲 “[60 | 架构分解:边界,不断重新审视边界](https://time.geekbang.org/column/article/170912?utm_term=zeusN8V46&utm_source=pcchaping)” 还会详细展开。
另一方面,它不体现业务,使用方不能在缺乏文档配合的情况下正确地使用这个接口。本来代码应该是精确的,但是这样的接口把精确性这个最佳的优点给放弃了。
基于 DOM 模型的两种模型中,看起来前者的接口很简洁,但实际上它和上面的 SAX 模型有类似的问题:不体现业务。而后者虽然看起来非常冗长,但是它可以脱离额外的接口说明文档而直接毫无心智负担地使用。毫无疑问,这才是我们该追寻的接口描述方式。
## 别用实现替代业务
在接口设计中,我们还看到另一种倾向,可以认为是用框架来替代业务的特例:用实现机制替代业务。
我个人经常给架构师们说的一句话是:
> 比框架(架构图)更重要的是数据结构,比数据结构更重要的是接口。
为什么数据结构比框架(架构图)更重要?业务数据结构是架构实现机制的灵魂。从共识确认的角度,数据结构相比框架而言,是更重要的共识。
一些架构师能够想清楚实现,但是想不清楚业务。他们用实现替代对业务系统的抽象。
用实现机制替代业务的典型案例是定义了数据结构,但是不抽象数据的业务逻辑,直接让使用方操作成员变量,或者定义一堆成员变量的 get/set 接口。
另一个例子是当我们用 ORM 框架操作数据库时,工程师非常容易犯的错误是,直接操作数据结构,而忽略定义业务接口的重要性。
## 结语
今天谈的内容,核心指向一点:
> 架构就是业务的正交分解。每个模块都有它自己的业务。
> 这里我们说的模块是一种泛指,它包括:函数、类、接口、包、子系统、网络服务程序、桌面程序等等。
它看似简单,但是它太重要了,重要到需要单独一讲来把它谈清楚。它是一切架构动作的基础。
架构行为的三步曲:“需求分析”、“概要设计”、模块的 “详细设计”,背后都直指业务的正交分解,只是逐步递进,一步步从模糊到越来越强的确定性,直至最终形成业务设计的完整的、精确无歧义的解决方案。
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “架构分解:边界,不断重新审视边界”。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。