一些个人碎碎念思考: 从什么是面向对象,发展历程,现代框架,到企业开发中的问题
声明:这篇文章没有特别明确的主旨。不是编程教学,不是技术分享,就是记录一些我平时的思考。可能像说梦话,想到哪说到哪。如果你追求的是"如何快速上手 Spring Boot",请现在直接按下Alt + F4。
任何抛开场景的优劣讨论都是没有意义的,都是耍流氓,本篇探讨的也只是个人实践中,在一些特定场景下遇到的问题,从而引发的思考和吐槽,不是为了反驳什么,并没有完全否认现在任何主流方案。而是当我意识到自己总是下意识默认了一些东西应当是这样的时候,才反应过来自己忘记了思考,
开篇:从别人告诉我的东西开始
学习路上,总是有人在告诉你"是什么":
goto是有害的,不要用- OOP 有三大特性:封装、继承、多态
- 继承是为了代码复用
- 组合优于继承
- 注解是优雅的,Spring 很优雅
- 声明式比命令式高级
然后大部分人会问:是不是?
很少有人会问,或深入思考的问:为什么?
老师说 goto 是有害的,就记住了:goto 是不好的,不要用,看到别人用就要站出来职责对方连基本功都没掌握。
但是如果去阅读 Linux 内核代码, 里充斥着大量的 goto。
刚学 Spring 的时候,觉得注解太优雅了。
哇噻哇噻
加一个 @Transactional,事务就有了。
加一个 @Cacheable,缓存就有了。
后来写了企业级开发,看到别人一个方法上加十几个注解:
@DataScope(deptId = "#deptId")
@DataFilter(roles = {"ADMIN", "USER"})
@RateLimit(key = "#id", requests = 10, window = 60)
@OperationLog(module = "user", action = "get")
@Cacheable(value = "user", key = "#id")
@Transactional
@Async
public User getUser(Long id) { ... }
我开始问:如果注解真的这么优雅,为什么我看到这一坨东西浑身难受
学 OOP 的时候,书上说:继承是为了代码复用。
然后看到企业级代码里:
class Order extends BaseEntity {}
class User extends BaseEntity {}
我开始问:Order 是一个 BaseEntity 吗?BaseEntity 是什么东西?
到底在复用什么。。好恶心
这篇文章就是我对这些问题的思考。
不是教科书式的答案,而是基于实践和个人观察的理解。
一、命令式 vs 声明式
1.1 传统定义
你应该见过这样的表格:
| 命令式 | 声明式 |
|---|---|
| 描述"怎么做" | 描述"是什么" |
| 流程控制 | 结果描述 |
| for 循环 | SQL |
| 一步步指令 | 配置 |
然后有人告诉你:声明式好,因为它隐藏了实现细节,程序员可以专注的关心业务逻辑。(就和最初学 Spring 的时候,加一个注解,功能就有了,哇噻哇噻太优雅了)
1.2 框架的声明式本质
框架的设计哲学是什么?
封装所有通用的东西,对外暴露所有不属于"通用职责"的变更点。
大部分情况下,这个"变更点"就是业务逻辑。
所以框架的本质是:让你用声明式的方式写业务。
// Spring 的声明式
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.getUser(id);
}
}
这段代码在"声明"什么?
- 声明这是一个 REST 控制器
- 声明路由是
/users/{id} - 声明 GET 请求会调用这个方法
但真正的业务逻辑在哪?在 userService.getUser(id) 里。而 userService 背后可能又是:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private EventPublisher eventPublisher;
@Override
@Transactional
public UserDTO getUser(Long id) {
// 真正的业务逻辑
}
}
然后 @Transactional 背后又有事务切面,@Autowired 背后又有依赖注入容器……
框架的声明式,最后变成了"配置驱动开发"。
总是想着灵活,配置,恨不得写一次代码之后,让产品和运营自己去改配置,最后发现还是要程序员来改。
于是有了各种DSL,甚至是在配置文件里面发明了自己的语法。写错了根本没有任何报错提示。
比如我见过的 (Spigot插件-MythicMobs的技能配置文件,我没否认这个做法是错误的,毕竟是通用插件框架。):
``
1.3 用命令式来声明
近几年我写代码,越来越倾向于用命令式的方式写声明式代码。
什么意思?
或者说:我的代码风格,更像是 Builder 模式的思想。
Builder 模式是什么?一步一步构建最终对象。每一步都是显式的,每一步都有迹可循。最终产物是一个完整的、逻辑闭环的对象。
// Builder 模式:显式、分步、可追溯, 每个参数都有名字,意图清晰
User user = User.builder()
.name("张三")
.age(18)
.email("zhangsan@example.com")
.activated(true)
.subscribed(false)
.build();
我用命令式写声明式,也是这个道理。
用 Builder 模式来改写缓存的例子:
// 纯声明式:逻辑链路不直观,依赖 AOP
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public User getUser(Long id) {
return userMapper.selectById(id);
}
// 用 Builder 模式显式构建缓存逻辑
public User getUser(Long id) {
return Cache.builder(User.class)
.key("user:" + id)
.loader(() -> userMapper.selectById(id))
.onHit(cache -> log.debug("cache hit"))
.onMiss(cache -> log.debug("cache miss"))
.expire(10, TimeUnit.MINUTES)
.build()
.get();
}
或者更通用的方式:
// 显式构建缓存配置、参数、回调
public User getUser(Long id) {
return Caching.get("user:" + id,
CacheConfig.<User>builder()
.loader(() -> userMapper.selectById(id))
.onHit(log::debug)
.onMiss(log::debug)
.expire(10, TimeUnit.MINUTES)
.build());
}
第一段代码有什么问题?
- 你看不出缓存逻辑在哪
- 你看不出缓存什么时候失效
- 你看不出缓存失败了会怎样
- 出了 bug,堆栈里有 20 个代理类
第二段代码呢?
- 配置、参数、回调都是显式传递
- 逻辑链路清晰
- 缓存策略一目了然
- 异常情况可以显式处理
- 显式优于隐式
有人说:但第一段代码更简洁啊!
我的回答是:简洁不等于清晰。简洁是给写代码的人看的,清晰是给读代码的人看的。而代码被读的次数,永远比被写的次数多。
1.4 AOP 的滥用
先声明一下:AOP 本身不是坏东西。
日志、权限、事务,这些横切关注点确实适合用 AOP 处理。
缓存也是。 我上面用 Builder 模式对比的例子,其实有点极端。大部分情况下,缓存用 @Cacheable 是合理的,没必要每个项目都搞一套 Builder 封装。
但问题是:有些人用 AOP 是为了"炫技",不是为了"解决问题"。
我见过一个方法上面加十几个注解的情况(虽然我找不到原代码了,但大概是这样):
// 这不是真实代码,但差不多是这个感觉
@DataScope(deptId = "#deptId")
@DataFilter(roles = {"ADMIN", "USER"})
@RateLimit(key = "#id", requests = 10, window = 60)
@OperationLog(module = "user", action = "get")
@Cacheable(value = "user", key = "#id")
@Transactional
@Async
@Scheduled(cron = "0 0/5 * * * ?")
@Retry(times = 3)
@CircuitBreaker
@SentinelResource
public User getUser(Long id) { ... }
这段代码的问题:
- 你看不出逻辑链路在哪
- 你看不出各个注解的执行顺序
- 出了 bug 不知道在哪,因为堆栈里有 20 个代理类
- 你想调试,但断点永远进不去,因为代码是动态生成的
这不是高级感,这在埋雷.. :)
我的观点很明确:能用显式代码表达的,就不要用隐式注解。注解应该是"辅助说明",不应该是"逻辑主体"。
二、OOP 的本质:从历史发展看设计哲学
2.1 历史发展脉络
要理解 OOP,得先理解它从哪来。
汇编 → 结构化编程 → OOP → 多范式融合
汇编语言的问题是什么?
- 非结构化
- 本质是地址跳转
- 维护性极差
你写一段汇编,三个月后你自己都看不懂。为什么?因为 jmp 0x4f3a 不会告诉你它为什么要跳,也不会告诉你跳过去之后会发生什么。
结构化编程解决了这个问题。
它抽象出了三个基本结构:
- 顺序:一行一行执行
- 分支:if/else
- 循环:for/while
这三个结构,足以表达任何算法。但关键是:它们是人类能理解的。
但这里有个问题:结构化编程破坏了跳转的自由性。
goto 就是受害者。
答案是:goto 本身没有问题,有问题的是没有规范的滥用。
Linux 内核里的 goto 有严格的规范:
- 只用于错误处理
- 只用于资源清理
- 只能向前跳转
- 有统一的标签命名(
out、err、cleanup)
在有明确目的和严格规范的时候,goto 是更优的选择。
这再次证明了我的观点:抛开场景谈好坏,都是耍流氓。
C 语言就是这个理念的最佳实践。
2.2 C 语言的位置
我说一句话,可能很多人不同意:
无论技术如何演进,总会有一个语言占据 C 现在的位置。
C 语言是什么位置?
底层硬件与上层抽象的最小平衡点。
- 再往下,是汇编,是寄存器,是内存地址
- 再往上,是对象,是垃圾回收,是运行时
C 语言刚好卡在中间:它给你指针,让你能操作内存;它给你结构体,让你能组织数据;它给你函数,让你能封装逻辑。
但也就到此为止了。C 语言没有"对象",没有"继承",没有"多态"。它相信程序员知道自己的行为,相信你不会随便*(int*)0。
这个位置是生态位,不是技术选择。 就像草原上总会有食草动物的位置,也会有食肉动物的位置。
2.3 OOP 的产生原因
那 OOP 为什么会出现?
教科书上说是为了"代码复用",为了"模块化"。
我认为这些答案都太浅了。
OOP 的本质是:工程上为了构造更复杂的系统。
注意这里的"复杂"是什么意思。
操作系统内核复不复杂?复杂。但它是一种"纯粹"的复杂——它关注的是进程调度、内存管理、文件系统。这些都是技术问题。
企业级系统复不复杂?也复杂。但它是"多样性"的复杂——它关注的是用户、订单、支付、库存、物流、营销。这些都是业务问题。
OOP 选择"对象"作为基本单元,是因为它更符合人类认知世界的方式。
现实世界里有什么?有"东西"。
- 一个人是一个东西
- 一辆车是一个东西
- 一笔订单是一个东西
这些东西有自己的属性(颜色、名字、状态),也有自己的行为(跑、飞、支付)。
OOP 就是把这种认知方式映射到代码里。
所以 OOP 不是"更好的结构化编程",它是另一个层面的东西。结构化编程解决的是"如何组织指令",OOP 解决的是"如何描述世界"。
三、封装、继承、多态
教科书上永远在讲"OOP 三大特性:封装、继承、多态"。
但这是"是什么",不是"为什么"。
我要从第一性原理出发,解释这三个概念。
3.1 封装:物自体的不可知性
先说封装。
教科书说:封装是把数据和操作数据的方法包装在一起,对外隐藏实现细节。
这个解释没错,但太浅了。
封装的本质是:属性本质上是不可探测的。
想象一个物体放在你面前。没有任何探测手段,没有任何仪器,你怎么知道它内部有什么结构?
你做不到。你唯一能做的,是和它交互:推它一下,看它动不动;敲它一下,听它响不响。
你只能通过行为推测内部结构,而不能直接获取内部状态。
这是哲学里的"物自体"概念。康德说:事物本身(物自体)是不可知的,我们只能认识现象(事物对我们的显现)。
对象就是物自体。
public class User {
private String name; // 物自体的内部状态
public String getDisplayName() { // 现象:对象对我们的显现
return this.name.toUpperCase();
}
}
name 是私有的,不是因为它"需要被保护",而是因为它本来就是不可探测的。getDisplayName() 是公有的,因为这是对象对我们的"显现"。
任何能被我们察觉到的,都是对外表现的行为,而不是直接是属性。
我们日常的与万物的交互,都是通过一个对象的行为来推测的,而不是直接能得到其"属性"。
反射的破坏性
这里可以引出反射的问题。
反射是什么?反射是上帝视角工具。
Field field = User.class.getDeclaredField("name");
field.setAccessible(true);
String name = (String) field.get(user);
这段代码做了什么?它绕过了对象的"现象",直接获取了"物自体"的内部状态。
这对 OOP 有破坏性。
注意:我说"破坏性"不是贬义。反射有很多合法用途:序列化、ORM、依赖注入。但它确实破坏了 OOP 的逻辑闭环。
为什么?
因为在 OOP 的哲学里,对象的状态应该是不可直接访问的。反射打破了这个规则,让逻辑在 OOP 层面无法闭环。
有人说:但反射套件本身也是对象啊,Field、Method 都是对象。
没错,这是语言语法层面的闭环,但不是 OOP 行为本身的闭环。就像一个游戏里,角色不能穿墙,但玩家可以按 F5 重置位置——这在游戏规则之外,但游戏引擎层面是合法的。
3.2 继承:is-a 关系的真相
继承是什么?
见过最多的说法是:继承是为了代码复用。
我认为这个解释误导了无数人。
继承的本质是:泛化的手段,符合世界规则的直觉方式。
强调三遍的原则:
继承必须是 is-a 关系!
继承必须是 is-a 关系!
继承必须是 is-a 关系!
怎么判断是不是 is-a 关系?很简单:把你写的类用 A is a B 读出来,判断这句话对不对。
// 正确的继承
class Dog extends Animal {} // Dog is a Animal ✓
// 错误的继承(企业级常见病)
class UserServiceImpl implements UserService {} // UserServiceImpl is a UserService ???
class OrderController extends BaseController {} // OrderController is a BaseController ???
第二段代码,你读出来看看对不对?
UserServiceImpl 是一个 UserService 吗?不,它是 UserService 的一个实现。
OrderController 是一个 BaseController 吗?不,它只是复用了 BaseController 的代码。
这不是 is-a 关系,这是 has-code 关系。
继承 vs 组合
然后说到"组合优于继承"。
这句话本身没错,但它被误解了。
"组合优于继承"的前提是:当你没有 is-a 关系的时候。
如果你有 is-a 关系,继承是自然的选择。Dog 和 Animal 是 is-a 关系,不用继承用什么?
但如果你只是为了复用代码,而没有 is-a 关系,那就用组合。
// 错误:为了复用而继承
class Stack extends ArrayList {} // Stack is a ArrayList ???
// 正确:用组合
class Stack {
private List<Integer> list = new ArrayList<>();
public void push(int x) { list.add(x); }
public int pop() { ... }
}
Stack 是一个 ArrayList 吗?不,它内部用了一个 ArrayList。
所以"组合优于继承"的真正意思是:不要为了复用而破坏 is-a 关系。
我见过的一些让我想吐槽的实践:
// 两个 Bean 做继承,就因为它们都有 id 和 createTime
class Order extends BaseEntity {}
class User extends BaseEntity {}
Order 是一个 BaseEntity 吗?BaseEntity 是什么?是一个哲学概念吗?
我不知道"组合优于继承"这句话是什么情况下得出来的,但它肯定不是在 JavaEE 的 Bean 继承场景下得出来的。
3.3 多态:对象自己决定如何响应
多态是什么?
教科书说:多态是同一接口,多种实现。
这个解释没错,但可以更深入。
多态的本质是:让对象自己决定如何响应,而不是让调用者决定对象是什么。
Animal a = new Dog();
a.speak(); // 对象自己决定如何响应
这段代码里,调用者(a.speak())不需要知道 a 是 Dog 还是 Cat。调用者只需要知道 a 是一个 Animal,而 Animal 有 speak() 方法。
至于具体怎么 speak,那是对象自己的事。
多态是封装的延伸。
既然我们只能通过行为认识对象,那么不同对象对同一消息有不同响应是自然的。
多态也是泛化的执行层面体现。
继承建立了类型层次(Dog 是 Animal 的子类),多态让这个层次在运行时工作(a.speak() 会根据实际类型调用正确的方法)。
基于继承的多态 vs 基于接口的多态
多态有两种形式:
// 基于继承的多态
Animal a = new Dog();
a.speak();
// 基于接口的多态
Flyable f = new Bird();
f.fly();
第一种是类型驱动的多态:Dog 是 Animal 的子类,所以可以统一处理。
第二种是能力驱动的多态:Bird 实现了 Flyable 接口,所以可以统一处理。
第二种更接近多态的本质。
因为现实世界里,我们更多是根据"能做什么"来分类,而不是根据"是什么"来分类。
- 会飞的东西:鸟、飞机、超人
- 能吃的东西:苹果、面包、饭
- 可以取消的东西:订单、订阅、任务
这些分类跨越了类型边界,但共享同一个"能力"。
这也是为什么现代语言(如 Go)用接口 + 结构体也能实现 OOP 的核心思想,而不需要类的继承体系。
四、接口 vs 类:能力与身份的区别
4.1 第一性原理的解释
为什么有了继承还要有接口?为什么接口可以多实现?
答案藏在实践里。
大部分接口的后缀都喜欢用 able!
interface Flyable { void fly(); }
interface Cancellable { void cancel(); }
interface Observable { void observe(); }
able 是什么?是"能够"。
接口表达的就是能力!
| 类(继承) | 接口(实现) |
|---|---|
| 表达"是什么"(is-a) | 表达"能做什么"(can-do) |
命名是名词:User, Order | 命名是形容词:Flyable, Cancellable |
| 单继承(一个东西只能有一个本质身份) | 多实现(一个东西可以有多种能力) |
为什么继承是单继承?因为现实世界中一个东西只能有一个本质身份。
为什么接口是多实现?因为一个东西可以有多种能力。
class Bird extends Animal implements Flyable, Singable, NestBuildingable {}
一只鸟是一个动物(本质身份),但它能飞、能唱、能筑巢(多种能力)。
4.2 接口命名的真相
好的命名会告诉你设计的本质。
- 如果接口名叫
Xxxable,它在描述能力 - 如果接口名叫
XxxService,它在描述角色 - 如果类名叫
Xxx,它在描述实体
当命名和设计目的一致时,代码是自然流畅的;当不一致时,代码就开始拧巴了。
比如:
// 拧巴的代码
interface UserService { ... }
class UserServiceImpl implements UserService {}
谁会写第二个 UserService 的实现?UserServiceImplV2?AdminUserService?
不会的。业务接口天生就是单一的,不需要多态。
所以企业级开发里面,所有的业务代码都定义一个接口,其实很傻逼(抛开框架需要的话,但是很多人学的时候被告知的是方便扩展,这是规范)。
本身业务就是天天变的,没人会去想着再实现一个你那 B 业务接口。
但类似短信平台、数据源等这种,是推荐用接口和多实现的:
// 合理的接口设计
interface SmsProvider {
void send(String phone, String message);
}
class AliyunSms implements SmsProvider {}
class TencentSms implements SmsProvider {}
这个接口有明确的能力边界,而且确实有多个实现的需求。
4.3 自然的设计演进
理解了这一点,编码就会自然而然:
// 先定义能力接口
interface Flyable { void fly(); }
interface PoisonAttack { void poisonAttack(); }
// 再定义基础类
abstract class Enemy {
protected int hp;
protected int damage;
}
// 然后实现具体的敌人
class FlyingPoisonEnemy extends Enemy implements Flyable, PoisonAttack {
public void fly() { ... }
public void poisonAttack() { ... }
}
这个设计是从第一性原理推出来的,不是从"设计模式"背出来的。
五、现状:组件系统与传统 OOP 的演进
5.1 传统 OOP 的类爆炸问题
游戏开发里有个经典问题。
假设你做一个游戏,敌人有不同的能力组合:
class FlyingEnemy extends Enemy implements Flyable {}
class PoisonEnemy extends Enemy implements PoisonAttack {}
class FlyingPoisonEnemy extends Enemy implements Flyable, PoisonAttack {}
class FlyingPoisonSummonEnemy extends Enemy implements Flyable, PoisonAttack, Summoner {}
class FlyingExplodingEnemy extends Enemy implements Flyable, Explodable {}
// ... 继续排列组合
每加一个能力组合,就要一个新类。
这不是工程问题,这是数学问题:n 个能力,就有 2^n 种组合。
5.2 组件系统的解决方案
组件系统怎么解决?
// 组件系统:运行时组合
Entity entity = new Entity();
entity.addComponent(new FlyComponent());
entity.addComponent(new PoisonAttackComponent());
// 动态变化
entity.removeComponent(FlyComponent.class); // 不能飞了
entity.addComponent(new ExplodableComponent()); // 可以爆炸了
能力变成了运行时状态,而不是编译时类型。
| 维度 | 传统 OOP | 组件系统 |
|---|---|---|
| 本质定义 | 类定义"是什么"(静态身份) | 组件集合定义"当前有什么能力"(动态状态) |
| 变化粒度 | 类型级别的变化需要新类 | 实例级别的变化只需添加/移除组件 |
| 组合时机 | 编译时组合 | 运行时组合 |
5.3 为什么组件系统还是 OOP
组件系统(如 ECS)乍一看好像是"反 OOP"的。
我认为恰恰相反:组件系统是更纯粹的 OOP。
为什么?
设计者在更高的层面上,找到了一个更优的抽象逻辑。
因为现实世界中,一个东西的"能力"本身就是动态的。
- 你穿上泳衣,就"能游泳了"
- 你脱下泳衣,就"不能游泳了"
- 你学会开车,就"能开车了"
- 你喝醉了,就"不能开车了"
传统 OOP 用类定义"是什么",无法表达这种动态变化。你的类定义好了,这个对象的"身份"就固定了。
组件系统用"有什么组件"定义当前状态,更符合现实。
组件系统不是抛弃了 OOP,而是把封装的粒度从"类"变成了"组件",把组合的时机从"编译时"移到了"运行时"。
这是一种更纯粹的 OOP。
5.4 设计模式的应对方案
在传统 OOP 中,怎么应对类爆炸问题?
答案是:设计模式。
| 模式 | 解决思路 | 与组件系统的关系 |
|---|---|---|
| 策略模式 | 把算法抽象成接口,运行时切换 | 类似组件,但粒度更细 |
| 装饰器模式 | 动态添加职责 | 几乎就是组件系统的思想 |
| 组合模式 | 用树形结构表示部分 - 整体 | 组件系统的理论基础之一 |
设计模式是"补丁",组件系统是"原生支持"。
设计模式告诉你:"你可以用这个模式解决类爆炸问题"。
组件系统告诉你:"这个语言原生支持组件,你不需要设计模式"。
但这有个前提:你的语言支持组件系统。Java 不支持吗?支持,但需要你自己写框架。C++ 不支持吗?支持,但需要你自己实现 ECS。
语言的选择,决定了你解决问题的成本。
5.5 解耦不全是好的,耦合不全是坏的
经常听到一句话:"高内聚,低耦合"。
这句话本身没错,但它被误解了。
耦合不是敌人,糟糕的耦合才是。
游戏开发里的类耦合是什么样的?
// 游戏里的对象,耦合是通畅的
class Player {
private Weapon weapon;
private Armor armor;
void attack(Enemy target) {
int damage = this.weapon.getDamage() + this.getStrengthBonus();
target.takeDamage(damage);
}
}
这段代码耦合了吗?耦合了。
但这种耦合是合理的、顺畅的、符合直觉的:
- 玩家有武器,很合理
- 攻击敌人时计算伤害,很自然
- 对象之间互相调用,很通畅
企业级开发里的解耦是什么样的?
// 企业级开发,为了耦合而解耦
interface IUserService {
UserDTO getUserById(Long id);
}
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private IUserMapper userMapper;
@Override
public UserDTO getUserById(Long id) {
return userMapper.selectById(id);
}
}
这段代码解耦了吗?解耦了。
但这种解耦是必要的吗:
- 一个永远不会有第二个实现的接口
- 一层又一层的抽象
- 为了"可扩展性"牺牲了可读性
企业级开发追求解耦,游戏开发接受耦合。
为什么?
因为企业级开发的耦合,往往是人为制造的耦合:
- 两个 Bean 都有 id,所以要继承 BaseEntity
- 为了"方便测试",所以要注入接口
- 为了"以后可能扩展",所以要加分层
而游戏开发的耦合,是业务本身的耦合:
- 玩家本来就有武器
- 攻击本来就要计算伤害
- 对象之间本来就要交互
解耦的目的是降低复杂度,不是为了耦合而解耦。
如果一个耦合是符合直觉的、是业务本质的,那它就不需要被解掉。
六、Java 框架吐槽大会
好了,下面进入吐槽环节。以下内容可能引起部分 Javaer不适,请酌情阅读。
6.1 过度抽象
想知道一个方法最终调用了什么?按住 Ctrl 点进去,再点进去,再点进去……点到第七层的时候,你已经忘了自己最初要找什么。
Java 企业级开发的分层:
Controller → Service → Manager → DAO → Entity
↓ ↓ ↓ ↓
DTO BO PO DO
↓ ↓ ↓ ↓
Converter Mapper Assembler ...
一个字段从数据库到前端,旅行了一整个微服务架构。
我不是说分层不好。分层是好的,但过度分层就是折磨人了。
我见过最离谱的项目:
UserController调用UserServiceUserService调用UserManagerUserManager调用UserRemoteServiceUserRemoteService调用UserFeignClientUserFeignClient调用远程服务- 远程服务返回
UserRpcDTO - 转成
UserDTO - 转成
UserVO - 转成
UserResponse
一个 selectById 能写出九曲十八弯的感觉。
6.2 配置大于代码
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
@Cacheable(value = "user", key = "#id", unless = "#result == null", sync = true)
@Async("userExecutor")
@Scheduled(cron = "0 0/5 * * * ?")
@EnableSomethingImportant
public class MyService {
// 真正的代码?谁在乎呢
}
注解比代码多,配置比逻辑复杂。
出了问题?查文档吧。注解的背后是三千行自动配置,是二十个条件装配,是五十个 Bean 的创建顺序。
"约定优于配置"的 Spring Boot,最后变成了"配置优于代码"。
6.3 框架职责过度
框架:我已经帮你设计好了一切,你只需要填空。
我:那我能自己设计吗?
框架:不能,填就完了。
Java 框架的经典套路:
- 继承
AbstractXxxService - 实现
XxxAware - 注入
XxxTemplate - 重写
doSomething()方法 - 在
@PostConstruct里初始化 - 在
@PreDestroy里清理
框架帮你决定了包名、类名、方法名、甚至变量名。
你不是在写代码,你是在完成框架的服从性测试。。。
6.4 AOP 过度切入:逻辑在哪?
日志、权限、事务、缓存、监控……甚至是莫名其妙的功能都用 AOP 切进去,然后祈祷它能正常工作。
AOP 的问题是什么?
- 代码里看不到逻辑,因为逻辑在切面里
- 出了 bug 不知道在哪,因为堆栈里有 20 个代理类
- 你想调试,但断点永远进不去,因为代码是动态生成的
显式优于隐式?不,隐式才是高级感? 也不是。
6.5 DDD 也是屎:换皮不换骨
DDD:领域驱动设计
实际:领域贫血,服务驱动,数据库驱动
DDD 的理念是好的:把业务逻辑放在领域对象里,而不是放在 Service 里。也就是所谓的"充血模型"。
但实际开发中是什么情况?
问题一:贫血模型
// 说好的领域对象呢?
public class Order {
private Long id;
private Long userId;
private BigDecimal amount;
// getter and setter
}
// 逻辑全在 Service 里
public class OrderService {
public void createOrder(Order order) {
// 校验用户
// 校验库存
// 计算价格
// 创建订单
// 发送事件
}
}
说是 DDD,实际上 Entity 只有 getter/setter。逻辑全在 Service 里,Domain 成了数据容器。
问题二:就算你充血了,也还是屎
退一步说,就算你真的把逻辑放进 Entity 里:
public class Order {
public void calculatePrice() { ... }
public void validate() { ... }
}
但 CRUD 的本质就是数据流转啊。你给 Order 加了十个方法,最后它还是要变成 DTO、变成 VO、变成 JSON。
DDD 改变不了 CRUD 是屎的事实。
问题三:过度设计
一个简单 CRUD,搞出:
- Entity(数据库映射)
- DTO(数据传输)
- VO(视图对象)
- BO(业务对象)
- DO(领域对象)
然后还有一个完整的分层:
- Controller 层
- Service 层
- Manager 层
- Domain 层
- Repository 层
累不累啊?
问题四:术语滥用
什么业务都往 DDD 上靠:
- 用户管理 = 聚合根?
- 增删改查 = 领域事件?
- 数据库表 = 实体?
话说回来:
CRUD 就是用 DDD 写也是 CRUD,屎的味道不会变。
你换个包装,屎还是屎。
甚至有时候,贫血模型 + 规范分层拉出来的屎,至少形状统一,看起来还规整一点。
6.6 编程乐趣的丧失
以前的编程:设计算法、优化结构、挑战难题
现在的编程:
@Autowired、return xxxMapper.selectById(id)
业务开发成了数据搬运工。
- 数据库 → Entity
- Entity → DTO
- DTO → VO
- VO → JSON
唯一的变化是字段名从 user_name 变成了 userName。
这不是编程,这是体力劳动。
七、框架的真相:服务于企业,不是服务于你
7.1 你以为框架是为了什么?
很多开发者有一个误解:
框架是为了帮我写出优雅的代码。
错。
框架的真正目的是:
让任何一个中级工程师,都能在半天内看懂你的代码,然后接替你的工作。
| 开发者以为的 | 实际的 |
|---|---|
| 框架帮我写出优雅的代码 | 框架帮我写出可替换的代码 |
| 框架服务于开发者 | 框架服务于企业 |
| 命名规范是为了代码好看 | 命名规范是为了任何人能快速看懂 |
| 分层设计是为了架构清晰 | 分层设计是为了流水线作业 |
7.2 框架的真正价值
- 降低对人的要求 —— 不需要天才程序员,只需要会填空的工程师
- 提高可替换性 —— 你离职了,随便招个人都能接手
- 统一输出质量 —— 下限提高了,上限也锁死了
但这是工程化的必然。
7.3 这不是坏事,是工程化最终必然导致的结果
企业需要的是什么?
企业需要的是稳定交付,不是代码艺术。
框架是工业流水线,不是个人工作室。
CRUD 本来就是屎,框架只是让屎有了统一的形状。
统一的屎,也是屎。但至少它看起来不那么屎了。
7.4 所以
- 别怪框架让你不爽,框架本来就不是为你爽的
- 别怪代码没有乐趣,CRUD 本身就没有乐趣
- 框架让你成为了程序员里的螺丝钉,但企业需要的就是螺丝钉
这是现实。但也不要难过
八、总结
8.1 编程范式的真相
写了这么多,我想说的核心观点是什么?
编程范式没有最好的,只有最适合场景的。
- OOP 不是信仰,是工具
- 框架不是敌人,是工程化的产物
- 组件系统不是反 OOP,是更纯粹的 OOP
理解这一点,你就不会被各种"最佳实践"绑架。
8.2 企业级开发的真相
企业级开发的真相是什么?
- 框架服务于企业,不是服务于你
- 规范是为了可替换性,不是为了代码质量
- CRUD 就是屎,框架只是让屎有统一的形状
理解这一点,你就不会对框架有不切实际的期待。
8.3 那我们能做什么?
既然框架是流水线,既然 CRUD 是屎,那我们还能做什么?
理解规则,然后选择:接受还是突破。
- 接受:在企业级开发里,按框架的规则写代码,保证可维护性和可替换性
- 突破:在业余项目里,写自己想写的代码,保持对编程的热爱
在流水线里,偶尔偷偷写点让自己骄傲的代码。
8.4 最后的建议
学习语言的时候,理解其设计的核心思想和第一性原理。
你会发现:
好的设计与实现,永远都是从上到下、从里到外,都能体现它的统一的思想和理念的。
Java 的 OOP 逻辑自举可能没有 Python 完善,但 Java 的编程范式思想是贯穿始终的。哪怕引入的函数式编程,也没有脱离 OOP 的体系。
理解这一点,你学习任何语言都会更快。
因为你不是在学"语法",你是在学"设计思想"。
后记
这篇文章写了很多我的个人观点。
有些观点可能偏激,有些观点可能片面。
但这就是我的真实想法。
编程不是只有优雅和屎,它中间有很多灰色的东西。
如果你觉得这篇文章对你有启发,或者你不同意我的观点,欢迎留言讨论。
文章是不是为了教会你什么,而是如何在思考中,知道自己在做什么,而不是一味地听从别人的说法。
但如果你说"Spring Boot 就是最好的框架",那我也不会反驳你。
因为你开心就好。



