写了几年 Java,我发现很多人其实一直在用“高级 C 语言”写代码

前言

很多 Java 工程师都有一个隐秘的习惯: 拿到需求,第一反应不是"这个业务对象有什么行为",而是打开数据库客户端,先把表建了。

表建好了,实体类用工具一键生成(Lombok 加上 @Data),Service 层再把 CRUD 一铺,完事。

这种开发模式爽不爽?爽,尤其是赶进度的时候。 但这种爽是透支未来的。几年下来,你可能会发现自己陷入了一个怪圈: 明明用的是面向对象的语言,写出来的代码却全是过程式的逻辑。

写了多年代码之后,都会有一个隐约的感觉:

"我好像在用 Java,但又怎么用到'面向对象'。"

"不先设计数据库表,我代码该怎么写?"

这个问题本身,其实已经说明了一件事: 我们习惯的是"围绕数据写代码",而不是"围绕对象写代码"。

这篇文章想聊的,并不是什么高深的理论,而是一个很现实的问题: 我们是不是在用一门面向对象的语言,却一直在用过程式的方式做业务开发?

那个越写越厚的 Service,就是罪证

在典型的 Spring Boot 项目里,我们最熟悉的"三板斧"是:Controller → Service → Dao。

一开始大家都相安无事。但随着业务迭代,你有没有发现 Service 层开始变得畸形?

  • 一个 OrderService 动辄两三千行。
  • 所有的业务规则(状态判断、权限校验、数据计算)都堆在这个类里。
  • 而对应的 Order 实体类,除了那一堆 get / set 方法,干净得像一张白纸。

Martin Fowler 早在十好几年前就给这种现象起过名字,叫**"贫血模型"(Anemic Domain Model)**。

说白了,我们把对象当成了**"数据垃圾桶"**,而把灵魂全部抽离到了 Service 里。Service 像个操碎了心的保姆,事无巨细地去掏对象里的数据,算完再塞回去;而对象本身,像个没有智商的木偶。

这不是面向对象,这是披着 Java 外衣的面向过程

把行为还给对象,让自己负责自己

光说不练假把式。我们来看一个常见的场景:修改订单收货地址

常见的"过程式"写法

这段代码你一定很眼熟。它的问题不在于逻辑错误,而在于逻辑的归属权错了

java 复制代码
public class OrderService {

    @Transactional
    public 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, Don't 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 变得极其简洁:

java 复制代码
public 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)

这些 StringInteger 是没有"防守能力"的。

  • 手机号格式对吗?

  • 邮箱是不是空的?

  • 角色类型是不是越界了?

如果不封装,你就要在 Service 的开头写上十几行的 StringUtils.isBlank 和正则校验。一旦漏写一个,脏数据就进数据库了。

尝试用"值对象"(Value Object)

java 复制代码
public 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 表,但在代码层面,请把它们拆开。不要为了省那几个类的定义,让系统耦合得像一团乱麻。

别幻想"一步到位"

看到这,可能有人会说:"我的项目已经烂成这样了,现在改得动吗?"

不要试图搞"大爆炸"式的重构。 业务不会停,时间也不允许。

更现实的方式是:

  1. 新功能:尝试充血模型,把逻辑写进对象里。
  2. 修 Bug 或优化:如果这段逻辑刚好在 Service 里乱飞,顺手把它收拢到实体类里。
  3. 接受现实 :Service 层依然需要,它负责事务控制、仓储调用、第三方服务编排。但请记住,它不该负责业务状态的判断

这是一个习惯的改变,而不是架构切换。

写在最后

很多工程师技术的瓶颈,不是不懂高并发或微服务,而是连最基本的代码分层和职责分配都没搞清楚

这种"面向对象"的思维转变,一开始会很别扭。你可能会觉得:"这不就是把代码从 Service 挪到了 Entity 吗?有什么区别?"

相信我,等你维护一个历经多年、多人经手过的系统时,你会感谢这种"搬动"的价值。

好的代码,不是展现你用了多复杂的技巧,而是让后来者在读代码时,能清晰地看到业务的轮廓,而不是一堆混乱的数据操作。

相关推荐
涡能增压发动积20 小时前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
云烟成雨TD20 小时前
Spring AI Alibaba 1.x 系列【6】ReactAgent 同步执行 & 流式执行
java·人工智能·spring
Wenweno0o20 小时前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
行乾20 小时前
鸿蒙端 IMSDK 架构探索
架构·harmonyos
于慨20 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz20 小时前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg32132120 小时前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
tyung20 小时前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald20 小时前
SpringBoot - 自动配置原理
java·spring boot·后端
殷紫川20 小时前
深入理解 AQS:从架构到实现,解锁 Java 并发编程的核心密钥
java