service 层膨胀成"万能垃圾桶"?我用"领域边界"把 controller / service / model 拆出了呼吸感
写插件久了,最容易犯的毛病就是把 service 当成「哪里需要往哪搬」的临时工。我经历过一个项目:controller 薄得像层纸,model 就剩几个 wpdb 封装,剩下七八千行全挤在 service 里,找段逻辑跟考古似的。后来逼着自己按「领域边界」重新切分,才把这团麻理清楚。
先说我的拆分原则,不是按技术层(查库/计算/输出),而是按业务域。比如一个会员积分插件,我拆出了 PointTransactionService、PointRuleEngine、UserPointProfileService 三个平级 service,各自对一块业务闭环负责。controller 只干一件事:把 HTTP 上下文翻译成领域语言,然后交给对的 service。
controller 的边界我卡得很死:
// 以前:controller 里直接算积分、查规则、写日志
// 现在:只负责"翻译"和"调度"
public function handle_redeem_request( WP_REST_Request $request ) {
$user_id = get_current_user_id();
$amount = absint( $request['amount'] );
// 参数校验交给 WP,业务校验下沉
try {
$result = $this->transaction_service->redeem( $user_id, $amount );
return new WP_REST_Response( $result->to_array(), 200 );
} catch ( InsufficientPointsException $e ) {
return new WP_REST_Response( ['error' => $e->getMessage()], 422 );
}
}
service 层我进一步分了两种形态:应用 service 管流程编排(先扣积分、再记日志、最后发通知),领域 service 管核心算法(比如积分过期策略的计算)。前者可以调多个后者,但后者绝不碰 $_POST、wp_mail 这些基础设施。
model 层我彻底抛弃了「一个表一个类」的贫血模型。现在 model 是带行为的:
class PointTransaction {
private int $user_id;
private int $amount;
private string $type;
public static function redeem( int $user_id, int $amount ): self {
if ( $amount insert( ... );
}
}
有个坑特别想说:service 之间能不能互相调用?我的经验是同层不调用,跨层单向流。应用 service 可以调领域 service 和 model,但两个应用 service 之间直接调,很快又会缠成毛线球。需要联动时,我改用领域事件解耦:
// TransactionService 完成扣减后抛事件
$this->event_dispatcher->dispatch( new PointsDeducted( $user_id, $amount ) );
// RuleEngineService 监听事件,异步处理等级变更
add_action( 'points_deducted', [ $this->rule_engine, 'reevaluate_user_tier' ], 10, 2 );
这样 controller / service / model 的依赖图变成单向树,测试时可以随便拔掉某根树枝 mock。之前那个七八千行的 service,拆完后最大的应用 service 也就四百行,领域 service 平均一百行出头。
最后贴个目录结构参考,和「一个 class-plugin.php 打天下」说拜拜:
src/ ├── Controller/ # HTTP 入口,只翻译不决策 ├── Service/ │ ├── Application/ # 流程编排,薄而宽 │ └── Domain/ # 核心算法,厚而窄 ├── Model/ # 带行为的领域对象 ├── Repository/ # 查询构建,model 的"仓库管理员" └── Infrastructure/ # WP 钩子、缓存、邮件等"脏活"
你们拆分 service 时有没有遇到过「拆太碎反而难找」的情况?我现在的底线是:一个 service 如果能用一句话说清楚它管什么,就说明边界对了;需要顿号列举的,继续拆。

