원클릭으로
oop-thinking
OOP 思考框架——用自然语言的名词/动词组织代码,与点语法无关。提炼自 Sandi Metz 的 POOD 与 99 Bottles of OOP
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
OOP 思考框架——用自然语言的名词/动词组织代码,与点语法无关。提炼自 Sandi Metz 的 POOD 与 99 Bottles of OOP
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
本项目 Scalable C 编码风格的 skill
Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication and to surface assumptions.
基于 Polya《How to Solve It》的通用解题方法论。遇到复杂问题时使用。
生成 Meta-lisp 代码时保证括号正确匹配的技术 + 验证脚本
SOC 직업 분류 기준
| name | oop-thinking |
| description | OOP 思考框架——用自然语言的名词/动词组织代码,与点语法无关。提炼自 Sandi Metz 的 POOD 与 99 Bottles of OOP |
本 skill 提炼自 Sandi Metz 的两本书:
核心定位:这不是写 OOP 代码的语法指南,而是写任何代码时都应该运用的思考方式。 无论你用的是 Lisp、TypeScript、Python、C 还是 Go,OOP 思考都能帮助你组织出可维护、可变化 的代码。
"The purpose of design is to allow you to do design later, and its primary goal is to reduce the cost of change." — POOD Ch.1
写代码时,始终面对两个问题:今天要实现的功能、未来会到来的变更。好的设计不是预 测未来需要什么,而是保留适应未来的选项。它不猜测,它给你留移动空间。
你写的每一行代码都是对未来的承诺。如果今天的代码让明天的变更变得困难,那今天 的代码就是坏的设计——不管它多"优雅"。
"The foundation of an object-oriented system is the message." — POOD Ch.2
对象是容器,消息才是设计的基本单位。思考代码时,先问:
好的设计让消息流动自然——调用方不需要知道接收方的实现细节,只需要知道它能 响应什么消息。
OOP 思考不依赖 object.method() 的点语法。它的本质是用自然语言中的名词和
动词来组织代码——"mechanic 准备 bike"这个关系,不同语法只是不同写法:
| 关系 | OO 语言 | Lisp / Scheme | C |
|---|---|---|---|
| mechanic 准备 bike | mechanic.prepare_bicycle(bike) | (mechanic-prepare-bicycle mechanic bike) | mechanic_prepare_bicycle(mechanic, bike) |
| trip 知道自己的 bikes | trip.bicycles() | (trip-bicycles trip) | trip_bicycles(trip) |
| wheel 计算直径 | wheel.diameter() | (wheel-diameter wheel) | wheel_diameter(wheel) |
三种语法形式不同,但表述的概念结构相同——"谁"对"什么东西"做"什么动作"。
判断标准:当一段代码可以用"主语-动词-宾语"大声朗读、非程序员也能大致听懂时, 它就是好的 OOP 设计。 如果你需要念完一整段操作步骤才能说清发生了什么, 说明职责划分有问题。
写出的代码应当是 TRUE 的(POOD Ch.2):
TRUE 的起点是单一职责:每个类、每个方法只做最小可行的有用的事。
写代码时,用以下原则自检(POOD Ch.1):
| 原则 | 含义 | 自检问题 |
|---|---|---|
| Single Responsibility | 每个类/模块只有一个变化理由 | 能用一句话描述这个模块的职责吗?句子里有"和"就说明做太多了 |
| Open/Closed | 对扩展开放,对修改关闭 | 加新功能需要改已有代码吗?需要改 = 不 closed |
| Liskov Substitution | 子类可以替换父类而不破坏程序 | 子类的行为会让调用方"惊喜"吗?惊喜 = 违约 |
| Interface Segregation | 接口应当小而专注 | 调用方被迫依赖它不需要的方法吗? |
| Dependency Inversion | 依赖抽象而非具体 | 高层模块依赖了低层模块的名字吗? |
"The failure here is not bad intention—it's insufficient patience." — 99 Bottles Ch.1
四种代码方案(99 Bottles Ch.1):
| 方案 | 特点 | 问题 |
|---|---|---|
| Incomprehensibly Concise | 极简、机智 | 不可读,变更成本爆炸 |
| Speculatively General | 预判未来的抽象 | 抽象大概率是错的;复杂度白付 |
| Concretely Abstract | 正确的直觉,过早抽象 | 方法名和抽象层级不匹配,改起来依然贵 |
| Shameless Green | 简单直白,不羞耻 | 看起来不酷,但变更容易、理解快 |
教训:先用最简单的方式把代码写对(Shameless Green),等变更到来时再抽象。 抽象应该从代码中"浮现"出来,而不是被塞进去。
关键判断:
"Complex behavior emerges from the repeated application of simple rules." — 99 Bottles Ch.3
当你需要从具体的代码中抽取出抽象时,不要靠"灵感"。用 Flocking Rules, 这是可通过反复执行而产出抽象机械式流程:
1. 选择最相似的东西
2. 找到它们之间最小的差异
3. 做出消除该差异的最简单修改:
a. 先让新代码能解析(parse)
b. 再让它解析且执行(parse + execute)
c. 再让它解析、执行且使用结果(parse + execute + use)
d. 删除不再使用的旧代码
每次只改一行。每次改完跑测试。测试失败就回退,换更小的改动。
当面对多个 switch case / if-else 分支 / 相似函数时:
这四个步骤把小步重构变成了流水线:每步只引入最小的不确定性,测试在每步都保持 绿色。
"Ask for 'what' instead of telling 'how'." — POOD Ch.4
坏消息: 发送方知道接收方的所有实现细节,接收方的任何变化都会破坏发送方。
# OO 语法
trip.clean_bicycle(bike)
trip.pump_tires(bike)
trip.check_brakes(bike)
# Lisp 语法
(trip-clean-bicycle trip bike)
(trip-pump-tires trip bike)
(trip-check-brakes trip bike)
好消息: 发送方只信任接收方知道怎么做。接收方的实现可以随意变化。
# OO 语法
mechanic.prepare_bicycle(bike)
# Lisp 语法
(mechanic-prepare-bicycle mechanic bike)
在好消息版本中,命名本身承载了"主语-动词-宾语"结构——mechanic + prepare_bicycle + bike。
代码就是可读的领域文档。在坏消息版本中,读起来像操作手册步骤列表——
发送方在指挥接收方"做这个、做那个",而不是委托一个任务。
判断标准: 看消息链——如果发送方需要序列化地告诉接收方每一步做什么,就说明 职责放错了地方。行为应该和数据放在一起。
一个对象知道的关于其他对象的所有事情,构成了它的上下文。上下文越小,对象 就越可复用、越容易测试。设计的目标是创造上下文无关的对象——它只需要知道 消息名和参数结构,不需要知道接收方的类型和来历。
当你搞不清对象之间该怎么通信时,画一张简单的序列图:
发送方 接收方
│ │
│──bicycles──────────│
│ │
│ for each bicycle: │
│──prepare(bike)────>│
│ │ (内部处理)
│<──done─────────────│
序列图帮你把注意力从"有什么对象"转移到"对象之间传递什么消息"。
"Referring to another class by its name creates a major sticky spot." — POOD Ch.3
坏代码: 在方法内部 new 或直接引用具体类名
gear_inches() {
return ratio * new Wheel(rim, tire).diameter()
}
好代码: 通过构造器/参数注入依赖
constructor(chainring, cog, wheel) {
this.wheel = wheel // wheel 只需要响应 diameter()
}
gear_inches() {
return ratio * this.wheel.diameter()
}
注入的不是"Wheel 的实例",而是"一个能响应 diameter 消息的对象"。 这个思维转变解放了你的设计——测试时可以注入假的 wheel,未来可以注入其他 实现了 diameter 的东西。
依赖应该指向更稳定的方向:
自检问题: 如果 A 依赖 B,A 修改的频率是否高于 B?如果不是,考虑反转依赖。
如果无法消除依赖(如调用第三方库),至少隔离它:
这样当第三方库变化时,只有适配层需要修改。
"It is more important that a well-defined interface exists than that it be perfect." — POOD Ch.4
公共接口是其他对象依赖的合约。设计公共接口时:
| 公共接口 | 私有接口 | |
|---|---|---|
| 可见性 | 对外暴露 | 仅内部可见 |
| 稳定性 | 稳定,改变需谨慎 | 可任意重构 |
| 测试 | 必须测试 | 不直接测试(通过公共接口覆盖) |
| 数量 | 越少越好 | 不限 |
"Talk only to your immediate friends."
a.b.c.d 这种消息链是坏味道。它意味着 a 知道 b、b 知道 c、c 知道 d——链条上的
任何改动都影响 a。
修复方式: 不是让 a 知道更少,而是让 b 提供更高级别的行为。把 a.b.c.d 变成
a.something_meaningful,让 b 内部负责处理 c.d。
(POOD Ch.6-8)
继承是"is-a"关系——子类是父类的一种特殊化。
用继承的场景:
继承的风险:
规则: 继承层级不超过 1-2 层。使用模板方法模式,让父类定义骨架,子类填空。 父类提供"钩子消息"(hook messages),让子类可选地参与行为。
组合是"has-a"关系——对象由其他对象组装而成。
用组合的场景:
组合的优势:
| 问题 | 答案指导方向 |
|---|---|
| 子类是父类的特殊化还是变体? | 特殊化→继承;变体→组合 |
| 子类需要父类的所有行为吗? | 不是 → 组合 |
| 子类和父类会同时被修改吗? | 是 → 组合(分开演进) |
| 运行时需要切换吗? | 是 → 组合 |
默认用组合。只有当你确信"is-a"且结构足够稳定时,才选继承。
(POOD Ch.5, Ch.7)
"If it quacks like a duck..."
鸭子类型是消息思维的自然延伸:你不关心对象的类,只关心它响应什么消息。
# Trip 不在乎 preparer 是 Mechanic、Driver 还是 Chef
# 它只需要 preparer 能响应 prepare_bicycle(bike)
def prepare(preparers)
preparers.each { |preparer| preparer.prepare_bicycle(bike) }
end
角色(Role)是一组消息的合约——不通过类继承,而是通过实现了相同的一组消息 来共享行为。角色是接口的抽象:一组对象扮演同一个角色,意味着它们都响应同一 组消息。
何时定义角色:
(99 Bottles Ch.3)
代码 smell 不是 bug——它是暗示设计问题的结构特征。以下是常见 smell 及对应的 修复策略:
| Smell | 表现 | 修复方向 |
|---|---|---|
| Duplicated Code | 相似代码出现在两个以上位置 | Flocking Rules → 提取共性 |
| Switch Statements | 基于类型的 switch/if-else 散落各处 | 用多态替换条件 |
| Long Method | 方法超过 5-10 行 | 提取更小的方法,每个只做一件事 |
| Large Class | 类超过 100 行或有 10+ 个公共方法 | 按职责拆分为多个类 |
| Primitive Obsession | 用基础类型(string/number)表示领域概念 | 封装成值对象 |
| Feature Envy | 方法对一个类的数据访问超过对自己类的访问 | 把方法挪到它真正关心的类里 |
| Inappropriate Intimacy | 两个类知道彼此太多内部细节 | 提取公共接口,减少直接访问 |
| Shotgun Surgery | 一个变更需要同时修改多个类 | 把相关职责集中到一个类 |
| Divergent Change | 一个类因为不同原因被修改 | 按变更原因拆分类 |
| Data Clumps | 一组数据总是一起出现 | 封装成新的类 |
当你面对多个 smell 时,问自己:
(POOD Ch.9)
测试不只是保证正确性——它降低代码变更的成本。好的测试让你敢于重构。
| 测试类型 | 测什么 | 不测什么 |
|---|---|---|
| 入站消息(公共接口) | 对给定输入返回正确输出 | — |
| 出站命令消息(对外有副作用) | 是否发送了正确的消息 | — |
| 出站查询消息(对外无副作用) | — | 不测(发消息本身不是目的,结果才重要) |
| 私有方法 | — | 不直接测(通过公共接口覆盖) |
| 角色(鸭子类型) | 所有扮演该角色的对象都遵守合约 | — |
"Tests are the first reuse of code." — POOD Ch.9
如果测试某个对象需要创建大量其他对象,说明你的设计耦合太紧——代码被 测试揭示出了设计问题。测试的痛苦是设计问题的信号。
以下是 AI 写代码时应当应用的具体行动准则:
先从 Shameless Green 开始:用最简单直白的方式实现功能。不要预判未来。 简单的代码即使要改,改起来也快。复杂的"抽象"一旦错了,改起来代价巨大。
优先用消息思维:不要想"我有哪些类",而是想"这些对象之间传递什么消息"。 先设计消息,再根据消息需要的接收者来创建对象。
问"要什么"不告诉"怎么做":方法名应该描述需求(prepare_bicycle),
而不是实现步骤(clean_bicycle + pump_tires + check_brakes)。
确保 TRUE:写完代码后自检——透明吗?合理吗?可复用吗?是好的范例吗?
用自然语言朗读检查设计:写完关键调用后,试着用"主语-动词-宾语"句式朗读。 如果读起来像操作流程图而非自然语句("先做这个、再做那个"),说明职责划分有问题。
用 Flocking Rules 重构:面对重复代码或 switch 语句,不要试图一步到位写出 抽象。遵循:找最相似 → 找最小差异 → 消除差异 → 测试通过 → 重复。
一次只修一个 smell:不要同时重构多个问题。选一个最小的 smell, 用 Flocking Rules 消除,测试通过后再看下一个。
依赖注入是默认选择:不要在方法内部创建依赖对象。通过构造器或参数传入。
推迟决策:当信息不足以做出正确判断时,先不做。等变更到来时, 新信息自然会告诉你该怎么设计。
默认组合,谨慎继承:用"has-a"而非"is-a"。只有当你确信子类是父类的 特殊化 + 结构足够稳定时,才使用继承。
测试痛苦 = 设计问题:如果写测试时需要创建大量 mock 或大量 setup, 停下来反思——是不是类之间的耦合太紧?是不是类做了太多事?