Files
erqiwang_youle/docs/games/engineering/02-可扩展性与配置化.md
T

148 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 02 · 可扩展性与配置化
本篇把总则里的 **OCP(对扩展开放)** 和 **配置优先** 落成可直接套用的模式与判据,
并给出**避免过度设计**的红线——扩展性是为了"改得动",不是为了炫技。
---
## 1. 可扩展性模式(够用就好)
下面几种模式覆盖子游戏绝大多数扩展需求。**先问"现在真的需要扩展吗",需要再用**(见 §3)。
### 1.1 注册表 + 策略(Registry + Strategy)— 最常用
把"一族可替换的行为"抽象成**策略对象**,用**注册表**收集,运行时按上下文选一个执行。
新增行为 = 写一个策略 + 注册,**核心零改动**。
```
Registry(注册表) ── register(strategy) ──▶ [strategyA, strategyB, ...]
│
Context(只读上下文)──▶ 选择器 select(ctx) ─────────┘──▶ 命中的策略.execute(ctx)
```
- **适用**:AI 决策分级、规则变体、牌型识别族、结算规则族——"同一类事有多种做法"。
- **要点**:
- 策略只依赖**只读上下文**,不反向修改全局;上下文封装它需要的权威数据。
- 有**默认策略兜底**(保证任何输入都有结果),高级策略**按需叠加**。
- 策略之间**互不知道**对方,新增不影响既有。
- **例**:分级决策框架——`Context/Registry/Pipeline + 基础策略 + 占位高级策略`,默认走最低级,
高级留扩展点;新增高级策略三步(写策略对象 + register + 单测)零侵入。
### 1.2 管线 / 中间件(Pipeline)
把一个复杂处理拆成**有序的小步骤**,每步只做一件事、可独立增删。
```
输入 ──▶ [校验] ──▶ [规则A] ──▶ [规则B] ──▶ [收敛] ──▶ 输出
每一节点单一职责,可插拔、可测试
```
- **适用**:决策流水线、校验链、结算的多阶段计分(比精 → 冲关 → 霸王 → 零和)。
- **要点**:节点间用**明确的数据结构**传递,不靠隐式全局;任一节点可单测。
### 1.3 工厂(Factory)
把"根据类型创建/选择实现"的分支收敛到一处,调用方只要"我要一个 X",不关心怎么造。
- **适用**:胡牌检测按牌型分派、可用操作枚举、不同房型的配置构建。
- **要点**:工厂是**唯一**的创建入口,避免 `if(type==...)` 散落各处(那是并行逻辑的温床)。
- **例**:胡牌检测工厂——检测逻辑只此一份,各处调用它,不自行手搓顺子/搭子判定。
### 1.4 事件总线(EventBus)— 前端解耦,谨慎用
用发布/订阅让"状态变化"与"表现响应"解耦:处理器改完数据 `emit` 事件,表现层订阅刷新。
- **适用**:一次数据变化要驱动多个互不相关的表现(动画 + 音效 + 计分板)。
- **克制**:
- **不要**用事件做"隐式流程控制"——关键流程的因果应显式可追踪,别埋进一堆事件里。
- 事件是**通知**,不是**命令**;订阅方不该反向决定发布方的流程。
- 能直接函数调用讲清的因果,就别为"解耦"硬拆成事件。
### 1.5 统一访问层(Facade over data)
对"读权威数据"提供一个**统一入口**(如 DataAccessHelper 之类),下游都走它读,不各自摸索
数据结构。好处:数据结构演化时只改访问层一处;配合"缺失显式失败",把校验集中。
---
## 2. 配置优先于硬编码
目标:**让"会变的东西"从代码逻辑里分离出来,用数据驱动**。改规则变成改配置,而非改逻辑 + 重测。
### 2.1 什么必须外提(满足任一即提取)
| 判据 | 说明 | 典型 |
|------|------|------|
| **会变** | 规则调整时可能改动 | 分数值、阈值、倍率、局数、超时 |
| **复用** | 超过一个模块用到同一值 | 跨模块共享的 key、类型标识 |
| **无语义** | 裸值无法自解释业务含义 | 魔法数字、状态字符串 |
按语义归入分层的常量文件(**分数/计分类、规则配置类、类型枚举类**……),不要一个巨型常量堆。
### 2.2 什么不必外提(避免过度设计)
- 单函数内一次性的临时值(循环初值 `0`、空数组 `[]`)。
- 框架约定的固定串(模块加载路径等)。
- 自解释的布尔开关、纯展示标点文字。
> 三问法:**会变吗?复用吗?有语义需要命名吗?** 三个都"否",就地内联,别为提取而提取。
### 2.3 规则驱动:把玩法开关变成数据
复杂玩法的差异(人数/局数/扣卡方式/各种开关)应编码成**一份配置**,在入口处**解析一次**,
之后全流程**只读消费**:
```
房型/配置编码 ──(解析器,解析一次)──▶ 规则对象(结构化、只读)
│
各模块按需读取规则对象的字段,不再各自解析原始编码、不再散落 if
```
- **解析只此一处**:避免"多处各自解析同一编码"产生的不一致(这是 SSOT 在配置上的体现)。
- **规则对象只读**:下游不回写、不推断缺省;缺字段是**配置或解析的 bug**,应显式暴露。
- **新增一个玩法开关** = 编码加一位 + 解析器认它 + 消费点读它,**不改无关逻辑**。
### 2.4 配置注入优于全局魔法值
模块需要的配置**从入口传入 / 从权威配置对象读取**,而不是在模块内部埋一堆魔法值或直接摸全局。
这让模块**可测试**(注入不同配置跑用例)、**可复用**(换配置即换行为)。
---
## 3. 避免过度设计(同等重要的红线)
可扩展性有成本:抽象层、间接跳转、认知负担。**过度设计和硬编码一样有害**。
### 3.1 什么时候**不要**加抽象
- **只有一种实现、且看不到第二种的现实需求** → 不要为"将来也许"预留策略/工厂。先写直接实现。
- **YAGNI**:没有当前需求的扩展点就是负债;等第二个变体真的出现,再**重构**成模式(那时你更懂共性)。
- **一次性逻辑** → 不要包装成"通用框架"。通用性从**重复中提炼**,不是凭空设计。
### 3.2 判断"值不值得抽象"
| 加抽象 | 别加抽象 |
|--------|----------|
| 已有 ≥2 个真实变体,且还会增加 | 只有 1 个实现,扩展是想象的 |
| 变体频繁新增(规则/策略/牌型) | 逻辑稳定,多年不变 |
| 核心被扩展反复改动、回归频发 | 改动集中、影响面小 |
| 抽象后核心显著变简单、变稳定 | 抽象后跳转变多、更难读 |
### 3.3 成熟的判据:三次法则 + 就近演进
- **三次法则**:同样的东西第 3 次出现时再抽象;第 1、2 次容忍重复。
- **就近演进**:先把直接实现写对、写清;当扩展点**自然浮现**(真来了第二个变体),
再**收敛**成注册表/策略——此时抽象是"被现实拉出来的",最贴合,也最不会过度。
> 一句话:**扩展性是留给"已知会变"的地方的;对"稳定不变"的地方,简单直接才是最好的设计。**
---
## 4. 本篇小结
- 扩展四件套:**注册表+策略、管线、工厂、事件总线(谨慎)**,外加**统一访问层**;够用即止。
- 配置化:会变/复用/无语义→外提;玩法差异→规则对象解析一次、只读消费;配置注入优于全局魔法值。
- 反过度设计:**YAGNI + 三次法则 + 就近演进**;只有一种实现就别造框架,抽象从重复中提炼。
下一篇 [03-数据权威·错误处理·演进](./03-数据权威·错误处理·演进.md):把"正确性"的地基——数据权威、错误处理、演进纪律——讲透。