| name | programming-design-style |
| description | Apply the user's general programming and engineering design style in any project. Use broadly for coding, code changes, feature implementation, bug fixes, architecture design, module design, API design, data model design, state management, UI/backend/gameplay implementation, refactoring, code review, technical planning, implementation explanation, tests, tooling, scripts, editors, configuration flows, data/presentation separation, single-responsibility modules, configurable behavior, event-based decoupling, tool-first workflows, and AI-assisted software work. Trigger when the user says to write code, change code, implement, fix, debug, review, refactor, design a system/module, explain implementation, or otherwise work on software according to the user's preferred engineering habits. |
编程设计风格
使用目标
按用户的通用工程风格协助做设计、实现、评审和重构。不要绑定某个具体项目、框架、存档方案或工具名;项目里的 ModuleControl、BaseModule、编辑器工具等只作为来源示例,真正要继承的是背后的设计习惯。
需要更多来源材料时,读取 references/source-notes.md。
工作流程
- 先理解需求:确认用户要解决的问题、想要的效果、不做什么、有什么限制。
- 先找现有实现:搜索已有模块、配置、事件、工具、UI、数据流和调用范式,优先复用项目习惯。
- 先拆清楚再写代码:把业务动作、数据状态、表现反馈、配置来源、各模块负责的事、模块之间怎么配合讲清楚。
- 再实现一条能跑通的主流程:先把最关键的一条链路做出来,再补特殊情况、报错、工具和表现细节。
- 最后自查:检查是不是把很多事塞进了同一个类,数据和表现是不是混在一起,能配置的东西是不是写死了,是否重复造轮子,是否没验证。
复杂问题的工作方式
当任务涉及架构、重构、复杂功能设计、模块拆分,或者以后还要长期维护时,按“先看哪里难受 -> 给几个做法 -> 问到能开工 -> 把说定的事记下来”的方式推进。
先看哪里难受
先在代码和需求里找实际卡住的地方,不要凭空套架构:
- 理解一个概念是否需要在很多文件之间来回跳转。
- 某个模块的对外用法是否几乎和内部实现一样复杂。
- 业务数据、表现动作、配置解析、资源加载、工具流程是否混在同一处。
- 为了测试而抽出来的函数是否只有形式上的可测,真正的问题仍散落在调用顺序里。
- 跨模块协作是否泄漏了内部状态或隐含时序。
发现可疑的模块、函数或“只是转一下”的代码时,想一下:如果删掉它会怎样?如果只是让很多调用方都要重复写一遍麻烦逻辑,它就还有价值;如果删掉后事情反而简单了,它可能只是多绕了一层。
先给几个做法
不要一上来只给唯一答案。先列 1-3 个做法,每个做法说明:
- 涉及哪些模块、文件,分别管什么事。
- 现在到底哪里难改、哪里绕、哪里容易出错。
- 准备把哪些事收拢到一起,哪些事拆出去,模块之间怎么换一种配合方式。
- 对数据和表现分离、动作列表、配置外置、事件/消息配合有什么影响。
- 验证会变简单还是更复杂。
- 推荐程度:强烈建议、可以试试、先放一放。
复杂关系优先用表格、流程图或 Mermaid 图表达。
问到能开工
用户选中方向后,继续问关键问题,把设计问到能直接开工。重点围绕限制、命名、谁管什么、数据状态、动作列表、配置入口、报错处理和怎么验证。
提问时避免泛泛问“还有什么要求”。优先问会改变做法的问题,例如:
- 这个状态谁拥有最终解释权?
- 这个表现动作是否需要排队、合并、跳过或回放?
- 这个配置由谁维护,错误时谁负责提示?
- 别的模块最少需要知道它什么?
- 如果以后换 UI、换资源或换存储,这里是否要改业务逻辑?
把说定的事记下来
如果讨论中出现了以后会反复用到的新概念,给它起一个清楚的名字,并写到项目文档、注释、设计文档或任务说明里。若用户明确拒绝某个方向,而且理由以后也会用到,建议把这个决定记下来,避免以后重复提出。
大幅重构先归档
如果用户准备或要求做大幅重构、替换旧架构、删除旧实现、迁移生成逻辑,先不要直接删旧文件。优先在项目根目录创建 archive/ 目录,并按原项目相对路径完整镜像旧文件位置,把即将废弃的旧实现复制进去。例如原文件是 src/foo/bar.js,归档位置就是 archive/src/foo/bar.js。这样后续可以对照旧行为、找回手调数据、确认不是误删新逻辑。
归档不是继续维护两套实现。完成迁移后,新逻辑必须有唯一入口;旧入口要么从正式调用链移除,要么在代码、索引、注册表、文档和测试里明确标注为过时,不允许“旧逻辑还活着但没人知道”。如果项目存在 SQL、迁移表、生成索引、工具 registry、任务表或文档索引,也要同步更新,避免下次 Agent 或工具又走回旧路径。
核心原则
模块化入口
优先把业务能力组织成清晰模块。可以使用模块中心、注册表、服务容器、依赖注入、路由器或明确的组合根来集中组织能力,但不要强制套用某个项目的类名。
模块要讲清楚:它负责什么、不负责什么、依赖什么、给别人用什么、通过什么事件或方法通知外部。
一个东西只干一类事
尽可能让一个类、组件或函数只做一种清楚的事情。发现一个对象同时处理数据、UI、动画、配置解析、资源加载、网络请求和跨模块协调时,优先把这些事情拆开。
拆分时不要为了拆而拆。只有当拆分能让代码少互相影响、更容易复用、更容易验证,或者让策划、美术、运营更容易调整时才拆。
身份字段按场景设计
不要无脑给所有东西加 UID。先判断这个对象的身份在当前场景里由什么保证:配置路径、资源地址、配置内部键、服务端 ID,还是运行时实例 ID。身份字段属于内部系统管理信息,需要时必须由系统自动生成、计算或由受控生成器分配,不作为人工配置内容。类似 Git commit hash、SVN revision 或发布版本号,它们是提交/发布流程产出的身份,不是人手写出来的身份。
配置文件通常不需要额外 UID。大多数配置文件以路径、资源地址或加载键作为引用源:谁要用这个配置,就通过路径、Addressable key、资源引用、配置表 key 或项目约定的加载入口找到它。只要项目规则允许这些路径/键长期稳定,就不要再给整个配置文件额外维护一套 UID。
只有配置文件内部存在会被独立长期引用的子项时,才考虑给子项稳定 ID。例如一个配置文件里包含多个关卡节点、奖励项、行为节点或可被外部引用的条目,而且这些条目会被存档、其他配置、DLC、补丁、运营数据、远端数据或外部工具引用。此时 ID 要由编辑器工具、导入工具或创建命令自动生成,并持久化到配置条目上;可以使用 GUID/UUID 生成库,也可以使用项目自定义的递增分配函数。
递增分配必须是受控生成器,而不是根据数组下标、当前文件顺序或临时最大值随手算出来。DLC、分支合并、直接删除旧配置、复制配置文件或多人编辑都会破坏非受控递增值的长期唯一性。只有存在明确作用域的自定义递增函数、中心化生成器、服务器分配器或发布流水线分配器时,递增 ID 才适合作为稳定身份。
运行时实例要和配置身份分开。一个运行时对象通常保存“它来自哪个配置”(例如配置路径、资源地址、Addressable key 或配置表 key)以及“这个实例自己的 UID”。只有当实例需要保存运行期数据、进入存档、跨帧/跨系统追踪、网络同步、回放或被其他对象长期引用时,才需要实例 UID。实例 UID 由生成入口在合适作用域内自动生成,可以来自 GUID/UUID 生成库、自定义递增函数、会话内分配器、存档分配器或服务端分配器;具体取决于它会被谁引用、保存和同步。
大部分数据获取、引用、更新、删除和跨模块传递,优先通过稳定身份进行。名字、标题、显示文本、排序位置和数组下标可以用于展示或辅助查找,但不要作为长期稳定身份;如果路径会被频繁移动,也不要把路径当长期主引用。
确保唯一数据源
每个业务事实都应该有唯一真源。无论数据来自配置表、运行时实体、存档、服务端状态还是编辑器资源,都要先判断“这个值到底归谁拥有”。除非明确是在做复制、快照、缓存、网络传输 DTO、历史记录或回放记录,否则不要 new 一个对象再把另一处的字段逐个拷贝进去,制造第二份看起来一样但可能分叉的数据。
配置数据和实例数据要分清。配置表负责静态定义,运行时实例只保存身份、状态变化和运行期差异。比如卡牌配置 CardConfig 里已经有 Atk,那么 CardInstance 通常只应该保存 ConfigId 和运行时状态,不应该再有一份 Atk,也不应该长期直接持有 CardConfig 对象;需要攻击力时,通过 ConfigId 到配置索引里读取 CardConfig.Atk。如果运行中攻击力会被修改,就要把它命名和建模成运行时状态或 modifier,而不是悄悄复制配置字段。
数据从属和管理器归口
设计数据时先判断它属于哪类管理器,而不是先塞进当前业务对象。配置数据、存档数据、运行时实例数据、服务端状态、资源索引和表现配置,都应该有清楚的归属入口。一个模块可以使用这些数据,但不代表它拥有这些数据。
配置数据归配置管理器、配置索引、资源加载入口或项目约定的配置服务维护。业务模块通过配置 key、路径、Addressable key、表 ID 或受控引用读取配置,不长期保存配置对象副本,也不在业务模块里临时解析和缓存一套无法统一失效的配置表。配置校验、默认值、缺失提示和索引构建优先放在配置归口处。
存档数据归存档管理器、玩家进度管理器、账号状态服务或对应业务数据仓库维护。运行时模块不要各自写文件、各自拼存档字段、各自决定持久化格式;它们应该提交明确的状态变更,或者通过归口管理器读取和更新存档事实。需要落盘、回滚、迁移、云同步、版本兼容和脏标记时,也由存档归口统一处理。
运行时实体只持有自己真正拥有的状态:实例身份、当前状态、临时计算结果、对配置/存档的引用键,以及必要的运行期差异。它不应该因为“用起来方便”就把配置字段、存档字段、表现字段都拷进自己身上。跨模块要读取数据时,优先通过拥有该数据的管理器或只读查询接口,而不是直接伸手拿别的模块内部字段。
当一个新字段出现时,先问三件事:它是配置事实、存档事实、运行时事实,还是表现配置?它的最终解释权归哪个管理器?当前类保存的是最终状态、引用键、缓存、快照,还是 DTO?这些问题答不清时,通常说明数据从属还没设计好。
允许缓存,但缓存必须显式声明。缓存字段、缓存字典或缓存对象的命名要带有 Cache、Cached 或项目约定的缓存前缀/后缀,让读代码的人一眼知道它不是唯一真源。缓存要能被重建,失效时机要清楚,不能让缓存字段承担最终业务状态。
参与逻辑判断、跨模块引用、资源查找、表现选择或状态切换的符号值,也必须有唯一真源。不要在业务逻辑里零散写 "normal_idle2"、"fishing_start"、"Done"、3 这类硬编码字符串、硬编码枚举名或状态 int。只要这个值未来可能被策划、美术、配置、工具、状态机、服务端协议或另一个模块共同引用,就应该收拢成命名常量、配置字段、枚举/状态对象、资源 key 注册表、动画 key 表或受控映射。否则两个地方都直接写同一个字面量时,代码搜索只能看到重复文本,看不出谁引用谁、谁拥有这个值、改名要改哪些调用点,也无法验证是否漏改。
只有非常局部、一次性、不会被外部引用、不会参与业务判断且几乎确定不需要调整的字面量,才可以留在当前位置。例如纯调试日志、临时测试文本或只在一个函数内部展示一次的固定分隔符。只要它表达的是业务状态、动画名、资源名、表字段名、协议状态、配置 key、UI 文案 key 或流程阶段,就优先给它一个有语义的入口。
数据和表现分离
先设计稳定的数据状态,再设计表现层如何响应它。业务数据不依赖 UI、动画、特效、音效、场景对象或当前画面状态。
表现层优先通过“动作列表”承接数据变化后的反馈编排。业务层产出数据变化和表现意图,表现层把它们转换成可排序、可组合、可跳过、可回放的动作列表,再依次执行 UI、动画、特效、音效、镜头和提示。
表现层可以通过事件、观察、绑定、刷新函数、返回结果或明确方法读取数据并生成动作列表。表现可以做预览、过渡和临时状态,但最终业务状态以数据层为准。
不要在业务流程中零散直接调用具体动画、特效或 UI 细节;需要表现反馈时,优先产出动作描述或表现命令,再交给表现层调度。
配置和表现外置
动效、渲染、特效、数值、关卡参数、行为节点、UI 文案和可调节体验,优先设计成外部可编辑或工具可维护。不要把策划、美术、运营需要调整的内容写死在程序里。
如果某类内容会频繁调整,优先考虑配置表、编辑器面板、资源引用、行为树节点、命令系统或其他工具化入口。
表现资源的选择也不要散落在业务逻辑里。动画名、特效名、音效 key、Sprite/Prefab key、UI 状态名如果会被多处播放、切换、预览或被工具扫描,应该由表现配置、资源映射或命名常量统一维护。业务层最多表达“进入钓鱼待机”“播放拖拽反馈”这类意图,具体对应哪个动画片段或资源 key 由表现层或配置决定。
模块之间少互相伸手
跨模块协作优先考虑事件、消息、回调、观察者、命令或明确方法,避免一个模块为了刷新表现或触发后续效果,直接深入修改另一个模块的内部状态。
事件名、消息类型和回调参数要集中、可搜索、可追踪。事件不是遮羞布:如果调用方必须知道对方内部细节,说明模块之间的分工仍然没想清楚。
分层和代码归类
写代码前先判断它属于哪一层:
- 引擎或框架能力
- 第三方插件适配
- 项目通用工具
- 业务模块
- 场景实体或运行时组件
- UI 和表现控制
- 编辑器、配置或资源工具
代码应放在最适合的位置。通用工具不要反向依赖具体业务;业务代码不要污染底层框架;UI 不要负责保存最终业务状态。
工具优先
重复、易错、需要非程序角色参与、需要批量处理或需要稳定验收的工作,优先做成工具、配置流程或编辑器扩展。
工具设计要服务实际协作:少让人记步骤,多让工具承载默认值、校验、批量操作和可视化反馈。
编辑器存档文件化
编辑器、路线工具、地图工具、静态报告和调试面板里的“保存”,默认必须写入项目文件,例如 JSON、CSV、Markdown 或项目已有存档格式。浏览器 localStorage、sessionStorage、IndexedDB、URL 参数和内存变量只能作为缓存、草稿恢复、性能加速或临时预览状态,不能作为正式存档真源。
如果工具运行在浏览器里,需要保存项目数据,优先设计 HTTP 本地工具接口、脚本命令或明确的文件写入流程。静态页面不能写文件时,UI 必须明确显示“已缓存/未保存到文件”,并提供可复制 JSON 或其他兜底,而不是把缓存叫做保存。
正式保存时先归档旧文件,再写新文件。归档文件放在同目录或项目约定的 archive/ 目录,文件名带日期时间后缀。多步骤生成工具要让每一步的数据独立存在:当前步骤基于上一步生成自己的结果,不能静默覆盖上一步源数据。
AI 协作方式
让 AI 先读现有实现和真实 API,再设计和修改。要求 AI 明确哪些地方复用、改哪些地方、不改哪些地方、数据怎么走、表现怎么刷新、怎么验证。
当 AI 给方案时,优先让它输出容易检查的设计:每个模块管什么、流程图、状态怎么变、模块关系、配置字段、事件列表或验证清单。不要只让 AI 直接堆代码。
设计输出建议
复杂功能优先输出这些内容:
- 模块分工表:每个模块负责什么、不负责什么。
- 数据流:数据从哪里来、如何变化、谁持有最终状态。
- 表现流:UI、动画、特效如何通过动作列表响应数据变化。
- 配置入口:哪些内容需要配置、由谁维护、如何校验。
- 数据从属:配置数据、存档数据、运行时状态、服务端状态、资源索引和表现配置分别归哪个管理器;业务模块通过什么身份或接口访问。
- 身份字段:哪些对象不需要 UID、哪些配置子项或运行时实例需要 UID、UID 由 GUID/UUID 库还是自定义分配器自动生成、通过什么索引或注册表查询。
- 唯一真源:每类业务事实由配置、实例、存档还是服务端拥有;哪些字段只是引用、运行时状态或缓存。
- 符号值入口:哪些字符串、枚举、状态 int、动画 key、资源 key 或协议码会被多处引用;它们应由常量、配置、枚举/状态对象、注册表还是工具生成。
- 协作方式:模块之间用方法、事件、命令还是服务调用。
- 工具化点:哪些重复操作值得做成工具。
- 验证方式:如何确认数据正确、表现正确、配置错误有提示。
- 几个做法:复杂设计先给 1-3 个做法,并标明推荐程度。
自查清单
- 是否把数据状态和表现反馈混在一起了?
- 是否区分了配置文件路径身份、配置内部子项身份和运行时实例身份?
- 是否明确了配置数据、存档数据、运行时状态和表现配置各自归哪个管理器,而不是散落在使用方对象里?
- 是否把需要长期追踪的运行时实例设计成通过自动生成的实例 UID 获取、引用和更新,而不是依赖名字、数组位置或非受控递增值?
- 是否每个业务事实都有唯一真源,避免无意义复制配置字段或实体字段?
- 是否把参与逻辑判断或跨处引用的字符串、枚举、状态 int、动画 key、资源 key 收拢到了一个命名入口,而不是在多个逻辑点直接写字面量?
- 如果某个字段是缓存,命名是否明确带有
Cache、Cached 或项目约定的缓存前缀/后缀,并且可重建、可失效?
- 是否把可调节内容写死在代码里了?
- 是否有类或函数承担太多事情?
- 是否先查过现有模块、工具、配置和事件?
- 是否有一个模块直接改另一个模块内部状态的情况?
- 是否区分了通用工具、业务逻辑、UI 和运行时组件?
- 是否有重复流程可以工具化?
- 是否能用一条主流程验证当前设计?
- 是否先找到了实际卡住的地方,而不是为了设计而设计?
- 是否把几个做法问清楚,收到了能开工的程度?