Java 面向对象:纯原生 面向对象 工作实战落地

2 阅读18分钟

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 框架,可以省掉两个麻烦事:

  1. new 不用手动 对象:给实现类加 @Service 注解,Spring 启动时自动创建对象并放入容器;

  2. 不用手动管理对象依赖:调用方加 @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 配合 ListMap,本质上也是在做这件事。

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 容器的简化版——它帮你管理所有的策略实现;

  • 未来如果要加银联支付,只需要:

    1. 新增一个UnionPayServiceImpl 实现 PayService

    2. PayStrategyFactory 的静态代码块中加一行 PAY_MAP.put("UNION", new UnionPayServiceImpl())

    3. 调用代码完全不用改!

  • 这就是「开闭原则」——对扩展开放,对修改关闭。

设计模式解决的问题核心思想工作中常见场景
策略模式多个算法可以互换使用封装算法,独立变化支付方式、优惠计算、日志输出
模板方法模式流程固定,步骤可变固定流程,延迟实现订单处理、数据导入、报表生成
工厂模式对象创建复杂封装创建逻辑,解耦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 落地核心原则(我的工作心得)

这半年的开发经验告诉我,无论用不用框架,这几个原则都必须遵守,反正方便自己拓展和向架构师拓展:

  1. 接口优先:任何业务逻辑都先定义接口,再写实现类,实现「调用方不依赖实现」;

  2. 封装彻底:属性私有化、公共逻辑藏在抽象类/实现类内部,外部只关心结果,可在 get/set 或方法中添加额外校验逻辑;

  3. 多态解耦:用父类(接口/抽象类)引用指向子类对象,更换实现类零成本;

  4. 继承复用:只继承有「is-a」关系的类,用抽象类抽公共逻辑,避免重复代码。

无框架开发中,我基本也是基于 Java 面向对象三大特性落地业务:

  1. 接口定义业务契约,实现类封装具体逻辑,通过多态调用做到「换实现不换代码」;

  2. 抽象类做模板,抽取所有子类的公共流程,子类只实现差异逻辑,减少重复代码;

  3. 封装保护数据和逻辑,属性私有化、公共方法隐藏细节,可在方法中添加校验等额外逻辑,保证代码安全性;

  4. Spring 框架只是简化了对象创建和依赖管理,核心还是 OOP 思想的落地。

建议

  • 反正我觉得别急着学 Spring,先把原生 OOP 练熟,就是要在看见一个业务的时候先试试从架构开始入手,而不是直接开写

  • 然后就是写代码时可以多想一想:「这里能不能用接口?能不能抽个抽象类?能不能在 get/set 中加校验逻辑?」;

  • 反正架构思维也不是一天练成的,就是要多练,看到业务和开始设计的时候才会若有神助

  • 还是推荐先去找项目开始练手,或者自己找个场景自己设计,然后看看拓展性等等,是不是很好,反正初级阶段就这些

阶段学习重点我的建议
入门阶段OOP 三大特性(封装、继承、多态)先别学框架,用纯原生 Java 写项目,重点练习封装的实际应用
进阶阶段接口、抽象类、设计模式每个设计模式都用原生 Java 实现一遍,结合实际场景思考应用
应用阶段Spring 框架(当前先不管这个)理解 Spring 是如何基于 OOP 思想简化开发的,不盲目依赖注解
架构阶段架构设计原则、高可用、高性能从业务角度出发,设计合理的架构