面向对象设计
概览
本技能包适用于以面向对象风格编写的代码库的设计规则,并非通用建议。项目自身书写的约定优先于本技能包。遵循正在修改的模块的惯用风格。本技能会评估任务规模并指明适用的技能。
在回答之前加载此索引命名的技能:其规则和示例位于那里,而不在这里。
任务需要多少设计
| 任务 | 使用 | 跳过 |
|---|---|---|
| 一次性使用后被丢弃的答案 | 什么都不做 | 所有内容 |
| 现有类中的一个方法 | 其名称和契约 | 重构 |
| 现有流程中的功能 | 该流程的惯用风格 | 新边界 |
| 新类或新模块 | 设计循环 | 所有内容 |
| 新上下文 | 设计循环,然后确定其边界 | 所有内容 |
| 从零开始的新系统 | 设计循环,上下文,然后结构 | 所有内容 |
设计之前
设计之前,先陈述一条关卡规则:软件必须遵循的一条规则,并提供一个接受示例和一个拒绝示例。例如:“一个预约槽最多只能容纳一个预约。”它接受将第二个病人预约到不同的槽位,拒绝将第二个病人预约到同一个槽位。如果你能写下这条规则,就可以开始。如果不能,那么缺失的就不是设计,本技能包也无法提供。
设计循环
- 用领域语言表述软件必须做什么(使用领域语言),并找到了解每个行为所需的对象(分配职责)。
- 找出一致性边界:在同一个事务中哪些部分必须同时保持一致(设计聚合)。
- 确定每条规则的位置:实体、值对象或领域服务(放置领域逻辑)。
- 设计每个对象响应的消息(封装状态和行为),并使非法状态无法构建(使非法状态不可能)。
- 反转依赖关系,使领域层对数据库或框架一无所知(反转依赖,分层系统)。
- 用领域语言命名内容,并仅注释代码无法表达的内容(命名类和方法,注释代码无法表达的内容)。
- 根据以下红旗检查结果(审查对象设计)。
规则的严格程度
本技能包中的每个核心规则都带有三个标签之一,标签说明了它的严格程度。
- 规则。 违反即为缺陷。任何框架都不能成为借口。
- 默认。 除非满足指定条件,否则执行此操作。条件自成一句,而非尾随从句。
- 判断。 两种答案都可以辩护。技能会说明如何权衡。
症状到技能
症状指向处理它们的技能,随着新技能的添加,此表格会增长。
| 症状 | 技能 |
|---|---|
| 哪个类应该做这件事? | 分配职责 |
| 这个类做了太多事情 | 分配职责 |
| 一个变更波及许多类 | 诊断耦合 |
| 这两个部分之间的耦合有多紧密? | 诊断耦合 |
| 这是一个对象还是仅仅是数据? | 判断对象或数据 |
| 我们的领域模型是贫血的 | 判断对象或数据 |
| 其他代码替这个对象做决定 | 封装状态和行为 |
| Setter方法允许此对象违反其规则 | 封装状态和行为 |
| 此对象可能以半构建状态存在 | 构建有效对象 |
| 此构造函数参数过多 | 构建有效对象 |
| 每个新案例都需要编辑工作代码 | 封装变化 |
| 这应该有多少灵活性? | 封装变化 |
| 这是一个值对象还是一个实体? | 建模值和实体 |
| 原始字符串和数字承载了领域规则 | 建模值和实体 |
| 应该扩展那个类还是持有它? | 选择组合优于继承 |
| 更改基类会破坏其子类 | 选择组合优于继承 |
| 子类违反了其父类的承诺 | 尊重可替换性 |
| 代码检查它持有的是哪个子类型 | 尊重可替换性 |
| 对同一类型的相同 switch 语句反复出现 | 用多态替换条件逻辑 |
| 子类还是模式匹配? | 用多态替换条件逻辑 |
| 代码通过一系列对象进行链式调用 | 限制对象的知识范围 |
| 这个类只转发调用 | 限制对象的知识范围 |
| 这个类或方法应该叫什么? | 命名类和方法 |
| 像 Manager、Helper 或 Util 这样的名称 | 命名类和方法 |
| 调用者依赖于它们从不使用的接口方法 | 设计接口 |
| 接口还是抽象基类? | 设计接口 |
| 领域代码依赖于数据库或框架 | 反转依赖 |
| 对象在哪里创建并组装在一起? | 反转依赖 |
| 调用者知道另一个类如何存储或格式化数据 | 隐藏实现 |
| 类泄漏了自己的内部错误或步骤 | 隐藏实现 |
| 标志或可选字段允许不可能的组合 | 使非法状态不可能 |
| 类型允许领域从未拥有的值 | 使非法状态不可能 |
| 代码的用语与团队的不一致 | 使用领域语言 |
| 同一个词对两个团队意味着不同的事情 | 使用领域语言 |
| 哪些对象必须一起变化? | 设计聚合 |
| 保存冲突或一次加载拉入太多内容 | 设计聚合 |
| 允许哪些状态转换? | 建模生命周期 |
| 已取消或已关闭的对象再次变化 | 建模生命周期 |
| 领域对象应如何存储和查找? | 设计仓库 |
| 仓库有每个屏幕对应的 find 方法 | 设计仓库 |
| 用例重复相同的业务检查 | 放置领域逻辑 |
| 领域服务、实体方法还是应用服务? | 放置领域逻辑 |
| 保存变更后其他部分必须做出反应 | 发布领域事件 |
| 为回滚的变更发送了消息 | 发布领域事件 |
| 一个团队的模型在哪里结束,另一个团队的模型在哪里开始? | 界定上下文 |
| 哪些部分是核心的,哪些可以购买? | 界定上下文 |
| 层间应该以什么方式依赖? | 分层系统 |
| 屏幕或任务在没有其框架的情况下无法测试 | 分层系统 |
| 模块以循环方式相互依赖 | 组织模块 |
| 模块化单体还是独立服务? | 组织模块 |
| 对 API 的更改破坏了调用它的应用程序 | 跨越系统边界 |
| 我们应该如何对此 API 或消息进行版本控制? | 跨越系统边界 |
| 映射工具塑造了我们的领域类 | 驯服持久化 |
| 列表对每一行执行一个查询 | 驯服持久化 |
| 屏幕需要与模型形状不同的数据 | 分离读写 |
| 读取模型、投影还是事件溯源? | 分离读写 |
| 此屏幕使用 MVC、MVP 还是 MVVM? | 分离表示层与领域层 |
| 屏幕直接编辑领域对象 | 分离表示层与领域层 |
| 哪种设计模式适合这个问题? | 选择模式 |
| 问题出现之前添加了模式 | 选择模式 |
| 这些模式类可以是一个函数 | 语言提供的模式 |
| Visitor,还是带匹配的封闭集合? | 语言提供的模式 |
| 方法在找不到内容时返回 null | 设计失败行为 |
| 拒绝以调用者忽略的代码形式返回 | 设计失败行为 |
| 线程共享一个对象并丢失或混合其更改 | 管理共享状态 |
| 锁还是此状态的单个所有者? | 管理共享状态 |
| 对另一个系统的调用挂起或中途失败 | 使工作可靠 |
| 重试导致工作执行了两次 | 使工作可靠 |
| 我们无法根据记录解释事件 | 设计记录内容 |
| 我们应该记录、计数或审计什么? | 设计记录内容 |
| 审查发现了上帝类或熔岩流 | 识别反模式 |
| 我们应该先修复哪个反模式? | 识别反模式 |
| 即使行为未改变,测试也失败 | 测试面向对象代码 |
| 为此协作对象使用 Mock、Stub 还是 Fake? | 测试面向对象代码 |
| 测试依赖于时间或随机值 | 设计可测试性 |
| 代码被设为 public 仅仅是为了测试可以访问 | 设计可测试性 |
| 我们如何安全地重构这段代码? | 重构以面向对象设计 |
| 重构变得太大而无法完成 | 重构以面向对象设计 |
| 我们必须更改没有测试的代码 | 处理遗留代码 |
| 逐块替换旧系统 | 处理遗留代码 |
| 本次审查应检查什么? | 审查对象设计 |
| 哪些审查发现会导致合并被阻止? | 审查对象设计 |
| 没有人记得代码为什么是这个形状 | 记录设计决策 |
| 我们应该绘制什么,以及在什么级别? | 记录设计决策 |
| 注释重复代码或与代码矛盾 | 注释代码无法表达的内容 |
| 我们在哪里解释为什么这段代码看起来奇怪? | 注释代码无法表达的内容 |
| 框架钩子或标记持有一条业务规则 | 在框架内工作 |
| 我们应该在多大程度上顺应框架? | 在框架内工作 |
| 计算也保存、发送或更改其输入 | 应用函数式技术 |
| 哪些函数式技术适合面向对象代码? | 应用函数式技术 |
语言技能
语言技能将核心规则保留在一种语言中,并且从不替换它们。基础语言技能适用于没有框架的代码。
oo-java,来自object-oriented-design-java:records、sealed types、equals和hashCode、checked exceptions、Optional,以及关闭扩展的类。oo-kotlin,来自object-oriented-design-kotlin:data classes、sealed types、nullable types、使用by的委托,以及扩展函数。oo-typescript,来自object-oriented-design-typescript:branded identifiers、#private状态、abstract classes、decorators、discriminated unions,以及 composition root。oo-javascript,来自object-oriented-design-javascript:c
…
[截断以进行翻译]