为什么你写的 Java 代码"看着面向对象、实际是面向过程"?

「设计模式与范式」系列 Day02

写在前面

用 Java 写代码 ≠ 写的是面向对象风格代码。Utils 满天飞、getter/setter 无脑生成、贫血模型------这些都是披着面向对象外皮的面向过程代码。

这篇先打地基:OOP 是什么、四大特性各解决什么问题,再看几个"看着面向对象、实际面向过程"的反例。


一、是什么

面向对象编程(OOP):以类/对象为基本单元,封装、抽象、继承、多态为基石的编程范式。面向对象编程语言,就是提供了现成语法支持这四大特性的语言------但用面向对象语言写代码不等于写的就是面向对象风格,这正是本篇要讲的重点。

落地过程分三步:面向对象分析(OOA,搞清楚做什么 )→ 面向对象设计(OOD,搞清楚怎么做,产出类的拆分、属性方法、类间关系,常用 UML 表达)→ 面向对象编程(把设计翻译成代码)。


二、为什么:四大特性各解决什么问题

特性 解决的问题 怎么实现 价值
封装 隐藏数据 public/protected/private 访问控制 数据不被随意改;接口简单易用
抽象 隐藏实现 接口 / 抽象类(不需要专门语法) 改实现不影响调用方;过滤复杂细节
继承 代码复用 单继承 / 多继承语法 表达类之间的 is-a 关系
多态 子类替换父类 继承重写 / 接口实现 / duck-typing 一大批设计模式的实现基础

多态在 Java 里只有两条实现路径:继承 + 重写,或者接口实现,父子类/实现类关系是必须的。Python、Ruby 这类动态语言还多一条 duck-typing------不要求任何继承或接口关系,只要方法名相同就能实现多态,靠的是运行时动态查找。Java 做不了这种"长得像鸭子就是鸭子"式的多态。

面向对象和面向过程也没有严格的官方定义,理解它们最好的方式是互相对照:面向过程以过程/方法为基本单元,数据和方法分离,适合单一主线的脚本任务;面向对象以类为基本单元,数据和方法绑定,更适合网状交互的复杂系统。两者不是二选一的对立关系------面向过程可以看作面向对象的地基,每个方法内部的实现逻辑,本身就是一段面向过程风格的代码。


三、怎么用:三个"看着 OOP、实际面向过程"的反例

反例一:无脑生成 getter / setter

封装要求"外部只能通过有限接口访问数据",IDE 一键生成全套 setter,等于把内部数据全部暴露,直接违背封装。

更隐蔽的坑------容器类型属性,只给 getter 也挡不住:

java 复制代码
public class Order {
    private List<Item> items;
    public List<Item> getItems() { return items; }
}

order.getItems().clear(); // 没调用任何 setter,内部数据照样被清空

缓解方法:

java 复制代码
public List<Item> getItems() {
    return Collections.unmodifiableList(items);
}
// order.getItems().clear() 会直接抛 UnsupportedOperationException

局限:这只挡住了"改容器结构",容器里每个 Item 对象自身的字段依然能被外部改(getItems().get(0).setPrice(0) 照样生效)。

反例二:滥用全局变量 / 全局方法

单例对象、静态成员变量、常量 = 全局变量;静态方法 = 全局方法,数据和方法分离,典型面向过程。

大而全的 Constants 类是重灾区:改一个常量要在巨大文件里翻找,可维护性差;任何引用都会因为这一个类的改动被牵连重新编译,拖慢构建;想复用其中几个常量,得把整个大类一起搬走,复用性也差。改进思路很直接:要么按功能拆分成 MysqlConstantsRedisConstants 这类小类,要么干脆不单建 Constants 类,谁用就近定义在谁的类里。

Utils 类同理:两个无继承关系的类要复用同一段逻辑,建一个只有静态方法的 Utils 类是务实选择,本质面向过程,但不算滥用------能归到具体业务类的逻辑,优先归到那个类,不要习惯性丢进 Utils

反例三:数据和方法分离的贫血模型

MVC 三层架构下,Controller/Service/DAO 全是方法,POJO 只有 getter/setter 没有业务逻辑------这是目前 Web 项目的主流开发方式,却严格违背面向对象编码风格。背后的取舍权衡,留到后面模块专门展开。

为什么面向对象代码总是不自觉写成面向过程

人的默认思维是流程化的(先做 A 再做 B),这和面向过程天然契合;面向对象要求反过来想------先拆类、设计交互、再组装流程,属于刻意训练出来的能力,退化回面向过程很正常,不用太自责。


四、面试追问

Q1:duck-typing 是什么,Java 能不能实现?

方法名相同即可实现多态,不要求继承或接口关系,动态语言特有。Java 是静态语言,多态只能靠继承重写或接口实现这两条路。

Q2:Utils 类要不要完全禁用?

不用。两个无关联的类复用同一逻辑时是务实方案。要避免的是不假思索地把什么逻辑都往里塞------能归到具体业务类的,优先归那个类。

Q3:只提供 getter、不提供 setter,等于封装到位了吗?

看类型。基本类型/不可变对象可以;集合类型不行------getter 返回的是内部引用,调用方能直接 clear()/add()。需要 Collections.unmodifiableXxx() 包一层,且这只挡住了容器结构,挡不住容器内元素自身被改。


下一篇预告

Day03:接口 vs 抽象类怎么选、"基于接口而非实现编程"怎么落地、为什么都说"多用组合少用继承",以及贫血模型和充血模型这道选择题背后的取舍。

相关推荐
geovindu7 小时前
sql: Data Modeling Patterns
数据库·sql·设计模式·sqlserver
YHL2 天前
🎯 JavaScript 单例模式(Singleton Pattern)—— 从理论到实战
javascript·设计模式
2401_868534782 天前
网络环境规划核心考点全梳理
网络·设计模式
feiyu_gao2 天前
Cocreation Framework:一套给所有人的 AI 协作思考系统
设计模式·aigc·ai编程
善良勤劳勇敢而又聪明的老杨3 天前
【AI编程系列】MCP 常用设计模式解读
设计模式·ai编程
geovindu3 天前
java: Strategy Pattern
java·开发语言·后端·设计模式·策略模式·行为模式
码匠许师傅4 天前
【设计模式精讲】29.行为型模式总结与对比(Behavioral Patterns Summary)
c++·设计模式·uml
geovindu4 天前
CSharp: Observer Pattern
开发语言·后端·观察者模式·设计模式·c#·.netcore·行为模式
YHHLAI4 天前
NestJS 后端开发实战:从设计模式到 CRUD 全栈
设计模式