一个订单控制器里塞进了验证输入、校验库存、算折扣、写数据库、扣库存、发邮件、通知仓库、更新统计——整整80行代码,全部挤在一个方法中。

功能跑通了,需求也满足了。但只要换个入口,比如从后台导入CSV、给开放API调用,或者用定时任务批量生成订单,这段逻辑就得原封不动地复制粘贴过去。同样的代码存在于多个地方,一旦业务规则调整,改一处漏一处,后面排查起来就是噩梦。

Service Layer要解决的正是这个问题。它在架构里划出一个专门存放业务逻辑的层,把"创建订单"这样有明确业务含义的操作,封装到独立的Service中。这个Service不依赖任何具体的输入机制——不管请求是从HTTP过来的,还是命令行、消息队列,它接收的都是原始数据或DTO,返回的是结果对象或异常。

它的位置也很明确:控制器在最上层,负责接收请求、做基础验证,然后调用Service。Service在中间,包含全部业务规则,协调各个操作,需要数据的时候就去调Repository或Model。最底层是Repository/Model,只管存取数据、持久化改动。Service完全不接触HTTP的那一套,不碰Request对象,也不负责拼接Response。正因为与传输层彻底解绑,它才能在任何上下文中被复用。

以框架中的ArticleService为例,它把文章相关的逻辑集中起来:getPublished接收分页参数返回已发布文章的分页结果,getBySlug通过slug查单篇文章,getByTag按标签过滤,getRelated基于当前文章获取关联内容。BlogController调用articleService的getPublished方法,把结果交给视图渲染页面。ApiController调用的是同一个方法,只不过把返回数据转成JSON。命令行生成站点地图的sitemap:generate,同样调用这个方法拿到文章列表。业务逻辑只有一份,却支撑了三种完全不同的使用场景。

Service跟Helper、Utility的区别在于是否理解业务领域。Service是有领域知识的,它知道"创建订单"意味着要校验库存、要计算优惠、要触发后续通知流程。OrderService和PaymentService都属于这一类,通常是有状态的,通过依赖注入持有各种协作者。Helper则对业务一无所知,纯粹是通用的字符串、路径、日期处理函数,无状态的工具集。Utility比Helper稍微聚焦一些,通常是一个只完成单一目标的静态类,比如负责图片尺寸调整的ImageResizer、解析CSV文件的CsvParser。判断标准很简单:如果一个类需要知道"什么是订单"才能干活,那就是Service;如果它只需要知道"什么是字符串",那就是Helper。

引入Service Layer不需要在所有项目一上来就直接套用。有三个信号值得关注:第一,同一段逻辑出现在了多个控制器里,或者同时被Web界面、API和命令行调用;第二,某个控制器方法里的业务代码超过20行;第三,这一段逻辑需要在脱离HTTP环境的情况下单独进行单元测试。当三个信号中的任意一个出现时,把逻辑抽到Service里会比继续堆在控制器中划算得多。

反过来说,如果应用本身只做简单的CRUD,几乎没有跨复用的业务规则,控制器方法也就五六行,那么引入Service Layer反而会额外增加一层难以带来实际收益的间接调用。Service Layer解决的是业务复杂度和复用范围的问题,不是所有代码都必须经过这一层才算规范。