Code Design
默认写函数,类要自己挣来
函数只做「输入→输出」,类把数据和操作捆成一团带在身上。多数代码没有需要长期守护的状态,硬塞进类里只是多一层壳。所谓 taste 高,不是讨厌类,是默认伸手拿函数,只有状态真的攒够了才升级成类。
从「两个词是什么」走到「taste 指什么」
五站:先分清函数和类各拿着什么,再看「不写类」到底在骂什么,然后是函数为什么是更顺手的默认值、类什么时候才真该上,最后落到 taste 这个词指的是哪件事。第五站直接回答你的原问题。
两个词先说清:函数管行为,类管状态加行为
函数是一段映射:给它参数,它算个结果还给你,不在外面留痕迹。add(2, 3) 永远返回 5,调一万次都一样。这种「只看参数、只返回结果、不碰外部」的函数,叫纯函数。
类是一个模板,把一组数据(状态)和操作这组数据的函数(方法)捆在一起。用它造出来的东西叫对象,每个对象自己记着自己的状态。一个 购物车 对象记着「现在装了哪些商品」,你调 cart.add(item),它改的是自己肚子里那份清单。
差别一句话:函数是动词,用完即走;类是名词,造出来的对象会一直活着、一直记着东西。
同一件事——跟人打招呼——两种写法。
函数:def greet(name): return f"Hi {name}"。给名字,吐招呼,没了。
类:class Greeter: 里塞一个 __init__(self, name) 把名字记到 self.name,再加一个 greet(self) 读出来。用的时候得先 g = Greeter("Sam") 再 g.greet()。
两步换一步,多出来的那一步——造对象、记状态——这里没换来任何好处。打个招呼不需要记住对方是谁。
| 维度 | 函数 | 类 / 对象 |
|---|---|---|
| 持有什么 | 什么都不持有,算完就忘 | 持有状态,对象活着就一直记着 |
| 基本单位 | 一次调用 | 一个对象(先实例化) |
| 怎么用 | 直接调 f(x) | 先 o = C() 再 o.m() |
| 读代码时 | 只看参数就懂这次干了啥 | 得先知道对象现在是什么状态 |
函数的心智负担只在那一次调用里;类的心智负担拖着整个对象的生命周期。
「写函数不写类」骂的是把简单事包装复杂
这句话不是反对类本身,是反对一类很常见的坏味道。2012 年 Jack Diederich 在 PyCon 做过一个出名的演讲,标题就叫《Stop Writing Classes》,专挑真实代码里多余的类来拆1 🟢 high。他给了一条很好记的判据:如果一个类只有两个方法、其中一个还是 __init__,那你其实想写的是一个函数。
还有一种:这个类一辈子只 new 一次,造出来用完就扔。那它跟一个函数没区别,状态根本没必要长期存在。Diederich 举的极端例子是一个叫 MuffinMail 的项目,API 从散在 22 个模块里的 20 个类,先压成 1 个 15 行的类,最后压成一个 3 行的函数1。功能没少,壳全没了。
把这种风气推到荒诞的,是社区里的玩笑项目「FizzBuzz 企业版」:判断一个数能不能被 3 和 5 整除——本来三五行——被用工厂、策略、代理、依赖注入包成几十个类几十个文件5。它在嘲讽的,正是把「名词」无脑做成类的习惯。Diederich 的原话很克制:类是名词没错,但不是每个名词都得是一个类。
「面向对象嘛,万物皆对象,看到一个概念就开个类。」
错在哪:类的成本是状态。开一个不持有真实状态、或者状态只在一次操作里活着的类,你付了「对象生命周期」这份心智税,却没买到任何东西。这种类常见的化身是「只有静态方法的工具类」——那其实是个伪装成类的命名空间,直接写成一组函数就好。
「不写类」不是教条,是一个默认值:能用函数表达清楚的,别先包成类。
为什么函数是更顺手的默认值
纯函数有三个实打实的好处。好测:同样输入永远同样输出,写测试就是「喂参数、对结果」,不用先把对象摆成某个状态。好懂:读一个纯函数,眼睛扫一遍参数和返回就够了,不用去翻这个对象之前经历了什么。好组合:纯函数之间拼起来不会互相打架,因为谁都不偷偷改外面的东西。
几个写代码的人反复说的是同一件事。John Carmack 2007 年那封讲「内联代码」的邮件里点破:内联真正要消灭的敌人,是意料之外的依赖和状态突变,而函数式更直接、更彻底地解决了这个问题3 🟢 high。Rich Hickey 在《Simple Made Easy》里把话说得更狠:状态把「值」和「时间」缠在了一起,所以对象、方法天生是「复杂」的构件,而值、函数、数据是「简单」的4。Erlang 之父 Joe Armstrong 给了那个最出名的比喻:你想要一根香蕉,面向对象却给你一只举着香蕉的大猩猩,外加它身后整片丛林——对象拖着一身隐式环境,想单独复用那根香蕉都难2。
self——那条看不见的线就是「隐式环境」。下面这段不纯,因为它偷读了外面的全局 RATE:
RATE = 0.8 def to_usd(cny): return cny * RATE
同样的输入 to_usd(100),明天有人改了 RATE,结果就变了。要让它变纯,关键一步是:?
对答案
把 RATE 变成参数传进去:def to_usd(cny, rate): return cny * rate。
现在函数只看自己的参数,to_usd(100, 0.8) 永远是 80。所有依赖都摆在了签名上——这正是 Armstrong 说的「不拖丛林」、Carmack 说的「消灭意料之外的依赖」。
函数把依赖全摆在签名上,看得见、管得住;这就是它能当默认值的根本原因。
类什么时候才值得写
反过来说,类不是原罪。当你手上有真正需要长期守护的状态,外加一条必须时刻成立的规矩,类就开始赚钱了。这条规矩有个名字,叫不变式(invariant):一个逻辑上必须永远为真的状态约束6 🟢 high。
例子:一个银行账户,余额不能为负。你把余额设成私有,外面碰不到,只能走 deposit 和 withdraw 两个方法,而每个方法都保证操作完余额仍 ≥ 0。这样一来,「余额不为负」这条规矩就被关进了类里,任何外部代码都没法把它弄坏。这是裸函数加一个公开变量很难做到的——变量一旦公开,谁都能直接写成 -999。
还有两种类真正赚钱的场合:
- 身份(identity):这个东西跨时间是「同一个」。一个购物车、一条数据库连接、一局游戏的状态机——它有生命周期,会变,但始终是它。
- 多态(polymorphism):一组对象遵守同一个接口,运行时按需换实现。
Logger今天写文件、明天写网络,调用方一行不改。
函数派会说:以上这些用闭包、用「不可变数据 + 一组函数」也能实现,Clojure、Elixir 这些语言整套大型系统都不靠类。状态确实可以用别的方式管。
本讲稿仍倾向「有真不变式就上类」的原因是:在 Java、C#、Python、TypeScript 这些主流语言和团队里,类是表达「这组状态归我管、只许走这几个口」最直白、工具链支持最齐的方式。能不能用别的实现,和在你的语境里哪种最省沟通成本,是两个问题。
判据收成一句:有需要被守护的状态和不变式,就上类;没有,就别。
所以「taste 高」指的是默认值选对了
把前面串起来,「写函数不写类 taste 高」这句话的真正意思,不是「类是错的」,而是一个关于默认值的判断:默认伸手拿函数,只有当状态和不变式真的攒够了,才把它升级成类。
taste 高的人不会为想象中的扩展性提前糊一层抽象。他们让复杂度跟着真实需求长——需求还没出现,壳就不先搭。这其实是 YAGNI(你不会需要它)落到「类 vs 函数」上的一个具体版本:抽象是有成本的,没到非抽象不可的时候,先别抽。反过来,taste 低的典型样子,就是第二节那些——把每个名词都做成类、为一次性的逻辑造一个对象、拿类当工具函数的命名空间。
有人写了个 Config 类,里面只有一个 __init__ 把几个值存进 self,再没别的方法,全程也只 new 一次。按本文的判据,这个类该留还是该删?删了用什么替代?
试着先答
该删。它只有 __init__、没有守护任何不变式、只实例化一次——三条全中 Diederich 的「你其实想写数据」。替代品:一个普通的字典、或一个不可变的 namedtuple / dataclass(frozen=True)。它是一坨数据,不是一个有行为要管的对象,就别给它穿类的外套。
taste 高 = 默认函数、按需升级,让抽象等到它真被需要的那一刻才出现。
给你一句能直接用的规矩
函数是「输入→输出」的一次性运算,类是把状态和方法捆在一起、造出会长期记事的对象。「写函数不写类 taste 高」不是贬低类,是反对一种把简单逻辑包装成多余对象的习惯——只有 __init__ 的类、只 new 一次就扔的类、当命名空间用的工具类,全是这种坏味道。落到操作上:默认写函数;当你发现自己在反复传同一组数据、而且这组数据有一条必须守住的规矩时,再把它收成一个类。
什么时候这套不成立?三种情况照写类不犹豫:一是你在做有真实身份和不变式的东西(账户、连接池、状态机);二是框架逼你这么写(很多 GUI、ORM、Spring 这类容器以类为单位);三是性能极端敏感、需要复用对象实例避免反复分配。这些场合里类不是 taste 低,是正确工具。taste 的高低从来不在「用没用类」,而在「这一层壳是不是真的换来了东西」。
这些地方我说实话也没全把握
- 「taste 高」本身是行业 vibe,没有量化标准 🔴 low。本文是把这句口号翻译成一个能操作的默认值判据,不是说存在一个客观的 taste 度量。
- 默认值跟语言文化强相关。Java/C# 生态里类是地基,Python/JS/Go 里函数更自然,Clojure/Elixir 干脆几乎不用类。同一句「少写类」在不同生态里的力度差很多。
- 大型有状态系统(游戏引擎、操作系统、复杂 UI)里类仍是主力。本文的「默认函数」主要针对的是日常业务逻辑那一层,不是说所有领域都该去类化。
读完盖住,试着答这几题
用一句话说出函数和类最根本的区别。
试着先答
函数不持有状态、算完即走;类造出的对象持有状态、会长期记事。函数的心智负担只在一次调用,类的拖着整个对象生命周期。
Diederich 那条「该写函数而不是类」的判据是什么?
试着先答
一个类只有两个方法、其中一个还是
__init__,或者一辈子只实例化一次,那它就该是个函数。同事写了个
StringUtils类,里面全是静态方法、没有任何字段。该不该保留?试着先答
不该。它没状态、不实例化,本质是个伪装成类的命名空间。直接写成一组模块级函数即可——这正是「当命名空间用的工具类」这种坏味道。
什么时候该毫不犹豫地写类?举一个判据 + 一个例子。
试着先答
判据:有需要长期守护的状态,外加一条必须时刻成立的不变式。例子:银行账户,余额私有、不能为负,只能走
deposit/withdraw,每个方法都保证余额 ≥ 0。为什么纯函数比方法「好测、好懂、好组合」?一句话点出共同的根。
试着先答
因为纯函数把所有依赖都摆在签名上、不碰外部状态,所以同样输入永远同样输出。没有隐式环境,就没有 Armstrong 说的「大猩猩和丛林」。
把这几题截图,过两三天再凭记忆答一遍 —— 记得住才算真学会。
Sources
- Jack Diederich, “Stop Writing Classes” (PyCon US 2012) — https://pyvideo.org/pycon-us-2012/stop-writing-classes.html
- Joe Armstrong on OOP(香蕉/大猩猩/丛林,出自 Coders at Work),John D. Cook 转述 — https://www.johndcook.com/blog/2011/07/19/you-wanted-banana/
- John Carmack on Inlined Code(2007 邮件,谈状态突变与函数式)— http://number-none.com/blow/john_carmack_on_inlined_code.html
- Rich Hickey, “Simple Made Easy”(transcript:state complects value & time)— https://github.com/matthiasn/talk-transcripts/blob/master/Hickey_Rich/SimpleMadeEasy.md
- FizzBuzz Enterprise Edition(过度面向对象的讽刺项目)— https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition
- Class invariant — Wikipedia — https://en.wikipedia.org/wiki/Class_invariant