Controller 里那 80 行参数校验,让我意识到"拆三层"不是搬家是换脑

站长杂谈 21 浏览 0 回复 返回上级

上周重构一个老模块,原 Controller 将近三百行,我信心满满按"标准三层"开拆。结果 service 成了 controller 的翻译官,model 成了 service 的传声筒,代码没少几行,跳转还多了两层。同事 review 时问了句:"你这 service 除了调 model 还有别的吗?"我愣了半天。

后来逼着自己重新想,发现拆不干净的根本问题是没分清"谁对什么负责"。不是物理上分成三个文件就叫分层,得先给每层定个"不可退让的底线"。

我现在给自己定的规矩是这样的:

Controller 只干"接待员"的活:收参数、做最基础的格式校验(比如 id 是不是正整数)、抛给 service、把结果包成 json。业务判断?不碰。数据库操作?绝不。之前我常在 controller 里写 `if ($status == 3 && $type == 'vip')` 这种鬼东西,现在看到就往回抽。

Service 是"项目经理":编排流程、处理跨模型事务、组合数据。关键点是,service 可以调多个 model,但不能知道自己被谁调。我吃过亏:在 service 里写了 `if (request()->isAjax())` 这种依赖具体上下文的代码,后来 CLI 命令行复用的时候直接炸。现在 service 方法签名里该传的上下文参数,显式传进去,绝不偷偷摸 request。

Model 只管"这张表的事":字段处理、作用域查询、简单的关联预加载。但有个边界我很在意——model 层不碰缓存。之前图方便在 model 的 `boot` 里加缓存逻辑,结果后台直接改数据库后前端死活不更新,排查半天才发现缓存埋在 model 深处。现在缓存策略上升到 service 或单独的 cache 层去管,model 保持"无状态"。

说个具体改造例子。老代码里有个"用户签到领积分":

controller 里先查用户状态,再算今天签没签过,再调积分接口,再写日志,最后返回。拆的时候我第一次把"查状态"和"算签到"扔给 service,"写积分"和"写日志"还是 controller 在串。后来改成:controller 只收 `user_id`,service 里一个 `checkIn($userId)` 方法,内部调 `UserModel::canCheckInToday()`、`UserPointModel::addPoints()`、`CheckInLogModel::create()`,整个事务包在 service 里。

但这里有个坑:service 方法粒度。我一开始 `checkIn` 里还顺手把"连续签到天数"的统计做了,后来发现运营后台也要展示这个统计,又拆了个 `getConsecutiveDays()` 出来。所以现在我会多问一句:这个逻辑有没有可能被别的入口调用?有就再往下拆一层,没有就暂时放着,但留个注释提醒自己。

还有个容易忽略的点——DTO 或者叫"参数对象"。PHP 没有强类型结构体,但我现在习惯把复杂入参封成数组并写清楚 `validate` 规则。不是炫技,是防止 controller 和 service 之间传着传着丢字段。之前一个 `createOrder` 方法,controller 传了 12 个字段,service 里用了 9 个,剩下 3 个"以为用但其实没用到"的,半年后发现是另一个接口的残留,清理的时候根本不敢动。

最后说个反直觉的:三层不是圣经。有个定时任务脚本,纯数据处理,没 HTTP 请求,我直接 service 调 model,省了 controller 这层。还有个小工具接口,就是查个配置表,我干脆让 controller 直接调 model 的作用域方法,service 空着反而别扭。分层是手段,不是目的,但"该有 layer 的时候不能偷懒"这个意识得先建立起来。

你们拆层的时候有没有遇到过"拆完更乱"的阶段?我大概经历了两次反复,现在才算摸到点门道。

评论0
回复 · 0
还没有回复
微信客服 微信客服