面向对象设计指南

面向对象设计指南

一般说设计模式都是指面向对象的设计模式,因为面向对象语言可以借助封装、继承、多态等特性更好的达到复用性、可拓展性、可维护性。

面向对象一般指以类、对象为组织代码的基本单元,并将封装、继承、多态、抽象四个特性(抽象有的定义里并不认为是四大特性)作为代码设计与实现的基石。

  • 封装:通过访问权限控制,只对外暴露必要的操作,保护数据;

  • 继承:代码复用,结构美感。不过 Java 语言不支持多重继承,原因是如果 BC 都继承了 A 并重写了某个方法,D 同时继承 BC 会产生歧义;

  • 多态:提高代码的复用性,主要通过两种方式实现:

    • 继承:父类引用指向子类对象;
    • 实现:接口引用指向具体实现类;
  • 抽象:有时并不计入四大特性,用来保护实现,例如接口就是对实现的一种抽象,无需关注实现。

用了面向对象语言就是面向对象开发吗?

  • 滥用 getter、setter 方法。lombok 的注解确实很方便,但这样其实违背了面向对象的封装特性,例如 createTime 等字段其实是不需要 setter 方法的,需要在创建对象的时候就确定。
  • 滥用全局变量、全局方法(Constants、Utils)。这样会导致修改后所有引用的地方都重新编译,而且有的时候只需要其中的某几个变量(或方法)却导入了整个类。
    • 功能拆分,不要定义一个大而全的类。例如 Constants 拆分为 DateConstants、RedisConstants、MysqlConstants
  • **定义数据与方法分离的类**。传统的 MVC 开发中,数据在相应的 BO、VO、PO 中,而操作却封装在对应的 Controller、Service 中,这就是典型的面向过程,也就是“贫血模型”的开发方法。不过这样的开发方式依然很流行,因为大部分的需求并不复杂,只是从数据库中找到一些字段,组织对应的 VO,先写 service 反推 controller。

如何理解接口与抽象类?

随着 jdk 版本的更新,接口也可以有默认实现,也可以定义变量作为常量使用。抽象类依然不允许被实例化,继承抽象类必须重写抽象类的所有方法。

先说结论:抽象类的作用更多是为了代码复用,而接口的作用则更偏向与“协议”,具备什么样的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class BaseEntity implements Serializable {

private static final long serialVersionUID = 8417380540303280008L;

@ApiModelProperty(value = "所属用户标识")
@Column(name = "USER_ID")
private String userId;

@ApiModelProperty(value = "记录是否有效,默认为1表示有效")
@Column(name = "ACTIVE")
private String active;

@ApiModelProperty(value = "创建时间,默认为当前时间")
@Column(name = "CREATED_AT", updatable = false)
@DateTimeFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND)
@JsonFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND, timezone = Constants.TIMEZONE)
private LocalDateTime createdAt;

@ApiModelProperty(value = "更新时间")
@Column(name = "UPDATED_AT")
@DateTimeFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND)
@JsonFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND, timezone = Constants.TIMEZONE)
private LocalDateTime updatedAt;

}

例如我们有一个上面的类,对于一个正常的删除来讲,一方面我们要查询这个数据是否存在(例如有些系统删除不存在的空数据会返回错误),另一方面判断当前登录用户是否具有删除权限(即资源的 USER_ID 是否为当前登录人或是否是当前登录人的下属),最后还需要记录日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void delete(String uuid) {
Entity entity = getEntityFromDB(uuid);
if (entity == null) {
throw ......
}
if (entity instanceof BaseEntity) {
BaseEntity e = (BaseEntity)entity;
if (!e.getActive.equals("1")) {

} else {
e.setActive("0");
saveEntityToDB(entity);
}
}
}

protected abstract T saveToDataBase(T entity);

protected abstract Entity getEntityFromDB(String uuid);

借助抽象类与多态,可以提高代码的复用性,减少重复代码。

如何理解基于接口而非实现编程?

假如目前有一个上传图片到公有云的需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class uploadPictureAliyunImpl {

// 获取合法 token
public String getToken() {

};

// 如果目录不存在就创建目录
public boolean createDictoryIfNotExists() {

};

// 上传图片
public boolean uploadPictureToAliyun() {

};

}

public class Main() {
public static void Main () {
uploadPictureAliyunImpl impl = new uploadPictureAliyunImpl();
String token = impl.getToken();
........
}
}

如果这样实现,后期替换为其他云厂商,例如自有的私有云,就需要替换很多代码,实际上这种情况只需要定义一个上传图片的接口,由不同的存储来实现就行。

基于接口编程,即进行更好的抽象设计,不暴露过多实现。

为什么说多用组合少用继承?

以鸟(bird)为例,可以分为是否会飞、是否会下蛋……

image-20250916170508725

当继承层次越来越深,关系会越来越复杂,会严重影响代码的稳定性与可维护性。但是当继承层次很浅且业务稳定时,依然可以利用继承和多态特性来实现特定功能。

继承实际上可以替换为组合来实现,例如定义两个接口:

1
2
3
4
5
6
7
public interface Flyable {
void fly();
}

public interface Eggable {
void egg();
}

每一种鸟类根据自己的情况来实现对应接口即可,但是这会引入新的问题,例如有 n 个鸟类实现的 fly 接口都是一样的,那代码重复会十分严重,解决方式就是“委托”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DefaultFlyableImpl implements Flyable {
void fly() {
......
}
}

public class AAABird implements Flyable {
// 一般是使用注解注入
private DefaultFlyableImpl defaultFlyImpl = new DefaultFlyableImpl();

void fly() {
defaultFlyImpl.fly();
}

}

面向对象设计指南
https://zhuwenjie0716.github.io/2026/05/16/面向对象设计的避坑指南/
作者
Wenjie Zhu
发布于
2026年5月16日
许可协议