HackerTube English?
软件边界与变化 →

一个模块究竟答应了调用者什么?

想换掉一个模块,却发现别处依赖它碰巧返回的顺序。从「接口写了什么」往前走一步:哪些决定本该藏在里面,又有哪些行为已经被人当成承诺?

本文的问题 · 13
  1. 少即是少处理例外与纠缠
  2. 用小接口和深实现减少使用者负担
  3. 小对象与小接口,不是同一个尺寸
  4. 模块边界既保护演进,也冻结选择
  5. 可观察的实现细节会变成兼容负担
  6. 使用者依赖了可观察行为之后,实现细节就可能变成兼容性承诺
  7. 模块名称限制多版本,内容哈希允许共存
  8. 替换实现之前,先写出调用者买到的操作
  9. 签名验过了,账本就只有一个版本吗?
  10. 接口藏起的成本去了哪里?
  11. 老程序还能启动,算兼容了吗?
  12. 一次必填字段变更,谁来搬家?
  13. 「能加热」要持续多久才算交付?

少即是少处理例外与纠缠

API 设计演讲建议不确定时删去不必要内容;Simple Made Easy 将简单解释为关注点不纠缠;两者共同强调减少使用者必须理解的额外选择与依赖。

对照原文与时间戳 · 2
When it doubt, leave it out.
How To Design A Good API and Why it Matters · ▶ 24:42–24:45
start seeing interconnections between things that could be independent
"Simple Made Easy" - Rich Hickey (2011) · ▶ 59:28–59:32

删减 API 选项只是一个设计手段;它不自动保证底层关注点已解耦。

用小接口和深实现减少使用者负担

Bloch 建议 API 保持小而清晰,拿不准的功能先不加入;Ousterhout 的深模块则以简单接口承载大量底层功能。两者共同指向一种具体设计收益:把复杂实现留在边界之后,让调用者只需理解少量稳定概念。

对照原文与时间戳 · 2
When it doubt, leave it out.
How To Design A Good API and Why it Matters · ▶ 24:42–24:45
very simple interface with a very large amount of functionality underneath it
A Philosophy of Software Design | John Ousterhout | Talks at Google · ▶ 14:24–14:29

公共 API 的功能取舍与内部模块深度是不同层级的设计问题;小接口本身不保证内部模块设计良好。

小对象与小接口,不是同一个尺寸

Metz 谈类和方法的粒度,以及彼此知道得更少;Bloch 谈 API 暴露的功能范围,并建议拿不准时不加入。可以比较内部拆分与外部接口收缩这两个不同设计维度。

对照原文与时间戳 · 2
make smaller things
RailsConf 2014 - All the Little Things by Sandi Metz · ▶ 0:54–0:58
When it doubt, leave it out.
How To Design A Good API and Why it Matters · ▶ 24:42–24:45

更多小类不必意味着更好的公共 API;不能把两段合并成对任意粒度的越小越好。

模块边界既保护演进,也冻结选择

Conway 演讲指出沟通成本越高,跨边界迭代越少;Bloch 指出 API 一旦有人使用就难以删除。合读可以把变更成本拆成跨团队协调和对既有调用方的兼容承诺,分别检查设计边界的代价。

对照原文与时间戳 · 2
the higher the cost of communication the less iteration will happen there
The Only Unbreakable Law · ▶ 27:06–27:16
not be able to take it out once you put it into an API.
How To Design A Good API and Why it Matters · ▶ 24:57–25:03

组织沟通成本与公共 API 兼容性是不同约束,不能把所有接口稳定性问题归因于组织图。

可观察的实现细节会变成兼容负担

Python的slots、类型与模块缓存被生态依赖,阻碍实现调整;Swiss Table演讲将同一现象明确表述为用户会依赖所有可观察行为,并用随机化抑制偶然迭代顺序成为契约。

对照原文与时间戳 · 2
people actually depend in very bizarre ways on some of the internals
How Python was Shaped by leaky Internals, Armin Ronacher, Flask framework · ▶ 4:31–4:38
anything a user can observe about the implementation of your API
CppCon 2017: Matt Kulukundis “Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step” · ▶ 5:19–5:27

不是所有内部行为都必须隐藏;Python演讲同时肯定栈状态暴露带来的调试收益。

使用者依赖了可观察行为之后,实现细节就可能变成兼容性承诺

解释器、哈希表和公共 API 是不同场景;能观察到某种行为,不等于它已被正式保证。

对照原文与时间戳 · 3
people actually depend in very bizarre ways on some of the internals
How Python was Shaped by leaky Internals, Armin Ronacher, Flask framework · ▶ 4:31–4:38
they will rely upon it, given enough users.
CppCon 2017: Matt Kulukundis “Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step” · ▶ 5:27–5:32
not be able to take it out once you put it into an API.
How To Design A Good API and Why it Matters · ▶ 24:57–25:03

模块名称限制多版本,内容哈希允许共存

Python演讲指出sys.modules以模块名定位对象,现有代码与pickle依赖这一语义,因此同库多版本难共存;Unison则把引用绑定到内容哈希,区分同名但不同定义,使多版本依赖无需争夺全局名字。

对照原文与时间戳 · 2
we would have to make S modules also have a version identifier
How Python was Shaped by leaky Internals, Armin Ronacher, Flask framework · ▶ 39:11–39:16
then it would be perfectly fine to allow as many different versions of the same library in our code base
"Unison: a new distributed programming language" by Paul Chiusano · ▶ 26:04–26:16

Unison不是对Python问题的补丁;类型在边界相遇时仍需要转换,运行时与生态不同。

替换实现之前,先写出调用者买到的操作

用平方根做外层计算的人,可以把「怎样改进猜测」封在过程里。若接口还承诺常数时间找父节点,隐藏实现就多了一项不能忽略的条件。紧凑数据结构的讲座把树压成位串,再用 rank 和 select 支撑导航。把这两件事放在一起看,文档要分开写清结果的含义、查询的代价,以及为达到这个代价需另存多少索引。换掉表示时,三项不一定同时保持。

原文与时间戳 · 3
using black-box abstraction to suppress detail.
Lecture 1A: Overview and Introduction to Lisp · ▶ 13:03–13:07
If I have rank and select in constant time, now I can do left child, right child, parent in constant time.
17. Succinct Structures I · ▶ 32:35–32:41
all I'm allowed for rank and select is little o of n space.
17. Succinct Structures I · ▶ 33:34–33:39

签名验过了,账本就只有一个版本吗?

把一笔付款送给两个节点,它们可以各自检查签名。若两条各自通过验证的历史互相冲突,签名并没有指定选哪条。Gensler 把下一层问题表述为「谁追加下一个块」,比特币讲解再引入累计工作量作为选择有效历史的依据。关于 256 位散列的估算只讨论撞中指定目标的通用尝试成本。它衡量的是一种计算任务,不能替我们验证账本规则、消息授权与历史选择这三层是否都正确。

原文与时间戳 · 3
how do you decide who appends that next block?
1. Introduction for 15.S12 Blockchain and Money, Fall 2018 · ▶ 18:33–18:39
the one with the most work put into it
But how does bitcoin actually work? · ▶ 19:10–19:22
on average, 2 to the 256 guesses
How secure is 256 bit security? · ▶ 0:33–0:37

接口藏起的成本去了哪里?

同样一次哈希查找,可以顺着密集的数据往后探测,也可以追着指针去很远的地址。Carruth 的数据结构演讲让你沿实际访问路径计成本。把同样的问题带到他的抽象开销演讲:即使一个调用点产生了漂亮指令,成本也可能留在编译时间,或出现在跨函数的调用约定上。对调用者说「快」以前,先说明承诺的是哪种负载、哪道边界,以及谁在付费。

原文与时间戳 · 2
maximize the cache locality of of handling a collision
CppCon 2014: Chandler Carruth "Efficiency with Algorithms, Performance with Data Structures" · ▶ 49:33–49:43
it just means the cost is paid somewhere else
CppCon 2019: Chandler Carruth “There Are No Zero-cost Abstractions” · ▶ 16:44–16:51

老程序还能启动,算兼容了吗?

把消息字段改名,服务自身的代码完全可以一起编译通过。Hickey 提醒你注意,越过进程边界的名字已经有别的读者。Goetz 则区分旧二进制能否链接、源代码能否重编,以及运行后行为是否一致。检查一次改动时可各保留一份旧制品、旧源码和旧消息,分别试;同一个「兼容」复选框装不下这三种实验。

原文与时间戳 · 2
as your names start escaping out of your processes, that they are global names
The Language of the System - Rich Hickey · ▶ 21:29–24:03 · 段落范围
existing class files will continue to link
Brian Goetz - Stewardship: the Sobering Parts · ▶ 20:53–20:59

一次必填字段变更,谁来搬家?

按照 Hickey 的提问方式,加必填字段是提高了调用者必须满足的门槛。Winters 的菱形依赖解释了为什么两支团队不能随时一起搬:它们可能经由不同中间库依赖同一组件。Wright 给出实际顺序,先在保留旧默认行为的条件下提供新接口,逐批迁移调用点,最后结束过渡。版本号可以指认两个版本,却不能替尚未迁完的那一支提供数据。

原文与时间戳 · 3
if you require more than somebody who's giving less now is not going to get what they want
Spec-ulation Keynote - Rich Hickey · ▶ 24:55–25:05
If everyone in your infrastructure doesn't upgrade at the same time, then we have version skew.
CppCon 2017: Titus Winters “C++ as a "Live at Head" Language” · ▶ 15:30–15:36
Usually you add the new thing without making it the default.
CppCon 2018:H. Wright “Large-Scale Changes at Google: Lessons Learned From 5 Yrs of Mass Migrations” · ▶ 13:15–13:19

「能加热」要持续多久才算交付?

Thwaites 拆开售价很低的烤面包机,数出数百个零件,最后自己制作的装置短暂发热,元件随即熔化。把这个实物当作接口来看,「接通后产生热」和「在使用时长内持续加热」会让同一件作品得到不同的验收结论。如果重要的是后一个约定,测试就要把持续时间和失效后的状态也写进去。

原文与时间戳 · 2
that, inside this object, which I'd bought for just 3.49 pounds, there were 400 different bits made out of a hundred-plus different materials.
Thomas Thwaites: How I built a toaster -- from scratch · ▶ 1:39–1:51
And for about five seconds, the toaster toasted, but then, unfortunately, the element kind of melted itself.
Thomas Thwaites: How I built a toaster -- from scratch · ▶ 10:31–10:40

读完再想一步

挑一个准备修改的接口:哪些行为有明确约定,哪些只是碰巧一直没变?