写了几年 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)

这些 String 和 Integer 是没有"防守能力"的。

  • 手机号格式对吗?

  • 邮箱是不是空的?

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

如果不封装,你就要在 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 吗?有什么区别?"

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

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

相关推荐
小码哥_常2 小时前
一套自己部署的 IM,客户端是同一份 Flutter
前端·后端
Decay__2 小时前
05_MySQL 常用 SQL 大全 —— 诊断、管理、优化辅助 SQL 与三大系统库速查
运维·数据库·sql·mysql·架构
代码山河4 小时前
Python就业方向全解析:后端、爬虫、数据分析、AI你选哪个?
python·学习·架构·教程·面向对象·项目
学长毕设5 小时前
基于SpringBoot的实验室资产管理系统
java·spring boot·后端
微订校园外卖平台6 小时前
校园外卖平台选模板还是定制:架构、数据流与验收清单
大数据·架构·外卖系统·本地生活·微订
zzu123zsw6 小时前
从臃肿的模板调用到声明式契约:深入 UniHttp 动态代理与多渠道生命周期扩展
java·spring boot·microsoft·unihttp
源码集结号7 小时前
云诊所门诊系统源码,基于SpringBoot+Vue.js架构,支持多租户设计与Docker容器化部署
java·spring boot·源码·医院信息系统·门诊系统·诊所系统
努力的小雨7 小时前
给 AI 眼镜开发应用有多难?我从零做了一个陪练,还上架了
后端
论文复现现场7 小时前
WRF、FEA 能用 L40S 加速吗?求解器检查、断点恢复与长期运行
java·数据库·算法·wrf·有限元分析·gpu计算·断点续算
ZhengEnCi7 小时前
用 AI 写代码为什么拼到最后还是拼人:算力瓶颈与领域专家提示词的本质差异
算法·架构