前言很多 Java 工程师都有一个隐秘的习惯 拿到需求第一反应不是“这个业务对象有什么行为”而是打开数据库客户端先把表建了。表建好了实体类用工具一键生成Lombok 加上 DataService 层再把 CRUD 一铺完事。这种开发模式爽不爽爽尤其是赶进度的时候。 但这种爽是透支未来的。几年下来你可能会发现自己陷入了一个怪圈明明用的是面向对象的语言写出来的代码却全是过程式的逻辑。写了多年代码之后都会有一个隐约的感觉“我好像在用 Java但又怎么用到‘面向对象’。”“不先设计数据库表我代码该怎么写”这个问题本身其实已经说明了一件事我们习惯的是“围绕数据写代码”而不是“围绕对象写代码”。这篇文章想聊的并不是什么高深的理论而是一个很现实的问题我们是不是在用一门面向对象的语言却一直在用过程式的方式做业务开发那个越写越厚的 Service就是罪证在典型的 Spring Boot 项目里我们最熟悉的“三板斧”是Controller → Service → Dao。一开始大家都相安无事。但随着业务迭代你有没有发现 Service 层开始变得畸形一个 OrderService 动辄两三千行。所有的业务规则状态判断、权限校验、数据计算都堆在这个类里。而对应的 Order 实体类除了那一堆 get / set 方法干净得像一张白纸。Martin Fowler 早在十好几年前就给这种现象起过名字叫**“贫血模型”Anemic Domain Model**。说白了我们把对象当成了**“数据垃圾桶”**而把灵魂全部抽离到了 Service 里。Service 像个操碎了心的保姆事无巨细地去掏对象里的数据算完再塞回去而对象本身像个没有智商的木偶。这不是面向对象这是披着 Java 外衣的面向过程。把行为还给对象让自己负责自己光说不练假把式。我们来看一个常见的场景修改订单收货地址。常见的“过程式”写法这段代码你一定很眼熟。它的问题不在于逻辑错误而在于逻辑的归属权错了。javapublic class OrderService {Transactionalpublic void updateAddress(Long orderId, String newAddress) {Order order orderMapper.selectById(orderId);// 痛点在这里Service 手伸得太长了// 它需要了解订单的所有内部状态细节if (order.getStatus().equals(OrderStatus.WAIT_PAY) ||order.getStatus().equals(OrderStatus.WAIT_DELIVER)) {order.setAddress(newAddress);order.setUpdateTime(LocalDateTime.now());orderMapper.updateById(order);} else {throw new BusinessException(当前状态不支持修改地址);}}}这种写法的隐患是 如果明天“取消订单”的逻辑里也要判断状态你是不是得把 if (status ...) 这一坨代码再复制粘贴一遍如果状态规则变了你得满世界找这些散落的 if。“面向对象”写法面向对象有一个核心原则Tell, Dont Ask要命令它不要询问它。别问对象“你是什么状态”然后你替它做决定而是直接告诉对象“我要改地址”让它自己判断能不能改。java// 这是一个有血有肉的领域对象不是单纯的数据库映射public class Order {// 状态和数据依然在对象内部private Integer status;private String address;private LocalDateTime updateTime;// 行为对象自己管理自己的状态流转public void changeAddress(String newAddress) {if (!canChangeAddress()) {throw new BusinessException(订单已锁定无法修改地址);}this.address newAddress;this.updateTime LocalDateTime.now();}// 规则什么是“可修改”的逻辑内聚在对象内部private boolean canChangeAddress() {return Objects.equals(status, OrderStatus.WAIT_PAY) ||Objects.equals(status, OrderStatus.WAIT_DELIVER);}}改造后的 Service 变得极其简洁javapublic class OrderService {public void updateAddress(Long orderId, String newAddress) {Order order orderRepository.findById(orderId);// Service 变得极度清爽只负责协调order.changeAddress(newAddress);orderRepository.save(order);}}你看Service 从“逻辑计算者”变成了“流程编排者”。代码的可读性瞬间提升了一个档次order.changeAddress(...)代码本身就是文档。这种变化看起来很小但带来的好处非常实际规则内聚修改逻辑只在一处不会散落可读性提升业务意图更加直白维护简单改需求时不用到处翻 Service别让 String 和 Integer 裸奔很多老系统的代码里充斥着这种“基础类型依赖症”Primitive Obsession。看看这个入参 public void register(String name, String phone, String email, Integer roleType)这些 String 和 Integer 是没有“防守能力”的。手机号格式对吗邮箱是不是空的角色类型是不是越界了如果不封装你就要在 Service 的开头写上十几行的 StringUtils.isBlank 和正则校验。一旦漏写一个脏数据就进数据库了。尝试用“值对象”Value Objectjavapublic class PhoneNumber {private final String number;public PhoneNumber(String number) {if (!isValid(number)) {throw new IllegalArgumentException(无效的手机号格式);}this.number number;}// ... getter logic}当你把入参改成 register(String name, PhoneNumber phone, ...) 时世界清静了。 你不需要再校验手机号格式因为只要能 new 出来的 PhoneNumber 对象一定是合法的。这才是强类型语言该有的安全感。认清现实一个“万能”对象往往什么都干不好很多系统的另一个痛点在于系统里有一个超级大的 User 类或者一个超级大的 Order 类。登录服务用它需要账号密码。交易服务用它需要收货地址。营销服务用它需要会员等级。最后这个类有了 100 多个字段谁都不敢动动一下不知道哪里会炸。这是因为我们把“数据库的表”等同于了“业务的对象”。在 DDD领域驱动设计里这叫“限界上下文”。说人话就是见人说人话见鬼说鬼话。在认证模块你应该设计一个 Account 对象只含 id, username, password。在物流模块你应该设计一个 Consignee收货人对象只含 id, address, phone。它们底层可能对应同一张 user 表但在代码层面请把它们拆开。不要为了省那几个类的定义让系统耦合得像一团乱麻。别幻想“一步到位”看到这可能有人会说“我的项目已经烂成这样了现在改得动吗”不要试图搞“大爆炸”式的重构。业务不会停时间也不允许。更现实的方式是新功能尝试充血模型把逻辑写进对象里。修 Bug 或优化如果这段逻辑刚好在 Service 里乱飞顺手把它收拢到实体类里。接受现实Service 层依然需要它负责事务控制、仓储调用、第三方服务编排。但请记住它不该负责业务状态的判断。这是一个习惯的改变而不是架构切换。写在最后很多工程师技术的瓶颈不是不懂高并发或微服务而是连最基本的代码分层和职责分配都没搞清楚。这种“面向对象”的思维转变一开始会很别扭。你可能会觉得“这不就是把代码从 Service 挪到了 Entity 吗有什么区别”相信我等你维护一个历经多年、多人经手过的系统时你会感谢这种“搬动”的价值。好的代码不是展现你用了多复杂的技巧而是让后来者在读代码时能清晰地看到业务的轮廓而不是一堆混乱的数据操作。原文链接https://juejin.cn/post/7594093947809300514