找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2275

积分

0

好友

304

主题
发表于 2 小时前 | 查看: 1| 回复: 0

“避免不必要的抽象”这句话,在 Go 圈子里几乎人人都听过。

当你试图引入 ORM、泛型 Map/Reduce、接口或者复杂的设计模式时,往往会收到类似的提醒。这句话本身并没有错,难点在于:到底什么是“不必要”的?

函数是抽象吗?汇编是抽象吗?如果不加定义地“避免抽象”,我们最终只能对着硅片大喊大叫。

在 GopherCon UK 2025 上,John Cinnamond 做了一场与众不同的演讲。他没有展示任何炫酷的并发模式,而是搬出了马丁·海德格尔(Martin Heidegger)和伊曼努尔·康德(Immanuel Kant),试图用哲学的视角,为我们解开关于 Go 抽象的困惑。

注:海德格尔与《存在与时间》
马丁·海德格尔(Martin Heidegger)是 20 世纪最重要的哲学家之一。他在 1927 年的巨著《存在与时间》(Being and Time) 中,深入探讨了人(此在)如何与世界互动。John Cinnamond 在演讲中引用的核心概念——“上手状态” (Ready-to-hand)“在手状态” (Present-at-hand),正是海德格尔用来描述我们与工具(如锤子)之间关系的术语。
这套理论很适合解释:为什么优秀的工具(或代码抽象)应该是“透明”的,而糟糕的工具会强行占据我们的注意力。

我们都在使用的“必要”抽象

首先承认一个事实:编程本身就建立在无数层抽象之上。

  • 泛型:这是对类型的抽象。虽然 Go 曾长期拒绝它,但在技术上它常常是必要的,否则重复代码会泛滥。
  • 接口:这是对行为的抽象。io.Reader 让我们不必关心数据来自文件还是网络。
  • 函数:这是对指令序列的抽象。没有它,我们只能写长长的 main
  • 汇编语言:这是对机器码的抽象。

所以,当我们说“避免不必要的抽象”时,更贴近事实的表达其实是——避免“不恰当” (Inappropriate) 的抽象

那问题来了:如何判断一个抽象是否“恰当”?

Go抽象设计指南:如何避免不恰当封装 - 图片 - 1

何为抽象?——一场有目的的“细节隐藏”

在讨论“正确”的抽象之前,先回到基本定义。John Cinnamond 在演讲中给出一个精炼的说法:

“抽象是一种表示 (Representation),但它是一种刻意移除被表示事物某些细节的表示。”

Go抽象设计指南:如何避免不恰当封装 - 图片 - 2

把这个定义拆开看,会更清楚:

  1. 抽象是一种“表示”,而非事物本身
    它不是代码的实体,而是代码的地图或模型。比如,一辆模型汽车是真实汽车的表示;而 Gopher 吉祥物是地鼠的抽象——它刻意省略了真实地鼠的所有细节,只保留了核心特征。

    Go抽象设计指南:如何避免不恰当封装 - 图片 - 3

  2. 抽象是“有目的的”细节移除
    它不是“不精确”或“粗糙”的同义词。抽象是有意为之的:它不试图覆盖所有方面,而是只关注某个特定维度

    Go抽象设计指南:如何避免不恰当封装 - 图片 - 4

  3. 抽象在编程中具有动态性

    • 不确定引用 (Indefinite Reference):一个抽象(如 io.Reader)通常可以指代许多不同的具体实现。
    • 开放引用 (Open Reference):抽象的内容或它所指代的事物可以随着时间而改变。

为什么要刻意移除细节?John 总结了几个常见动机:

  • 避免重复代码:把重复逻辑提取到抽象中。
  • 统一不同的实现:允许以统一方式处理本质上不同的数据结构(例如所有实现了 Read 方法的类型)。
  • 推迟细节:隐藏那些当下不重要、或开发者不关心的细节(例如你坐火车参会,不需要知道每节车厢的编号)。
  • 揭示领域概念:用抽象表达业务领域中的核心概念。
  • 驾驭复杂性:最根本的理由——没有抽象,我们的大脑无法一次性处理所有细节,也就很难解决复杂问题。

但请注意:抽象并不等价于“越多越好”。 John 把抽象分为三类(这对你判断抽象质量非常关键):

  1. 基于“它是如何工作的” (How it works)
    为了复用实现而提取的抽象。比如你发现两处代码都在做“检查用户是否是管理员”,就把它提取成函数。
    这类抽象通常更脆弱:实现细节一变,抽象可能就失效。

  2. 基于“它做了什么” (What it does)
    Go 里接口(Interface)最典型的用法。比如 io.Reader:不关心它是文件还是网络,只关心它能“读取字节”。这是一种行为抽象。

  3. 基于“它是什么” (What it is)
    基于领域模型的抽象。比如一个 User 结构体代表系统中的实体,强调本质属性。

现实中,好的抽象往往是这三者的混合体。但在设计阶段,你至少得先想明白:你抽象的是“行为”,还是“实现”?搞反了,后续维护往往会很痛。

Go抽象设计指南:如何避免不恰当封装 - 图片 - 5

理解抽象的本质后,你可能会想:既然抽象能驾驭复杂性,那是不是抽象越多越好?

先别急着下结论。评判抽象是否“恰当”,不能只盯着代码本身,还有一个经常被忽略的现实:抽象不仅存在于代码里,也存在于人与人的协作里。

抽象的代价——代码是写给人看的

John 提醒我们:软件开发本质上是一项社会活动 (Social Activity)。

“除非你是为了自己写着玩,否则你的代码总是写给别人看的。团队是一个微型社会,它有自己的习俗、信仰和‘传说’(Lore)。”

引入一个新的抽象,等价于给这个“微型社会”引入一种新规则。它带来的代价至少包括:

  1. 社会成本(Social Cost)
    如果抽象与团队现有习惯相悖——比如在一个从未用过函数式编程的 Go 团队里强推 Monad——阻力往往会非常大。

  2. 团队的保守性
    成熟团队往往趋于保守,改变既定习惯需要能量。你不能只因为某个抽象在理论上很美就引入它,你得证明:收益足以覆盖它带来的社会摩擦成本

  3. 认知负担是共享的
    抽象对你来说可能很清晰,但如果它让队友困惑,那就是在消耗整个团队的智力预算。

Go抽象设计指南:如何避免不恰当封装 - 图片 - 6

因此,判断抽象是否“恰当”,不能只问“我能不能写出来”,还得问:它是否合群? 这也是把海德格尔哲学引入工程讨论的现实基础。

锤子哲学——“上手状态” vs “在手状态”

John 引用了海德格尔在《存在与时间》中的概念:Ready-to-hand(上手状态)Present-at-hand(在手状态)

  • 上手状态 (Ready-to-hand):当你熟练使用锤子钉钉子时,注意力在“钉钉子”这件事上。锤子本身在意识中是透明的,它像身体的延伸。
  • 在手状态 (Present-at-hand):当锤子坏了(锤头掉了),或你拿到一把陌生、设计古怪的工具时,注意力被迫从“钉钉子”转移到“锤子本身”。你开始研究它的结构、重量和用法。

这对代码意味着什么?

  • 好的抽象是“上手状态”的
    比如 for 循环。经验丰富的开发者使用它时思考的是“我要遍历数据”,而不是“循环语法怎么编译”。它顺手、透明,让你专注于问题本身。

    Go抽象设计指南:如何避免不恰当封装 - 图片 - 7

  • 坏的抽象是“在手状态”的
    比如复杂、过度设计的 ORM,或晦涩的 Monad 库。使用它时,你的思维被迫中断,你不得不停下来问:“这个函数到底在干什么?这个参数是什么意思?为什么要这么绕?”

如果一个抽象让你频繁从“解决业务问题”切换到“研究工具本身”,它很可能就是一个坏抽象。

Go抽象设计指南:如何避免不恰当封装 - 图片 - 8

注:通过学习和实践,在手状态 (Present-at-hand) 的抽象可以转变为上手状态 (Ready-to-hand) 的抽象。

真理的检验——“本质真理” vs “巧合真理”

接着,John 又借用了康德关于真理的分类,引导我们思考抽象的持久性

  • 分析真理 (Analytic Truth):由定义决定的真理。比如“所有单身汉都没结婚”。在代码里类似 unnecessary abstractions are unnecessary,正确但没太大信息量。
  • 综合真理 (Synthetic Truth):由外部事实决定的真理。比如“外面在下雨”,真假取决于环境。
  • 本质真理 (Essential Truth):不由定义决定,但反映世界更稳定的结构规律。比如“物质由原子构成”。

这对抽象意味着什么?

当你提取一个抽象时,问问自己:它代表的是代码的“本质真理”,还是仅仅是一个“巧合”?

举个例子:你有一段过滤商品的代码,既要按“价格”过滤,也要按“库存”过滤。你提取了一个抽象:Filter(Product) bool

  • 如果未来过滤需求(颜色、大小等)都能用这个签名解决,你就捕捉到了一个本质真理,这个抽象会更稳固。
  • 但如果新需求是“过滤掉重复商品”,这需要知道所有商品的状态,而不仅仅是单个商品。原本的 Filter(Product) bool 签名就会瞬间失效。

如果你提取抽象只是因为几段代码“长得像”(巧合),而不是因为它们“本质上是一回事”,那么需求一变,这个抽象就会崩塌,反而变成负担。

从这个角度看,好的抽象不是被“创造”出来的,而是被“发现”(Recognized)出来的——它是对问题结构的识别与捕捉。

实战指南——如何引入抽象?

最后,John 给出了一个评估抽象是否“恰当”的五步清单(也很适合当作团队 code review 的共同语言):

  1. 明确收益 (Benefit):你到底是为了解决重复、隐藏细节,还是仅仅因为觉得它“很酷”?
  2. 考虑社会成本 (Social Cost):编程是社会活动。这个抽象符合团队的习惯吗?引入它会不会消耗大量团队认知成本?(例如在 Go 里强推 Monad 等函数式范式)
  3. 是否处于“上手状态” (Ready-to-hand):它能融入开发者直觉吗?还是会成为注意力的绊脚石?
  4. 是否本质 (Essential):它是否捕捉到了问题的核心结构,能经得起未来变化?
  5. 是否涌现 (Emergent):它是从现有代码中“识别”出来的模式,还是你强加给代码的枷锁?

Go抽象设计指南:如何避免不恰当封装 - 图片 - 9

小结:保持怀疑,但别放弃好奇

Go 社区“避免不必要的抽象”的文化,本质上是在防御认知负担。工程实践里,为了抽象而抽象的代码确实太常见了;而这些“看起来很优雅”的东西,往往会把团队拖进维护泥潭。

但也别走到另一个极端:把抽象当洪水猛兽。正确且必要的抽象依然是强大的武器,它让我们能够驾驭规模、复杂度与协作成本。

如果我们能像海德格尔审视锤子那样审视自己的代码:区分“上手”与“在手”,区分“本质”与“巧合”,就更有机会在 后端架构 的真实约束中,找到适合自己团队的“恰当抽象”。

Go抽象设计指南:如何避免不恰当封装 - 图片 - 10

资料链接: https://www.youtube.com/watch?v=oP_-eHZSaqc

延伸阅读与讨论:你也可以到 云栈社区 继续交流,看看大家在项目里是如何权衡抽象与复杂度的。




上一篇:Git多人协作进阶:commit本质、merge方向与stash实战
下一篇:Port-Killer跨平台端口占用一键终止工具
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-18 21:32 , Processed in 0.415999 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表