当前位置: 首页 > news >正文

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

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

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

原文链接:https://juejin.cn/post/7594093947809300514

http://www.jsqmd.com/news/716425/

相关文章:

  • 如何提高SQL简单查询的数据一致性_使用事务快照读取
  • 避坑指南:NCCL多机多卡测试中,mpirun命令参数到底该怎么配?
  • 5分钟搞定:GHelper让你的华硕笔记本性能翻倍还更安静
  • 应对2026论文AIGC检测新规:DeepSeek高阶降AI指令与3款实测工具盘点
  • 2026年4月更新:PVC专用机深度选型指南,宁波华维机械有限公司展现技术硬实力 - 2026年企业推荐榜
  • ESP32 + micro-ROS实战:用Action Server控制RGB灯,并修复那些烦人的序列化bug
  • OpenClaw认知异化——从“知识容器”到“认知代理”的主体性危机(第二十一篇)
  • 为什么顶尖嵌入式团队已禁用非constexpr数学库?C++27 constexpr std::math全面落地后的5个不可逆架构升级点
  • 015、PCIE带宽计算:理论vs实际——调试手记
  • 保姆级教程:用KiCad/EAGLE从零画一块带eMMC的核心板(信号完整性与电源滤波全解析)
  • 超元力XR黑暗乘骑科技赋能:重构文旅游乐的创新表达
  • 2026Java 后端面试完整版|八股简答 + AI 大模型集成技术(最新趋势)
  • 从‘贝克尔境界’到高效团队管理:用倒U形曲线优化你的敏捷开发节奏
  • ABAP老司机经验谈:SUBMIT抓ALV数据,CL_SALV_BS_RUNTIME_INFO用对了是真香,用错了全是坑
  • 移动端安全防护
  • 如何在3分钟内掌握League Akari:告别繁琐操作,提升游戏效率
  • 2025届最火的十大AI写作工具解析与推荐
  • Hitboxer终极指南:4种模式彻底解决键盘输入冲突,游戏操作精准度提升300%
  • 3步搞定Windows安卓应用:告别模拟器的极简方案
  • LFM2.5-1.2B-Thinking-GGUF参数详解:max_tokens/temperature/top_p调优实战手册
  • LazyLLM框架解析:如何用“懒惰”哲学高效开发大语言模型应用
  • 别再只会复制粘贴了!用STM32F103C8T6和RC522,从零手撸一个门禁卡读写器(附完整源码)
  • [具身智能-498]:DeepSeek本地部署的成本
  • CZSC缠论分析插件:通达信终极量化交易解决方案完整指南
  • 改进YOLOv10:引入SIoU角度感知损失实现高精度旋转目标检测
  • 5.AI入门:从机器学习到生成式AI,普通人也能看懂(五)—— 深度学习入门
  • 【Unity拼图游戏模板】不卷3A大作,这类小游戏反而更容易变现
  • Yokogawa F3PU10-0N电源模块
  • 五月は花緑青の窓辺から
  • 百考通AI:让毕业答辩PPT,从“手忙脚乱”到“从容闪耀”