Java 面向对象:纯原生 OOP 工作实战落地
前言:为什么我要写这篇文章?
作为一个工作了半年的 Java 开发者,我其实见过我同事写代码:有很多直接面向实现编程,根本不用接口;要么是为了用而用,写一堆空壳接口。今天,我想从自己的实际开发经验出发,用第一人称的视角,和大家聊聊 「如何把架构思想真正应用到日常编码中」。
这篇文章会兼顾两个视角:
-
对初学者:我会特别提醒你哪些设计思想是核心,为什么重要,别只抄代码;
-
对有经验的开发者:我们一起思考 Spring 这些框架到底在解决什么问题,如何用原生 Java 写出「有架构感」的代码。
核心思想:无框架下的「接口 + 实现类 + 多态」落地
工作中即使不用 Spring 等框架,面向接口编程也是我必须遵守的 OOP 规范。在我看来,核心流程其实很简单:
定义接口(契约) → 编写实现类(逻辑) → 通过接口调用(多态解耦)
1. 场景:用户管理模块(查询 + 新增)
让我从一个最常见的业务场景开始——用户管理。我会用这个例子告诉你,我这半年工作中,是怎么把架构思想落地的。
(1)第一步:定义接口(只规定「能做什么」)
在我刚参加工作时,我也觉得接口是多余的——直接写实现类不香吗?我这里模拟一个需求哈,把用户模块的内存存储实现,迁移到本地文件存储实现,
我发现如果不用接口,要改的地方简直是灾难,因为一般的项目这个接口肯定有好多的地方调用了这个方法:
没使用接口前(噩梦级维护):
// Controller 里直接依赖内存实现类 UserDaoMemory userDao = new UserDaoMemory(); // Service 里也是直接依赖具体实现 UserServiceImpl userService = new UserServiceImpl();使用接口后(优雅切换):
// 只依赖接口,具体实现随便换 UserDao userDao = new UserDaoFile(); UserService userService = new UserServiceImplV2();
你要明白:接口是能力契约,必须先定义
接口只写方法签名,不写具体逻辑,目的是让调用方和实现方解耦。
/**
* 用户服务接口(契约)
* 规定:用户模块能做的两件事——查用户、新增用户
*/
public interface UserService {
// 根据ID查询用户
User getUserById(Long id);
// 新增用户
boolean addUser(User user);
}
别小看这个接口!Spring 的
@Service注解,本质上就是在帮你管理这些接口和实现类的关系;理解「契约」这个概念,比学会 10 个注解都重要——这是架构思维的起点。
| 对比维度 | 不使用接口 | 使用接口 |
|---|---|---|
| 代码耦合度 | 调用方直接依赖实现类,耦合度极高 | 调用方只依赖接口,耦合度极低 |
| 切换实现 | 需要修改所有调用代码 | 只需修改 new 的实现类,其他代码不动 |
| 可测试性 | 难以 Mock 测试 | 容易 Mock 接口进行单元测试 |
| 团队协作 | 实现类没写完,调用方没法开发 | 接口定好,双方可以并行开发 |
(2)第二步:编写实现类(封装)
实现类负责写具体业务逻辑,比如操作本地存储、校验数据等,把细节全部封装起来。这是我理解的「封装」最有价值的地方——外部不需要知道你是用 HashMap 还是本地文件,只需要调用接口就行。
这里我模拟内存数据库(实际工作中可以换成本地文件、MySQL、Redis 等,完全不影响调用方)。
import java.util.HashMap;
import java.util.Map;
/**
* 用户服务的内存实现类
* 所有逻辑都封装在这个类里,外部完全看不到
*/
public class UserServiceImplMemory implements UserService {
// 模拟数据库:用Map存储用户数据(封装在类内部,外部无法直接访问)
private static final Map<Long, User> USER_DB = new HashMap<>();
@Override
public User getUserById(Long id) {
// 模拟数据库查询逻辑
if (USER_DB.containsKey(id)) {
return USER_DB.get(id);
}
System.out.println("用户ID不存在:" + id);
return null;
}
@Override
public boolean addUser(User user) {
// 模拟数据校验逻辑(封装的体现:外部不用关心怎么校验)
if (user == null || user.getId() == null || user.getName() == null) {
System.out.println("用户数据不合法!");
return false;
}
// 模拟插入数据库
USER_DB.put(user.getId(), user);
System.out.println("用户新增成功:" + user.getName());
return true;
}
}
我会工作中都会把实现类的属性都设为
private,这是封装的基本要求;实际工作中,这个实现类可能会有几百行代码,但没关系——因为外部只依赖接口,你怎么改实现类都不会影响调用方。
| 封装级别 | 修饰符 | 访问范围 | 我的使用建议 |
|---|---|---|---|
| 私有封装 | private | 仅当前类 | 所有属性都应该用这个 |
| 受保护封装 | protected | 当前类、子类、同包 | 抽象类中给子类用的方法 |
| 默认封装 | 无修饰符 | 当前类、同包 | 很少用,尽量避免 |
| 公开封装 | public | 任何地方 | 仅用于对外暴露的方法 |
(3)第三步:编写实体类(封装数据)
实体类是数据载体,通过 private 修饰属性,提供 get/set 方法访问,这是封装的核心体现。很多人觉得 get/set 方法只是简单的取值赋值,其实不然——我们可以在 get/set 方法中加入额外逻辑,比如对年龄进行校验,保证数据合法性,这也是封装的重要应用。
/**
* 用户实体类(封装数据)
*/
public class User {
// 私有属性:外部不能直接访问,必须通过get/set方法
private Long id;
private String name;
private Integer age;
// 空参构造(必须有,方便后续扩展)
public User() {}
// 有参构造(方便快速创建对象)
public User(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
// 公共的get/set方法:外部唯一的访问入口,可添加额外逻辑
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
// 额外逻辑:校验年龄合法性,保证年龄在0-130岁之间
if (age < 0 || age > 130) {
throw new IllegalArgumentException("年龄不合法,必须在0-130岁之间");
}
this.age = age;
}
// 重写toString:方便打印对象信息
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', age=" + age + "}";
}
}
像上面这样,在 setAge 方法中加入年龄校验逻辑,就能保证无论外部如何赋值,User 实体的 age 属性始终是合法的——这就是封装的实际价值,把数据校验的逻辑封装在实体类内部,不用在每个使用 User 的地方重复写校验代码,既减少冗余,又保证数据安全。
(4)第四步:测试调用(多态的核心体现)
这是我最喜欢的部分——调用方只依赖接口,不依赖具体实现类。这就是解耦的关键!
/**
* 测试类:无框架下的OOP调用
*/
public class UserServiceTest {
public static void main(String[] args) {
// 核心:父类接口 指向 子类实现(多态)
// 这里的实现类是 UserServiceImplMemory,后续可以无缝替换
UserService userService = new UserServiceImplMemory();
// 1. 新增用户(测试年龄合法场景)
User user1 = new User(1L, "张三", 22);
boolean addResult1 = userService.addUser(user1);
System.out.println("新增用户1结果:" + addResult1);
// 2. 新增用户(测试年龄不合法场景,会抛出异常)
try {
User user2 = new User(2L, "李四", 150);
boolean addResult2 = userService.addUser(user2);
System.out.println("新增用户2结果:" + addResult2);
} catch (IllegalArgumentException e) {
System.out.println("新增用户2失败:" + e.getMessage());
}
// 3. 查询用户
User queryUser = userService.getUserById(1L);
System.out.println("查询到的用户:" + queryUser);
// ========== 扩展:无缝切换实现类 ==========
// 假设后续需要换成 本地文件实现,只需要新增一个实现类
// UserService userServiceFile = new UserServiceImplFile();
// 调用代码和上面完全一样,一行都不用改!
}
}
(5)运行结果
用户新增成功:张三
新增用户1结果:true
新增用户2失败:年龄不合法,必须在0-130岁之间
查询到的用户:User{id=1, name='张三', age=22}
2. 这里用到的 OOP 知识点总结
| 代码部分 | OOP 特性 | 核心作用 |
|---|---|---|
UserService 接口 | 接口、契约设计 | 定义规范,让调用方和实现方解耦 |
UserServiceImplMemory 实现类 | 接口实现、封装 | 藏起存储操作、数据校验等细节 |
UserService userService = new UserServiceImplMemory() | 多态 | 更换实现类时,调用方代码零修改 |
User 类的 private 属性 + get/set 方法 | 封装 | 保护数据安全,通过方法添加额外校验逻辑 |
3. Spring 如何简化这个过程?(从架构角度思考)
上面的代码是纯原生 Java 写法,实际工作中一般是使用 Spring 框架,可以省掉两个麻烦事:
-
new不用手动 对象:给实现类加@Service注解,Spring 启动时自动创建对象并放入容器; -
不用手动管理对象依赖:调用方加
@Autowired注解,Spring 自动从容器中取对象,不用写new UserServiceImplMemory()。
我的深度思考:
Spring 的注解设计思想,本质上就是「约定优于配置」——你按照约定加注解,框架就帮你处理对象的创建和依赖;
但千万别以为用了 Spring 就不用理解 OOP 了!Spring 只是工具,帮你简化对象创建和依赖管理,底层还是「接口 + 实现类 + 多态」的 OOP 思想!
对初学者:先理解原生 OOP,再学 Spring,你会豁然开朗——原来 Spring 只是帮你省了一些体力活。
| 特性 | 原生 Java 写法 | Spring 写法 | Spring 帮你省了什么 |
|---|---|---|---|
| 对象创建 | new UserServiceImplMemory() | @Service + @Autowired | 手动 new 对象 |
| 依赖管理 | 自己传递对象引用 | Spring 容器自动注入 | 手动管理依赖关系 |
| 作用域控制 | 自己写单例模式 | @Scope 注解 | 单例、多例等作用域管理 |
原生写法 vs Spring 写法对比示例:
// 原生 Java 写法 UserService userService = new UserServiceImplMemory(); UserController controller = new UserController(userService);// Spring 写法 @Service public class UserServiceImplMemory implements UserService { ... } @RestController public class UserController { @Autowired private UserService userService; }
二、抽象类实战:模板方法模式(无框架)
1. 场景:不同类型订单(VIP/普通)的统一流程
在我这半年的开发工作中,做过简单的购买商品相关需求,有这样的场景:所有订单都有相同流程(校验参数 → 构建订单 → 保存订单),但构建订单的逻辑不同。这时候,用抽象类抽公共逻辑就是最佳选择,其实一般公司项目都有架构做好了,你只需要知道这个即可,但是还是建议自己试试制作这种架构出来,这个是简化版的那种,但是思想很重要
(1)第一步:定义抽象类(抽公共流程)
抽象类是模板,写所有子类都通用的逻辑,用抽象方法留差异逻辑给子类实现。
/**
* 订单服务抽象类(模板)
* 作用:抽公共流程,约束子类必须实现差异逻辑
*/
public abstract class AbstractOrderService {
// 模板方法:统一订单创建流程(用 final 防止子类修改)
public final boolean createOrder(Order order) {
// 1. 校验参数(所有订单都一样,公共逻辑)
if (!checkParam(order)) {
return false;
}
// 2. 构建订单(子类自己实现,差异逻辑)
buildOrder(order);
// 3. 保存订单(所有订单都一样,公共逻辑)
saveOrder(order);
return true;
}
// 抽象方法:子类必须实现(差异逻辑)
protected abstract void buildOrder(Order order);
// 私有方法:公共逻辑,子类不能访问(封装)
private boolean checkParam(Order order) {
if (order == null || order.getOrderId() == null || order.getAmount() <= 0) {
System.out.println("订单参数不合法!");
return false;
}
return true;
}
// 私有方法:公共逻辑
private void saveOrder(Order order) {
System.out.println("保存订单成功:" + order.getOrderId());
}
}
注意模板方法用了
final修饰!这是为了防止子类修改流程——既然是模板,流程就不能乱改;这就是「模板方法模式」,Spring 中到处都是这种设计(比如
JdbcTemplate)阅读源码也是一个非常好的习惯。
| 对比项 | 接口 | 抽象类 | 我的选择建议 |
|---|---|---|---|
| 方法实现 | 全部都是抽象方法(Java 8 前) | 可以有抽象方法和具体方法 | 需要抽公共逻辑时用抽象类 |
| 继承关系 | 一个类可以实现多个接口 | 一个类只能继承一个抽象类 | 有「is-a」关系时用抽象类 |
| 构造函数 | 没有构造函数 | 有构造函数 | 需要初始化属性时用抽象类 |
| 变量类型 | 只能定义常量 | 可以定义实例变量 | 需要维护状态时用抽象类 |
| 注:比如文章里的「VIP 订单实现类」和「抽象订单服务类」:VIP 订单是一个订单,满足「is-a」,所以可以继承;但如果是「支付服务」和「订单服务」,就不满足(支付不是订单),强行继承会导致代码混乱、难以维护。 |
接口 vs 抽象类使用场景对比:
// 接口:定义能力契约 public interface Payable { void pay(BigDecimal amount); }// 抽象类:抽取公共流程 public abstract class AbstractPayment implements Payable { // 公共逻辑 protected void checkAmount(BigDecimal amount) { if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("金额不合法"); } } }
(2)第二步:子类实现差异逻辑
子类只需要写自己的独特逻辑,不用重复写校验、保存等公共代码。这就是「继承复用」的价值!
/**
* VIP 订单实现类
*/
public class VipOrderServiceImpl extends AbstractOrderService {
@Override
protected void buildOrder(Order order) {
// VIP 订单专属逻辑:打 8 折
order.setAmount(order.getAmount() * 0.8);
order.setOrderType("VIP");
System.out.println("构建 VIP 订单,享受 8 折优惠");
}
}
/**
* 普通订单实现类
*/
public class NormalOrderServiceImpl extends AbstractOrderService {
@Override
protected void buildOrder(Order order) {
// 普通订单专属逻辑:原价
order.setOrderType("NORMAL");
System.out.println("构建普通订单,原价购买");
}
}
(3)第三步:订单实体类
/**
* 订单实体类
*/
public class Order {
private Long orderId;
private double amount;
private String orderType;
// get/set 方法(可根据需求添加额外逻辑,如金额校验)
public Long getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public double getAmount() { return amount; }
public void setAmount(double amount) {
// 额外逻辑:确保金额为正数
if (amount <= 0) {
throw new IllegalArgumentException("订单金额必须大于0");
}
this.amount = amount;
}
public String getOrderType() { return orderType; }
public void setOrderType(String orderType) { this.orderType = orderType; }
}
(4)测试调用(多态)
public class OrderServiceTest {
public static void main(String[] args) {
// 1. 创建 VIP 订单
AbstractOrderService vipOrderService = new VipOrderServiceImpl();
Order vipOrder = new Order();
vipOrder.setOrderId(1001L);
vipOrder.setAmount(200.0);
vipOrderService.createOrder(vipOrder);
System.out.println("VIP 订单最终金额:" + vipOrder.getAmount());
System.out.println("==========");
// 2. 创建普通订单
AbstractOrderService normalOrderService = new NormalOrderServiceImpl();
Order normalOrder = new Order();
normalOrder.setOrderId(1002L);
normalOrder.setAmount(200.0);
normalOrderService.createOrder(normalOrder);
System.out.println("普通订单最终金额:" + normalOrder.getAmount());
// 测试金额不合法场景
System.out.println("==========");
try {
Order invalidOrder = new Order();
invalidOrder.setOrderId(1003L);
invalidOrder.setAmount(-50.0);
normalOrderService.createOrder(invalidOrder);
} catch (IllegalArgumentException e) {
System.out.println("创建订单失败:" + e.getMessage());
}
}
}
(5)运行结果
构建 VIP 订单,享受 8 折优惠
保存订单成功:1001
VIP 订单最终金额:160.0
==========
构建普通订单,原价购买
保存订单成功:1002
普通订单最终金额:200.0
==========
创建订单失败:订单金额必须大于0
2. 这里用到的 OOP 知识点
-
抽象类:作为模板,抽公共逻辑,约束子类行为;
-
继承:子类继承抽象类,复用校验、保存等代码;
-
多态:父类引用指向子类对象,统一调用入口;
-
封装:公共逻辑藏在抽象类,子类只关心差异。
三、多态 + 策略模式:支付方式切换(无框架)
1. 场景:支持微信、支付宝两种支付方式,随时切换
支付场景也是我在工作中接触过的,实际是代码已经有了这个代码,我是把这个场景抽象出来作为示例,用户可以选择微信支付或支付宝支付,未来可能还要加银联支付。这时候,「策略模式」就派上用场了!当然这个在后面的Java高级会详细介绍
(1)第一步:定义支付接口
import java.math.BigDecimal;
/**
* 支付服务接口
*/
public interface PayService {
// 支付方法
void pay(BigDecimal amount);
}
(2)第二步:实现不同支付方式
import java.math.BigDecimal;
/**
* 微信支付实现类
*/
public class WechatPayServiceImpl implements PayService {
@Override
public void pay(BigDecimal amount) {
// 可添加额外逻辑:比如校验支付金额、记录支付日志
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("支付金额必须大于0");
}
System.out.println("微信支付:扣款 " + amount + " 元");
}
}
/**
* 支付宝支付实现类
*/
public class AliPayServiceImpl implements PayService {
@Override
public void pay(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("支付金额必须大于0");
}
System.out.println("支付宝支付:扣款 " + amount + " 元");
}
}
(3)第三步:策略工厂(管理所有支付方式)
这是我从 Spring 的设计思想中借鉴来的——用一个工厂类来管理所有策略。Spring 的 @Autowired 配合 List 或 Map,本质上也是在做这件事。
import java.util.HashMap;
import java.util.Map;
/**
* 支付策略工厂
* 作用:根据支付类型,获取对应的支付实现类
*/
public class PayStrategyFactory {
// 存储所有支付方式
private static final Map<String, PayService> PAY_MAP = new HashMap<>();
// 静态代码块:初始化支付方式(项目启动时执行一次)
static {
PAY_MAP.put("WECHAT", new WechatPayServiceImpl());
PAY_MAP.put("ALI", new AliPayServiceImpl());
}
// 对外提供获取支付方式的方法
public static PayService getPayService(String payType) {
// 额外逻辑:校验支付类型是否存在
if (!PAY_MAP.containsKey(payType)) {
throw new IllegalArgumentException("不支持该支付方式:" + payType);
}
return PAY_MAP.get(payType);
}
}
我的开发经验:
这个工厂类,就像是 Spring 容器的简化版——它帮你管理所有的策略实现;
未来如果要加银联支付,只需要:
新增一个
UnionPayServiceImpl实现PayService;在
PayStrategyFactory的静态代码块中加一行PAY_MAP.put("UNION", new UnionPayServiceImpl());调用代码完全不用改!
这就是「开闭原则」——对扩展开放,对修改关闭。
| 设计模式 | 解决的问题 | 核心思想 | 工作中常见场景 |
|---|---|---|---|
| 策略模式 | 多个算法可以互换使用 | 封装算法,独立变化 | 支付方式、优惠计算、日志输出 |
| 模板方法模式 | 流程固定,步骤可变 | 固定流程,延迟实现 | 订单处理、数据导入、报表生成 |
| 工厂模式 | 对象创建复杂 | 封装创建逻辑,解耦 | DAO 层、Service 层、策略管理 |
不使用策略模式 vs 使用策略模式对比:
// 不使用策略模式(代码充满 if-else) public void pay(String payType, BigDecimal amount) { if ("WECHAT".equals(payType)) { System.out.println("微信支付:扣款 " + amount); } else if ("ALI".equals(payType)) { System.out.println("支付宝支付:扣款 " + amount); } else if ("UNION".equals(payType)) { System.out.println("银联支付:扣款 " + amount); } }// 使用策略模式(优雅扩展) public void pay(String payType, BigDecimal amount) { PayService payService = PayStrategyFactory.getPayService(payType); payService.pay(amount); }
(4)测试调用
import java.math.BigDecimal;
public class PayTest {
public static void main(String[] args) {
// 模拟前端传入的支付类型
String payType = "WECHAT";
// 1. 获取支付方式(多态)
PayService payService = PayStrategyFactory.getPayService(payType);
// 2. 支付(测试合法金额)
payService.pay(new BigDecimal("100"));
// 切换支付方式:只改 payType 就行
payType = "ALI";
payService = PayStrategyFactory.getPayService(payType);
payService.pay(new BigDecimal("200"));
// 测试不支持的支付方式
System.out.println("==========");
try {
payType = "UNION";
payService = PayStrategyFactory.getPayService(payType);
payService.pay(new BigDecimal("150"));
} catch (IllegalArgumentException e) {
System.out.println("支付失败:" + e.getMessage());
}
// 测试非法支付金额
System.out.println("==========");
try {
payType = "WECHAT";
payService = PayStrategyFactory.getPayService(payType);
payService.pay(new BigDecimal("-50"));
} catch (IllegalArgumentException e) {
System.out.println("支付失败:" + e.getMessage());
}
}
}
(5)运行结果
微信支付:扣款 100 元
支付宝支付:扣款 200 元
==========
支付失败:不支持该支付方式:UNION
==========
支付失败:支付金额必须大于0
四、无框架 OOP 落地核心原则(我的工作心得)
这半年的开发经验告诉我,无论用不用框架,这几个原则都必须遵守,反正方便自己拓展和向架构师拓展:
-
接口优先:任何业务逻辑都先定义接口,再写实现类,实现「调用方不依赖实现」;
-
封装彻底:属性私有化、公共逻辑藏在抽象类/实现类内部,外部只关心结果,可在 get/set 或方法中添加额外校验逻辑;
-
多态解耦:用父类(接口/抽象类)引用指向子类对象,更换实现类零成本;
-
继承复用:只继承有「is-a」关系的类,用抽象类抽公共逻辑,避免重复代码。
无框架开发中,我基本也是基于 Java 面向对象三大特性落地业务:
用接口定义业务契约,实现类封装具体逻辑,通过多态调用做到「换实现不换代码」;
用抽象类做模板,抽取所有子类的公共流程,子类只实现差异逻辑,减少重复代码;
用封装保护数据和逻辑,属性私有化、公共方法隐藏细节,可在方法中添加校验等额外逻辑,保证代码安全性;
Spring 框架只是简化了对象创建和依赖管理,核心还是 OOP 思想的落地。
建议:
反正我觉得别急着学 Spring,先把原生 OOP 练熟,就是要在看见一个业务的时候先试试从架构开始入手,而不是直接开写
然后就是写代码时可以多想一想:「这里能不能用接口?能不能抽个抽象类?能不能在 get/set 中加校验逻辑?」;
反正架构思维也不是一天练成的,就是要多练,看到业务和开始设计的时候才会若有神助
还是推荐先去找项目开始练手,或者自己找个场景自己设计,然后看看拓展性等等,是不是很好,反正初级阶段就这些
| 阶段 | 学习重点 | 我的建议 |
|---|---|---|
| 入门阶段 | OOP 三大特性(封装、继承、多态) | 先别学框架,用纯原生 Java 写项目,重点练习封装的实际应用 |
| 进阶阶段 | 接口、抽象类、设计模式 | 每个设计模式都用原生 Java 实现一遍,结合实际场景思考应用 |
| 应用阶段 | Spring 框架(当前先不管这个) | 理解 Spring 是如何基于 OOP 思想简化开发的,不盲目依赖注解 |
| 架构阶段 | 架构设计原则、高可用、高性能 | 从业务角度出发,设计合理的架构 |