写了几年 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 吗?有什么区别?"

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

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

相关推荐
A黑桃2 小时前
Paimon Action Jar 实现机制分析
大数据·后端
@我们的天空2 小时前
【FastAPI 完整版】路由与请求参数详解(query、path、params、body、form 完整梳理)- 基于 FastAPI 完整版
后端·python·pycharm·fastapi·后端开发·路由与请求
武子康2 小时前
大数据-211 逻辑回归的 Scikit-Learn 实现:max_iter、分类方式与多元回归的优化方法
大数据·后端·机器学习
txinyu的博客2 小时前
结合游戏场景解析UDP可靠性问题
java·开发语言·c++·网络协议·游戏·udp
一路向北North2 小时前
springboot基础(85): validator验证器
java·spring boot·后端
蜗牛^^O^2 小时前
Spark详解
后端
前端不太难2 小时前
Flutter 状态复杂度,如何在架构层提前“刹车”
flutter·架构·状态模式
数说星榆1812 小时前
在线简单画泳道图工具 PC端无水印
大数据·论文阅读·人工智能·架构·流程图·论文笔记
1.14(java)2 小时前
掌握数据库约束:确保数据精准可靠
java·数据库·mysql·数据库约束