通过分阶段计划将原型重构为模块化结构,使每次变更都保持小、易测且可回滚,覆盖路由、服务、数据库与 UI。

原型之所以感觉快,是因为所有东西都紧靠在一起。一个路由直接访问数据库、整理响应,然后 UI 渲染它。这种速度是真实的,但它掩盖了代价:一旦更多功能加入,最初的“快速路径”就会变成所有东西依赖的路径。
首先出问题的通常不是新代码,而是旧假设。
对路由做一个小改动就可能悄悄改变响应结构,从而破坏两个界面。一个“临时”查询被复制到三个地方后开始返回略有差别的数据,没人知道哪个是正确的。
这也是大规模重写即便出于好意也会失败的原因。它们同时改变结构和行为。当出现 bug 时,你无法判断原因是新的设计选择还是基本错误。信任下降,范围膨胀,重写被拖延。
低风险重构意味着把改动保持得小且可逆。你应该能在任何一步停手,并且应用仍然可用。实践中的规则很简单:
当每一层开始做别的层的工作时,路由、服务、数据库访问和 UI 就会纠缠在一起。解开纠缠不是去追求“完美架构”,而是一次移动一根线。
把重构当成一次搬家,而不是翻修。保持行为不变,并让结构更容易在以后改变。如果你在重组时顺便“改进”功能,你会忘了是什么地方出了问题。
写下哪些内容暂时不变更。常见的“暂不”项:新特性、UI 重设计、数据库模式更改以及性能优化。这个边界能保持工作低风险。
选一个“黄金路径”用户流程并保护它。选择用户每天都会做的事,比如:
sign in -> create item -> view list -> edit item -> save
你会在每个小步骤后重新跑这个流程。如果行为相同,就可以继续前进。
在第一次提交前就约定回滚方式。回滚应该很无聊:一次 git revert、短期 feature flag,或可恢复的平台快照。如果你在 Koder.ai 上构建,快照与回滚是你在重组时的有用保障。
为每个阶段保留一个小的完成定义。你不需要很长的清单,只要足够防止“迁移+修改”悄悄发生:
如果原型只有一个文件同时处理路由、数据库查询和 UI 格式化,不要一次性拆分所有内容。先把路由处理器移动到一个文件夹,保持逻辑不变,即使需要复制粘贴也行。一旦稳定,再在后续阶段提取服务和数据库访问。
在开始前,先把现状做个映射。这不是重设计,而是个安全步骤,方便你做小且可逆的移动。
列出每个路由或端点,并用一句话说明它的作用。包括页面路由和 API 路由。如果你用聊天驱动的生成器导出过代码,也按同样方式处理:清单应把用户看到的内容与代码实际触及的内容对应起来。
一个轻量但有用的清单:
为每个路由写一条简单的“数据路径”说明:
UI 事件 -> handler -> 逻辑 -> DB 查询 -> 响应 -> UI 更新
在过程中,标注出高风险区域,这样在清理临近代码时不会不小心改动它们:
最后,草拟一个简单的目标模块映射。保持扁平。你是在选择目的地,而不是构建一个新系统:
routes/handlers, services, db (queries/repositories), ui (screens/components)
如果你无法解释一段代码应放在哪里,那这块代码就是一个适合以后重构的候选,等你建立了更多信心再来做。
先把路由(或 controller)当作边界,而不是改进代码的地方。目标是在把端点放到可预测的位置时保持每个请求的行为不变。
为每个功能域创建一个薄薄的模块,例如 users、orders 或 billing。避免“移动时清理”的做法。如果你在同一个提交中既重命名又重组文件又重写逻辑,就很难发现哪里出错了。
一个安全的顺序:
具体示例:如果你有一个单文件处理 POST /orders,它解析 JSON、校验字段、计算总额、写数据库并返回新订单,不要重写它。把处理器提取到 orders/routes,并调用旧逻辑,比如 createOrderLegacy(req)。新的路由模块成为前门;旧逻辑暂时保持不动。
如果你在处理生成的代码(例如在 Koder.ai 中导出的 Go 后端),心态不变。把每个端点放到可预测的位置,封装遗留逻辑,并证明常见请求仍然成功。
路由不是业务规则的好归宿。它们会迅速增长、混合关注点,每次改动都显得风险很大,因为你触及了所有东西。
为每个面向用户的动作定义一个服务函数。路由应收集输入、调用服务并返回响应。把数据库调用、定价规则和权限检查移出路由。
当服务函数只做一件事、输入清晰、输出明确时,它们更容易理解。如果你不断在里面添加“还有……”,就把它拆分。
一个常用的命名模式:
CreateOrder(input) -> orderCancelOrder(orderId, actor) -> resultGetOrderSummary(orderId) -> summary把规则放在服务层,而不是 UI。例如:不要仅靠 UI 根据“高级用户可以创建 10 个订单”来禁用按钮,把这条规则放在服务层强制执行。UI 仍然可以展示友好提示,但规则应只在一处。
在继续之前,增加足够的测试以使改动可逆:
如果你使用像 Koder.ai 这样的快速生成/迭代工具,服务层会成为你的锚点。路由和 UI 可以演进,但规则保持稳定且易测。
一旦路由稳定且服务存在,就不要让数据库“随处可见”。把原始查询隐藏在一个小而无聊的数据访问层后面。
创建一个小模块(repository/store/queries),暴露少量命名清晰的函数,例如 GetUserByEmail、ListInvoicesForAccount 或 SaveOrder。不要追求优雅。目标是为每个 SQL 字符串或 ORM 调用找到一个明显的位置。
把这个阶段严格限定在结构上。避免模式变更、索引调整或“既然在这里顺便做了”的迁移。这些应当是各自计划好的变更与回滚。
一个常见的原型味道是事务散落各处:一个函数开始事务,另一个默默打开自己的事务,错误处理各不相同。
相反,创建一个入口点在事务中运行回调,并让 repository 接受事务上下文。
保持移动小颗粒度:
例如,如果“创建项目”既插入项目又插入默认设置,把这两次调用放在同一个事务 helper 中。如果某处失败,你就不会出现只有项目存在但其设置不存在的情况。
一旦服务依赖接口而不是具体的 DB 客户端,你就能在不使用真实数据库的情况下测试大部分行为。这会降低恐惧感,这正是本阶段的目的。
UI 清理不是为了美化,而是为了让屏幕可预测并减少意外副作用。
按功能把 UI 代码分组,而不是按技术类型。一个功能文件夹可以包含其页面、更小的组件和本地 helper。当你看到重复的标记(相同的按钮行、卡片或表单字段),就抽取它,但先保持标记和样式不变。
保持 props 简单。只传递组件需要的内容(字符串、id、布尔值、回调)。如果你传了一个巨大的对象“以防万一”,就定义一个更小的结构。
把 API 调用移出 UI 组件。即使有了服务层,UI 代码仍常常包含 fetch、重试和映射。为每个功能(或每个 API 区域)创建一个小的客户端模块,返回页面可直接使用的数据。
然后统一屏幕间的加载和错误处理。选一个模式并复用:可预测的加载状态、带一个重试操作的统一错误信息、以及能说明下一步的空状态。
每次抽取后都做一个快速的界面检查:点击主要动作、刷新页面并触发一个错误用例。小步胜过大改。
想象一个小原型包含三个页面:登录、列出项、编辑项。它能用,但每个路由都混合了认证检查、业务规则、SQL 和 UI 状态。目标是把这个功能变成一个干净的模块,变更可回退。
之前,“items” 逻辑可能分散如下:
server/
main.go
routes.go
handlers.go # sign in + items + random helpers
db.go # raw SQL helpers used everywhere
web/
pages/
SignIn.tsx
Items.tsx # fetch + state + form markup mixed
之后,行为保持相同,但边界更清晰:
server/
routes/
items_routes.go
handlers/
items_handler.go
services/
items_service.go
repositories/
items_repo.go
web/
pages/
Items.tsx
components/
ItemForm.tsx
先移动代码而不改逻辑。保持函数名和返回形状相同,这样回滚主要就是文件移动。
先重构一个路由。处理器只应处理 HTTP,不做规则或 SQL:
// handlers/items_handler.go
func (h *ItemsHandler) UpdateItem(w http.ResponseWriter, r *http.Request) {
userID := h.auth.UserIDFromRequest(r) // same auth logic as before
itemID := mux.Vars(r)["id"]
var in UpdateItemInput
json.NewDecoder(r.Body).Decode(&in)
item, err := h.itemsService.UpdateItem(r.Context(), userID, itemID, in)
if err != nil { writeError(w, err); return }
writeJSON(w, item)
}
接着,把业务规则移动到服务层。在这里放像“用户只能编辑自己的项”或“名称不能为空”这类检查。服务调用 repository 执行 DB 工作:
// services/items_service.go
func (s *ItemsService) UpdateItem(ctx context.Context, userID, itemID string, in UpdateItemInput) (Item, error) {
if strings.TrimSpace(in.Name) == "" { return Item{}, ErrInvalidName }
return s.repo.UpdateItem(ctx, userID, itemID, in)
}
然后在 repository 中隔离数据库访问:
// repositories/items_repo.go
func (r *ItemsRepo) UpdateItem(ctx context.Context, userID, itemID string, in UpdateItemInput) (Item, error) {
row := r.db.QueryRowContext(ctx,
`UPDATE items SET name=$1 WHERE id=$2 AND user_id=$3 RETURNING id, name`,
in.Name, itemID, userID,
)
var it Item
err := row.Scan(&it.ID, &it.Name)
return it, err
}
在 UI 端,保持页面布局,但把重复的表单标记抽成一个被“新建”和“编辑”共用的组件:
pages/Items.tsx 保留抓取与导航components/ItemForm.tsx 负责输入字段、校验提示和提交按钮如果你使用 Koder.ai(koder.ai),它的源代码导出在做更深层重构前会很有用,快照/回滚能帮助你在移动出问题时快速恢复。
最大风险是把“移动”工作和“修改”工作混在一起。当你在同一个提交中既移动文件又改写逻辑,错误会隐藏在嘈杂的 diff 里。保持迁移无趣:相同的函数、相同的输入、相同的输出,只是换了个地方。
另一个陷阱是清理时改变了行为。重命名变量可以;但重命名概念就不行。如果 status 从字符串变为数字,你改变的就是产品而非代码。那类改动要留到之后带着清晰测试并有计划地发布。
一开始你可能想为未来构建一个很大的文件夹树和多层结构。那通常会拖慢你并让你更难看清真实工作所在。先从最小有用的边界开始,当下一个特性迫使你时再扩展。
还要注意 UI 直接访问数据库(或通过 helper 调用原始查询)的捷径。它看起来快,但让每个页面都要负责权限、数据规则和错误处理。
风险放大器要避免:
null 或通用消息)一个小例子:如果某屏幕期望 { ok: true, data },但新的服务返回 { data } 并在出错时抛出异常,半个应用可能就停止展示友好消息。先在边界处保持旧形状,然后逐个迁移调用方。
在下一步之前,证明你没有破坏主要体验。每次都跑相同的黄金路径(登录、创建项、查看、编辑、删除)。一致性能帮助你发现小的回归。
每个阶段后使用一个简单的通过/不通过门:
如果有一项不通过,停下来修复再往上构建。小裂缝之后会变大。
合并后花五分钟验证能否回退:
胜利不在于第一次清理,而在于随着功能增加依然保持形状。你不是追求完美架构,而是让未来改动可预测、小且容易撤销。
根据影响度和风险选择下一个模块,而不是选择最令人烦恼的地方。好的目标是用户经常触达的部分,且行为已经被理解。把不明确或脆弱的区域留到你有更好的测试或更明确的产品答案时再做。
保持简单节奏:小的 PR 移动一件事、短周期审查、频繁发布,以及一个停止规则(如果范围扩大,就拆分并先发布小块)。
在每个阶段之前设定一个回滚点:git tag、release 分支或你知道可部署的工作构建。如果你在 Koder.ai 上构建,Planning Mode 可以帮助你分阶段变更,避免不小心同时重构三层。
模块化应用架构的实用规则:每个新特性遵循相同边界。路由保持薄、服务负责业务规则、数据库代码集中在一处、UI 组件专注于展示。当新特性违背这些规则时,越早重构越好,因为改动还小。
默认做法:把它看成风险。即便是细微的响应格式变化也能破坏多个页面。
改为这样做:
选择用户每天都会执行且涵盖核心层(认证、路由、数据库、UI)的流程。
一个合适的默认流程是:
保持流程足够短以便重复执行。也加上一个常见的失败用例(例如缺少必填字段),这样能尽早发现错误处理回归。
选择能在几分钟内执行的回滚方案。
实用选项:
尽早验证一次回滚(实际执行),不要只把它当作纸上方案。
一个安全的默认顺序是:
这个顺序能减少波及范围:每一层在你修改下一层前都成为更清晰的边界。
把“移动”和“改变”分成两个独立任务。
有用的规则:
如果必须改变行为,等到之后用明确的测试和有计划的发布单独做。
可以——把它当作任何遗留代码库来处理。
实用步骤:
CreateOrderLegacy)只要在外部行为上一致,生成代码就能安全地重组。
把事务集中并让它变得无聊。
默认模式:
这样能避免部分写入(例如创建了记录但其关联设置未写入),并让错误更容易追踪。
从能让改动可回退的最少覆盖开始。
最小有用测试集:
目标是减少恐惧感,而不是一夜之间构建完美的测试套件。
先保持布局和样式不变;关注可预测性。
安全的 UI 清理步骤:
每次抽取后做一个快速的视觉检查并触发一个错误用例。
使用平台的安全特性让变更小且可恢复。
实用默认做法:
这些习惯支持主要目标:可回退的小步重构,建立持续的信心。