为什么你的Service越写越臃肿?三层架构的“业务逻辑层”是个黑盒

三层架构把所有"业务相关"的代码塞进一个叫Service的黑盒里。DDD把这个黑盒打开了。

你的Service层,是不是已经超过1000行了?

我问过几十个后端开发,回答"是"的超过80%。

更可怕的是,改一个业务规则,你发现要改三四个Service,改完心里还没底------不知道漏了哪里。上线之后果然出了Bug,积分规则在某一个角落没有被同步。

问题出在哪?

出在三层架构的那个"业务逻辑层"是一个黑盒。

它用三个字定义了一切,却从未告诉你:里面到底该怎么组织?四种不同性质的逻辑混在一起,凭什么不爆炸?

今天这篇文章,用一个真实的 "下单支付、扣余额、加积分" 的例子,带你把三层架构的Service解剖开,然后用DDD把它重写一遍。

看完你会明白:为什么有人能把Service写进坟墓,有人能写出清明上河图。

读完这篇文章,你将收获什么?

✅ 看清三层架构的Service里,到底混了哪四种 不同性质的逻辑

✅ 理解DDD为什么要拆出 "应用层"和"领域层" ,它们跟原来的Service有什么区别

✅ 用"下单支付扣余额加积分"的完整代码,搞清楚 "贫血模型"和"充血模型" 的本质区别

✅ 从包结构 一眼判断一个项目是真DDD还是挂羊头卖狗肉

✅ 一张表讲透 "原来的Service → 应用层 → 领域层" ,读完这张就够了

一、三层架构:一个被我们默认为"标准答案"的起点

先定标。经典三层架构长这样:

层级 职责 典型组件
表示层 接收请求、返回响应 Controller
业务逻辑层 处理业务规则、流程编排 Service
数据访问层 操作数据库 DAO / Mapper

依赖方向自上而下:Controller 依赖 Service,Service 依赖 DAO。

新人三天上手------一个请求进来,Controller 调 Service,Service 调 DAO,返回数据。这套模式统治了绝大多数企业级开发。

但三层架构在"业务逻辑层"这个方框里画了个问号。 它把所有说不清道不明的代码全部扔进Service,然后用一句话堵住你的嘴:"这是业务逻辑。"

事实是,Service里什么都有------有业务规则,有数据库操作,有缓存读写,有消息发送。它们交织在一起,像一团乱麻。谁也不敢动,谁也不敢改。

这就是三层架构的真相:它不是架构,它只是一个起跑线。

二、解剖一个真实的Service:它到底装着什么?

来看一段典型的"下单支付成功,扣钱包余额,加积分"的三层架构代码:

java 复制代码
// 文件: service/OrderService.java(三层架构)

@Service
public class OrderService {
    @Autowired private UserDao userDao;
    @Autowired private OrderDao orderDao;
    @Autowired private CreditLogDao creditLogDao;
    @Autowired private RedisTemplate redis;
    @Autowired private MqProducer mq;
    
    @Transactional
    public void payOrder(Long orderId, Long userId) {
        // 1. 技术动作:查数据
        Order order = orderDao.findById(orderId);
        User user = userDao.findById(userId);
        
        // 2. 业务规则:校验订单状态
        if (order.getStatus() != OrderStatus.UNPAID) {
            throw new BizException("订单已支付或已取消");
        }
        // 3. 业务规则:校验余额是否充足
        if (user.getBalance() < order.getTotalAmount()) {
            throw new BizException("余额不足");
        }
        
        // 4. 业务规则:扣余额
        user.setBalance(user.getBalance() - order.getTotalAmount());
        // 5. 技术动作:存数据库
        userDao.save(user);
        
        // 6. 业务规则:加积分(消费1元积1分)
        int points = order.getTotalAmount().intValue();
        user.setPoints(user.getPoints() + points);
        // 7. 技术动作:存数据库
        userDao.save(user);
        
        // 8. 业务规则:更新订单状态
        order.setStatus(OrderStatus.PAID);
        order.setPayTime(LocalDateTime.now());
        // 9. 技术动作:存数据库
        orderDao.save(order);
        
        // 10. 技术动作:记录积分流水
        creditLogDao.save(new CreditLog(userId, points, "订单支付"));
        
        // 11. 技术动作:更新缓存
        redis.opsForValue().set("user:" + userId, JSON.toJSONString(user));
        
        // 12. 技术动作:发送消息
        mq.send("order.paid", orderId);
    }
}

现在我问你:这个方法里,到底有几种不同性质的逻辑?

仔细数:

业务规则 (说"是什么"):第2行校验状态、第3行校验余额、第4行扣余额、第6行加积分、第8行更新状态

持久化逻辑 (说"存哪里"):第1行查数据、第5/7/9行存数据库、第10行记流水

技术辅助 (说"怎么更快/更联动"):第11行更新缓存、第12行发消息

流程编排(说"谁先谁后"):整个方法的执行顺序

四种不同性质的逻辑,在同一个方法里交错出现。

原来的Service不是因为"太大"而坏掉的,而是因为它同时承担了四种不同性质的职责,却没有在结构上把它们分开。

当业务规则变化时(积分配置改为黄金会员积2分),你去Service里找。

当技术方案变化时(Redis换Caffeine),你还是去Service里找。

Service变成了所有变化的交汇点------业务人员和技术人员的变更,都瞄准同一个文件。任何一个维度的变动,都会冲击其他维度。

如果你再看User这个类:

java 复制代码
// 文件: entity/User.java(三层架构)

@Entity
public class User {
    private Long id;
    private BigDecimal balance;
    private Integer points;
    // 只有 getter 和 setter,没有任何行为
}

User是个 "空心人" ------知道自己有余额和积分,但不知道自己能做什么。扣余额的逻辑在Service里,加积分的逻辑在Service里。一切行为都在Service里。

Martin Fowler管这叫 "贫血模型"

后果:如果AdminService也要给用户加积分(比如活动赠送),你只能把第6行那段复制粘贴过去。业务规则散布在各处,改漏一个就是Bug。

贫血模型 = 业务逻辑散落四处 → 低内聚

Service直接依赖DAO/Redis/MQ → 业务与技术绑定 → 高耦合

根本原因:业务逻辑层是一个未定义内部结构的黑盒

三、DDD的回应:不是"加一层",而是"拆一类"

DDD的第一刀就落在这里:把不同性质的逻辑拆开,各自归位。

三层架构 DDD四层架构 拆分的逻辑
表示层 用户接口层 对应,但更宽(HTTP/RPC/MQ)
业务逻辑层(Service) 应用层 接手"流程编排"+"技术协调"------查、存、事务、发消息
领域层 接手 全部业务规则------余额够不够、积分怎么算、状态对不对
数据访问层 基础设施层 接手"持久化实现"+"技术辅助"------实现仓储接口、Redis、MQ

关键规则

  • 应用层 只能做流程编排和技术协调,不允许出现任何业务规则 (不能有if (balance < amount)
  • 领域层 只能做业务规则,不允许出现任何技术代码 (不能有@Autowired@Transactional@Table

下面用"下单支付,扣余额加积分"逐层展示拆分后的样子。

四、拆分后的四层代码长什么样?

4.1 领域层:定义"业务规则是什么"(核心!)

所有业务规则的唯一居所。纯Java,没有Spring注解,不依赖任何技术组件。

java 复制代码
// 文件: domain/user/User.java

public class User {
    private Long id;
    private String name;
    private Money balance;        // 值对象:金额
    private Points points;        // 值对象:积分
    private UserLevel level;      // 枚举:普通/黄金/钻石
    
    public User(String name) {
        this.name = name;
        this.balance = Money.zero();
        this.points = Points.zero();
        this.level = UserLevel.NORMAL;
    }
    
    // 业务规则:扣余额(只有这一个入口)
    public void deductBalance(Money amount) {
        if (amount == null || amount.isNegative()) {
            throw new DomainException("扣减金额不能为空或负数");
        }
        if (this.balance.lessThan(amount)) {
            throw new DomainException("余额不足");
        }
        this.balance = this.balance.minus(amount);
    }
    
    // 业务规则:加积分(根据会员等级计算不同倍数)
    public void addPointsForConsumption(Money spentAmount) {
        if (spentAmount == null || spentAmount.isNegative()) {
            throw new DomainException("消费金额不能为空或负数");
        }
        int basePoints = spentAmount.getAmount().intValue(); // 1元=1积分
        int multiplier = this.level.getPointMultiplier();    // 普通:1, 黄金:2, 钻石:3
        this.points = this.points.add(basePoints * multiplier);
    }
}
java 复制代码
// 文件: domain/user/Money.java

public class Money {
    private BigDecimal value;
    
    public Money(BigDecimal value) {
        if (value == null || value.compareTo(BigDecimal.ZERO) < 0) {
            throw new DomainException("金额不能为负");
        }
        this.value = value;
    }
    
    public static Money zero() { return new Money(BigDecimal.ZERO); }
    public boolean isNegative() { return this.value.compareTo(BigDecimal.ZERO) < 0; }
    public boolean lessThan(Money other) { return this.value.compareTo(other.value) < 0; }
    public Money minus(Money other) { return new Money(this.value.subtract(other.value)); }
    public BigDecimal getAmount() { return this.value; }
}
java 复制代码
// 文件: domain/user/Points.java

public class Points {
    private int value;
    
    public Points(int value) {
        if (value < 0) throw new DomainException("积分不能为负");
        this.value = value;
    }
    
    public static Points zero() { return new Points(0); }
    public Points add(int amount) {
        if (amount < 0) throw new DomainException("增加积分必须为正");
        return new Points(this.value + amount);
    }
    public int getValue() { return value; }
}
java 复制代码
// 文件: domain/user/UserLevel.java

public enum UserLevel {
    NORMAL(1), GOLD(2), DIAMOND(3);
    private int pointMultiplier;
    UserLevel(int multiplier) { this.pointMultiplier = multiplier; }
    public int getPointMultiplier() { return pointMultiplier; }
}
java 复制代码
// 文件: domain/order/Order.java

public class Order {
    private Long id;
    private Long userId;
    private Money totalAmount;
    private OrderStatus status;
    private LocalDateTime payTime;
    
    public Order(Long userId, Money totalAmount) {
        this.userId = userId;
        this.totalAmount = totalAmount;
        this.status = OrderStatus.UNPAID;
    }
    
    // 业务规则:支付(状态校验 + 状态变更)
    public void pay() {
        if (this.status != OrderStatus.UNPAID) {
            throw new DomainException("订单已支付或已取消");
        }
        this.status = OrderStatus.PAID;
        this.payTime = LocalDateTime.now();
    }
    
    public Money getTotalAmount() { return totalAmount; }
}
java 复制代码
// 文件: domain/order/OrderStatus.java

public enum OrderStatus { UNPAID, PAID, CANCELLED }
java 复制代码
// 文件: domain/repositories/UserRepository.java(接口,不实现)

public interface UserRepository {
    User findById(Long id);
    void save(User user);
}
java 复制代码
// 文件: domain/repositories/OrderRepository.java

public interface OrderRepository {
    Order findById(Long id);
    void save(Order order);
}

领域层的三个特征

  • 没有任何@Autowired@Transactional@Table注解
  • 不知道数据库、Redis、MQ的存在
  • 只关心一件事------业务规则

4.2 应用层:负责"流程怎么跑"

接手流程编排和技术协调。不做任何业务判断。

java 复制代码
// 文件: application/service/PayOrderAppService.java

@Service
public class PayOrderAppService {
    // 依赖的是领域层定义的接口,不是具体的DAO!
    private final UserRepository userRepo;
    private final OrderRepository orderRepo;
    private final EventPublisher eventPublisher;
    
    public PayOrderAppService(UserRepository userRepo, 
                              OrderRepository orderRepo,
                              EventPublisher eventPublisher) {
        this.userRepo = userRepo;
        this.orderRepo = orderRepo;
        this.eventPublisher = eventPublisher;
    }
    
    @Transactional
    public void handle(Long orderId, Long userId) {
        // 1. 技术协调:获取领域对象
        Order order = orderRepo.findById(orderId);
        User user = userRepo.findById(userId);
        
        // 2. 【核心】调用领域方法------没有一行if/else业务判断!
        //    所有规则都在领域层内部执行
        order.pay();                               // 校验状态+改状态
        user.deductBalance(order.getTotalAmount());// 校验余额+扣钱
        user.addPointsForConsumption(order.getTotalAmount());// 计算积分+加分
        
        // 3. 技术协调:持久化
        orderRepo.save(order);
        userRepo.save(user);
        
        // 4. 技术协调:发布事件
        eventPublisher.publish(new OrderPaidEvent(orderId, userId));
    }
}

应用层的特征:你找不到if (balance < amount),找不到points = amount * level 它只做四件事:获取、调用、保存、发事件。

4.3 基础设施层:实现"技术怎么落地"

java 复制代码
// 文件: infrastructure/persistence/UserRepositoryImpl.java

@Repository
public class UserRepositoryImpl implements UserRepository {
    @Autowired private UserJpaMapper userMapper;
    
    @Override
    public User findById(Long id) {
        UserPo po = userMapper.selectById(id);
        return UserConverter.toDomain(po);
    }
    
    @Override
    public void save(User user) {
        UserPo po = UserConverter.toPo(user);
        userMapper.updateById(po);
    }
}
java 复制代码
// 文件: infrastructure/persistence/converter/UserConverter.java

public class UserConverter {
    public static User toDomain(UserPo po) {
        User user = new User(po.getName());
        user.setId(po.getId());
        user.setBalance(new Money(po.getBalance()));
        user.setPoints(new Points(po.getPoints()));
        user.setLevel(UserLevel.valueOf(po.getLevel()));
        return user;
    }
    
    public static UserPo toPo(User user) {
        UserPo po = new UserPo();
        po.setId(user.getId());
        po.setName(user.getName());
        po.setBalance(user.getBalance().getAmount());
        po.setPoints(user.getPoints().getValue());
        po.setLevel(user.getLevel().name());
        return po;
    }
}
java 复制代码
// 文件: infrastructure/message/EventPublisherImpl.java

@Component
public class EventPublisherImpl implements EventPublisher {
    @Autowired private MqProducer mqProducer;
    
    @Override
    public void publish(DomainEvent event) {
        if (event instanceof OrderPaidEvent) {
            mqProducer.send("order.paid", ((OrderPaidEvent) event).getOrderId());
        }
    }
}

4.4 用户接口层:接收请求

java 复制代码
// 文件: interfaces/web/OrderController.java

@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired private PayOrderAppService appService;
    
    @PostMapping("/pay")
    public Result pay(@RequestBody PayRequest req) {
        // 只做参数校验和路由
        appService.handle(req.getOrderId(), req.getUserId());
        return Result.success();
    }
}

五、一张表讲透:原来的Service vs 应用层 vs 领域层

如果你没时间读完全文,读完这张表就够了。

对比维度 原来的三层Service DDD的Application Service DDD的Domain Layer
文件位置 service/OrderService.java application/service/PayOrderAppService.java domain/user/User.java domain/order/Order.java
代码长啥样 if (balance < amount) user.setBalance(...) userDao.save(user) redis.set(...) mq.send(...) order.pay() user.deductBalance() user.addPoints() userRepo.save() eventPublisher.publish() if (balance < amount) balance = balance - amount points = amount * level.getMultiplier()
包含业务规则吗? ✅ 混在技术代码中 不包含 全部包含
包含技术协调吗? ✅ 混在业务代码中 ✅ 查、存、发事件 不包含
包含流程编排吗? ✅ 混在代码里 ✅ 显式按顺序调用 不包含
依赖什么? DAO、Redis、MQ Repository接口 谁都不依赖
单元测试 需启动Spring 需Mock接口 直接new对象

六、高内聚、低耦合:两把尺子量到底

三层架构 = 业务低内聚 + 技术高耦合

  • 低内聚 :扣余额、加积分的规则散布在OrderServiceAdminServiceActivityService里。改一处规则,多处修改,漏一个就Bug。
  • 高耦合:Service直接依赖DAO、Redis、MQ。换缓存方案,Service代码跟着改。业务被绑在技术上。

DDD = 业务高内聚 + 技术极低耦合

  • 高内聚 :User的所有行为在User.java里,Order的支付在Order.java里。改积分规则,只改这两个文件。
  • 极低耦合:领域层不知道数据库、缓存的存在。换技术方案,只换基础设施层,领域层毫不知情。

七、包结构:看一眼就知道是真DDD还是假DDD

三层架构(按技术角色分)

text 复制代码
com.company.project
├── controller/          # 表示层
├── service/             # 所有逻辑混在一起
├── dao/                 # 数据访问
└── entity/              # 贫血实体

DDD(先按业务领域分,再按层分)

text 复制代码
com.company.project
├── interfaces/
│   └── web/OrderController.java
├── application/
│   └── service/PayOrderAppService.java
├── domain/              # ← 核心!最大最活跃
│   ├── user/
│   │   ├── User.java        # 充血模型!
│   │   ├── Money.java
│   │   ├── Points.java
│   │   └── UserLevel.java
│   ├── order/
│   │   ├── Order.java       # 充血模型!
│   │   └── OrderStatus.java
│   └── repositories/
│       ├── UserRepository.java
│       └── OrderRepository.java
└── infrastructure/
    ├── persistence/
    │   ├── UserRepositoryImpl.java
    │   └── converter/
    └── message/

一眼判断

  • 顶级包是controller/service/dao/entity → 三层
  • 顶级包是interfaces/application/domain/infrastructure,且domain包最大 → DDD
  • domain里的类带@Table → 假DDD;纯Java带行为方法 → 真DDD

八、何时选择?三层不是被淘汰,而是被超越

场景 三层架构 DDD
CRUD、管理后台 ✅ 够用 ❌ 过度设计
金融、供应链、电商核心 ❌ 很快撑不住 ✅ 长期受益
业务稳定 ✅ 快速交付 ❌ 没必要
规则频繁变化 ❌ 改不动 ✅ 高内聚

渐进路径:

  1. 先把复杂逻辑从Service移到Entity(先充血)
  2. 引入Repository接口,实现依赖倒置
  3. 拆分Service为Application Service + Domain Service
  4. 按业务边界划分限界上下文

最后说一个可能得罪人的判断

三层架构没有死,它只是被用错了地方。

90%的CRUD系统、管理后台、内部工具,三层架构足够用。 硬上DDD是过度设计,是炫技。

但另外10%的系统------那些业务复杂、规则多变、长期演进的系统------如果不从三层架构向DDD演进,就是技术债的滚雪球。

区别在于:三层架构是你为"当前功能"付出的成本。DDD是你为"未来变化"付出的投资。

问题是:你的系统,还能活多久?

总结

三层架构有一个叫Service的黑盒 → 打开发现混了四种逻辑 → DDD拆成应用层(流程)和领域层(规则)→ 用充血模型把规则内聚到对象里 → 依赖倒置,领域层成为纯核心 → 业务高内聚,技术低耦合 → 这一切长在包结构里。

三层架构解决的是"怎么分层"的问题。

DDD回答的是更深一层的问题:业务逻辑层里面到底是什么,以及怎么组织它。

当你在代码里写下第一个充血模型时------比如把if (balance < amount)从Service挪到User.java里------你其实是在用代码说:

我知道这个业务规则属于谁,以及它应该待在哪里。

这才是架构从"能用"走向"好用"的那一步。

如果这篇文章对你有帮助,可以把它分享给那个正在为Service臃肿而头疼的同事。

欢迎在评论区留下你的观点:你的Service超过1000行了吗?你是选择重构还是继续堆代码?

相关推荐
趴下吧1 小时前
spring boot启动流程总结
java·spring boot·spring
何时梦醒1 小时前
Docker 容器化入门:从「我电脑能跑」到「哪台机器都能跑」
后端·docker·面试
颜进强1 小时前
11 - 从需求拆解到 OpenSpec:为什么不要直接敲 /opsx:explore
前端·后端·ai编程
foggyprojects1 小时前
AI 说销售额下降了,哪些客户拖累了结果?
后端
show4331 小时前
2026微信小程序批量处理视频文件架构方案:免费批量实测
微信小程序·小程序·架构
一拳不是超人1 小时前
Godot 信号不是线程安全的:我是怎么在后台线程里翻车的
前端·架构
用户852495071841 小时前
NestJS 架构实战:给后端代码请来一位“项目经理
后端
唐青枫1 小时前
一个点号省掉一堆类型:Zig .{} 语法、类型推导与实战
后端
月才1 小时前
告别System.out.println,打造专业日志系统
后端