软件工程就是一场“抽象”游戏:从 abstract 关键字到架构设计的认知跃迁

写在前面

"我接触过的抽象,好像就是抽象类和抽象方法。抽象类里只能写抽象方法,子类必须重写。其他地方好像用到的不多......对了,有时候把数据展示给前端,好像也需要'抽象'一层,返回给前端不同的对象。"

这是很多 Java 初学者的真实困惑。我们把"抽象"理解成了一个语法关键字 ,而不是一种设计思想。所以当听到"软件工程就是一个抽象的过程"这种话时,总觉得玄乎,和自己写的 CRUD 没什么关系。

但事实上,你每天都在不知不觉地使用抽象------只是你没有意识到。

  • 当你定义 List 接口而不是直接用 ArrayList 时,你在抽象

  • 当你把数据库的 User 实体转换成 UserVO 返回给前端时,你在抽象

  • 当你编写一个通用的 RedisService 封装了缓存逻辑时,你在抽象

  • 当你按照三层架构(Controller/Service/DAO)组织代码时,你更是在做宏观的抽象

今天,我们就从"abstract 关键字"出发,一路走到架构设计层面,彻底搞懂:抽象到底是什么?为什么说它是软件工程的核心?Java 开发者如何用好抽象这把利器?

一、什么是抽象?从地图说起

想象你要去一个陌生的城市。你会怎么做?

  • 你会拿到一张城市地图。地图上画了主要道路、地标建筑、公交线路。

  • 但地图上不会画出每一棵树、每一个井盖、每一户人家的窗户。

地图就是现实世界的"抽象":它保留了关键信息(路怎么走、地标在哪),去掉了无关细节(树木、窗户)。这让你能快速理解城市结构,而不被海量细节淹没。

软件工程中的抽象,本质上和地图一样:忽略与当前目标无关的细节,聚焦于本质特征

  • 当你调用 list.get(0) 时,你不关心 ArrayList 内部是数组还是链表------List 接口就是抽象

  • 当你发送 HTTP 请求时,你不关心底层 TCP 三次握手------HttpClient 就是抽象

  • 当你使用 @Transactional 时,你不关心事务的开启、提交、回滚细节------Spring 帮你抽象了

抽象的目的不是让代码"变少",而是让复杂度"可控"。 通过分层抽象,我们才能在有限的大脑容量下构建出百万行级别的软件系统。

二、Java 中的抽象语法:abstract 关键字只是冰山一角

2.1 抽象类 vs 接口:纠正常见误区

很多初学者认为:"抽象类里只能有抽象方法,抽象方法必须重写。"

这是错误的。 抽象类可以有:

  • 抽象方法(没有方法体,子类必须实现)

  • 具体方法(有方法体,子类可继承或重写)

  • 成员变量、构造器(虽然不能直接实例化,但可以被子类构造器调用)

java 复制代码
public abstract class AbstractAnimal {
    private String name;  // 可以有字段
    
    public AbstractAnimal(String name) {  // 可以有构造器
        this.name = name;
    }
    
    public void eat() {  // 具体方法,子类可直接使用
        System.out.println(name + " is eating");
    }
    
    public abstract void makeSound();  // 抽象方法,子类必须实现
}

抽象类和接口的选择原则

  • 抽象类:用于表示"is-a"关系(猫是一种动物),且需要共享状态或通用行为时

  • 接口:用于表示"can-do"关系(鸟会飞、车能跑),且需要多实现或完全解耦时

Java 8 之后接口可以有默认方法和静态方法,接口的能力大大增强。现代 Java 开发中,接口的使用频率远超抽象类。

2.2 抽象方法的重写:不是"被迫",而是"填空"

抽象方法强制子类提供具体实现,这其实是模板方法模式的体现。父类定义"骨架",子类填充"细节"。

java 复制代码
public abstract class DataProcessor {
    // 模板方法:定义了处理流程的骨架
    public final void process() {
        loadData();
        processData();
        saveResult();
    }
    
    protected abstract void loadData();    // 子类实现
    protected abstract void processData(); // 子类实现
    protected abstract void saveResult();  // 子类实现
}

这种设计让核心流程不变,而具体步骤可以灵活替换。你在 Spring 中见过的 JdbcTemplateRedisTemplate 都大量使用了这种思想。

三、抽象在开发中的真实应用(你每天都在用)

3.1 面向接口编程:最经典的抽象实践

java 复制代码
// 不抽象:直接依赖具体实现
private ArrayList<User> users = new ArrayList<>();

// 抽象:依赖接口
private List<User> users = new ArrayList<>();

为什么第二段更好?因为将来你可以把 ArrayList 换成 LinkedListCopyOnWriteArrayList,而调用方代码(users.get(0)users.add(user))完全不用改。

依赖倒置原则 :高层模块不应该依赖低层模块,二者都应该依赖抽象。你写的 Service 层应该依赖 UserDao 接口,而不是具体的 UserDaoImpl

3.2 数据交互中的抽象:PO/DO/DTO/VO 的划分

你提到的"把数据展示给前端需要抽象一层",这正是数据抽象最典型的例子。

  • PO(Persistent Object):数据库表结构的一对一映射,可能包含密码、创建时间等内部字段

  • VO(View Object):为前端定制的对象,只包含前端需要的字段,可能聚合多个 PO 的数据

如果直接把 UserPO(包含 passwordsalt)返回给前端,不仅泄露敏感信息,而且前端可能只需要 nameavatar,传输了大量无用数据。

java 复制代码
// 不好的抽象:直接暴露内部实体
@GetMapping("/user/{id}")
public UserPO getUser(@PathVariable Long id) { ... }

// 好的抽象:返回 VO
@GetMapping("/user/{id}")
public UserVO getUser(@PathVariable Long id) {
    UserPO po = userService.getById(id);
    return new UserVO(po.getId(), po.getName(), po.getAvatar());
}

这一层抽象,隔离了内部模型和外部契约,让前后端可以独立演进。

3.3 设计模式中的抽象:23 种模式几乎都在讲抽象

  • 策略模式:将算法抽象成接口,运行时替换

  • 工厂模式:将对象创建过程抽象,客户端不关心具体类

  • 适配器模式:将不兼容的接口抽象成统一的调用方式

  • 代理模式:在不修改原始类的情况下,抽象出额外的控制逻辑(如 Spring AOP)

3.4 分层架构:宏观层面的抽象

三层架构(Controller/Service/DAO)就是一种抽象:

  • Controller 层抽象了 HTTP 请求的处理方式

  • Service 层抽象了业务逻辑的核心流程

  • DAO 层抽象了数据存储的细节

每一层只关心它需要知道的,不关心下层具体怎么实现。这就是关注点分离

四、抽象思想的更高层次:从代码到架构

4.1 微服务中的抽象边界

微服务拆分本质上是在做领域抽象:哪些功能应该放在一起(高内聚),哪些应该隔离开(低耦合)。一个好的微服务边界,就是一个合理的业务抽象。

比如"订单服务"抽象了订单创建、支付、查询的逻辑,而"库存服务"抽象了库存扣减、锁定的逻辑。两者通过 API 契约(也是抽象)交互,互不依赖内部实现。

4.2 领域驱动设计(DDD)中的抽象

DDD 中的聚合根、值对象、领域事件都是抽象工具。它们帮助开发者在复杂的业务逻辑中,提炼出核心的领域模型,忽略非本质的技术细节。

4.3 配置与策略的抽象

现代框架大量使用声明式配置application.yml、注解)来抽象底层实现。你写一行 @Cacheable,Spring 就帮你完成了缓存逻辑的编织------你不需要知道它是用 Caffeine 还是 Redis。

五、抽象不是万能的:过度抽象的代价

抽象虽好,但过度抽象也会带来问题:

  • 过度设计:为了"可能将来会变"而做多层抽象,结果代码臃肿、难以追踪

  • 性能损耗:多层抽象可能带来额外的间接调用和方法分派开销

  • 学习成本:过度抽象的代码,新人需要花大量时间理解"这层是干什么的"

抽象的原则 :只抽象那些确实会变化 的部分,遵循 YAGNI(You Aren't Gonna Need It) 原则。Rails 之父 DHH 有一句名言:"过度抽象比重复代码更糟糕。"

六、总结:抽象能力是区分"码农"和"工程师"的关键

回到最初的问题:抽象到底是什么?

  • 在语法层面,它是 abstract 关键字、接口、抽象类

  • 在代码层面,它是面向接口编程、设计模式、分层架构

  • 在思想层面,它是忽略细节、聚焦本质的思维方式

一个初级开发者只会写"能跑的代码";一个高级工程师会写"能应对变化的代码"。而抽象,就是应对变化最有力的武器。

当你下次写代码时,不妨多问自己一句:

"我这里的逻辑,哪些是核心本质,哪些是当前细节?我有没有办法把本质抽象出来,让细节可以随时替换?"

培养这种思维习惯,你就已经走在从"码农"到"工程师"的路上了。

你最近写的代码中,有没有哪一块逻辑是"写死了具体实现",但实际上可能在未来需要替换的?如果现在让你重构,你会如何引入一层抽象?欢迎在评论区写下你的案例和设计思路。

相关推荐
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(十):线程安全和重入、死锁相关话题
java·linux·运维·服务器·c++·学习·安全
QuZero2 小时前
getCategoryData False Fault Alarm Process
java·经验分享
梦梦代码精2 小时前
LikeShop 深度测评:开源电商的务实之选
java·前端·数据库·后端·云原生·小程序·php
likerhood2 小时前
设计模式之建造者模式(Builder Pattern)java版本
java·设计模式·建造者模式
冷雨夜中漫步2 小时前
AI入门——MCP 协议核心解读:从 JSON-RPC 到 Host/Client/Server 实战
人工智能·后端·ai
happymaker06262 小时前
Nexus私服的使用(配合Maven)
java·maven
JAVA学习通2 小时前
本地知识库接入大模型时的权限隔离与安全设计
java·人工智能·安全·spring
AbandonForce2 小时前
C++ 多态(多态定义 多态应用 多态底层||final override关键字||抽象类)
java·开发语言·c++
程序员cxuan2 小时前
马斯克把 Cursor 给收了
人工智能·后端·程序员