作为一名常年和系统设计打交道的开发者,每次都会被 OOD(面向对象设计)的六大原则戳中 —— 这六条看似抽象的规则,其实是避开 “代码越写越烂” 陷阱的核心心法。

很多人刚接触时会觉得 “这些原则太理论,实际开发用不上”,但只要真正理解并落地,你会发现代码的扩展性、可维护性会发生质的变化。今天就用大白话拆解这六大原则,每个原则都配了 Java 代码示例,从概念到实践一步到位,新手也能看懂。

一、开闭原则:对扩展开放,对修改关闭

核心概念:这是 OOD 原则的 “老大”,核心思想是 —— 当需要给系统新增功能时,尽量通过 “扩展已有代码” 实现,而不是 “修改已有代码”。这样能避免改动旧代码时,不小心引入新 Bug,也能让系统更稳定。

举个例子:比如你开发了一个电商系统的 “订单折扣” 功能,初期只有 “会员折扣”,后来要加 “节日折扣”,如果一开始就遵循开闭原则,就不用改原来的会员折扣代码,直接加个新的折扣类就行。

下面给出一个示例代码:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 1. 先定义一个抽象的折扣接口(稳定的抽象层)
public interface Discount {
// 计算折扣后的价格
double calculateDiscount(double originalPrice);
}

// 2. 会员折扣实现类(已有的功能,无需修改)
public class MemberDiscount implements Discount {
@Override
public double calculateDiscount(double originalPrice) {
// 会员打9折
return originalPrice * 0.9;
}
}

// 3. 新增节日折扣(扩展新类,不修改旧代码)
public class HolidayDiscount implements Discount {
@Override
public double calculateDiscount(double originalPrice) {
// 节日打8折
return originalPrice * 0.8;
}
}

// 4. 订单服务(依赖抽象接口,而非具体实现)
public class OrderService {
// 接收 Discount 接口,不管是哪种折扣,这里都不用改
public double calculateFinalPrice(double originalPrice, Discount discount) {
return discount.calculateDiscount(originalPrice);
}
}

// 测试:新增功能时,直接传新的实现类
public class Test {
public static void main(String[] args) {
OrderService orderService = new OrderService();
double originalPrice = 100;

// 会员订单
double memberPrice = orderService.calculateFinalPrice(originalPrice, new MemberDiscount());
System.out.println("会员价:" + memberPrice); // 输出 90.0

// 节日订单(新增功能,旧代码没动)
double holidayPrice = orderService.calculateFinalPrice(originalPrice, new HolidayDiscount());
System.out.println("节日价:" + holidayPrice); // 输出 80.0
}
}

从上面的代码中我们可以看到,对于折扣这个对象我们抽象出一个接口,然后对于不同类型的折扣通过接口的实现了创建具体的类。这样一来,创建订单时,我们只要区分现在要用的折扣是哪个类型的就可以了。以后要新增一个折扣类型(比如“618折扣”)只要扩展Discount这个接口就行了。

二、里氏替换原则:子类能无缝替代父类,且不破坏程序逻辑

核心概念:简单说就是 “子类是父类的加强版,但不能颠覆父类的原有功能”。如果一个程序里用了父类对象,把它换成子类对象后,程序还能正常跑,这就符合里氏替换;反之如果换了之后程序崩了,那就是违反了这个原则。

最典型的反例就是 “正方形不是长方形”—— 如果把长方形的 “宽” 和 “长” 分开设置,子类正方形强制让宽 = 长,那用子类替换父类后,修改长或宽的逻辑就会出错。

我们看下面的代码

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 1. 父类:鸟类(有“飞”的基础功能)
public class Bird {
// 飞行速度
protected double speed;

// 计算飞行时间(基础逻辑)
public double calculateFlyTime(double distance) {
return distance / speed;
}

// 设置飞行速度
public void setSpeed(double speed) {
this.speed = speed;
}
}

// 2. 子类:麻雀(继承鸟类,不破坏父类逻辑)
public class Sparrow extends Bird {
// 麻雀的飞行速度有默认值,也可以通过父类方法修改
public Sparrow() {
this.speed = 50; // 假设麻雀默认飞行速度50km/h
}
}

// 3. 子类:老鹰(继承鸟类,扩展功能但不颠覆)
public class Eagle extends Bird {
public Eagle() {
this.speed = 150; // 老鹰飞得更快
}

// 新增“俯冲”功能(扩展,不修改父类方法)
public void dive() {
System.out.println("老鹰正在俯冲捕猎!");
}
}

// 测试:子类替换父类,程序正常运行
public class Test {
public static void main(String[] args) {
double distance = 100;

// 父类对象
Bird bird = new Bird();
bird.setSpeed(80);
System.out.println("普通鸟飞行时间:" + bird.calculateFlyTime(distance)); // 100/80=1.25

// 子类麻雀替换父类
Bird sparrow = new Sparrow();
System.out.println("麻雀飞行时间:" + sparrow.calculateFlyTime(distance)); // 100/50=2.0

// 子类老鹰替换父类
Bird eagle = new Eagle();
System.out.println("老鹰飞行时间:" + eagle.calculateFlyTime(distance)); // 100/150≈0.666

// 老鹰还能调用自己的扩展方法
((Eagle) eagle).dive(); // 输出“老鹰正在俯冲捕猎!”
}
}

在上面的代码中,Sparrow和Eagle都继承自Bird这个类,是Bird的“特例”,但是本质上她们都是鸟,所以都有fly这个能力。因此就算是在另一个地方把Sparrow替换成Bird,程序也不会报错,因为它没有破坏Bird的逻辑。

三、依赖倒置原则:依赖抽象,不依赖具体实现

核心概念:这条原则其实是开闭原则的 “支撑”,核心是 “高低层模块都要依赖抽象,抽象不能依赖具体”。简单说就是 —— 不要让你的代码依赖某个具体的类,而是依赖接口或抽象类,这样高层模块(比如服务类)就不会被低层模块(比如工具类)的变动影响

比如你开发一个 “消息通知” 功能,高层模块是 “通知服务”,低层模块是 “短信通知”“邮件通知”。如果通知服务直接依赖 “短信通知” 这个具体类,后来要加 “邮件通知”,就得改通知服务的代码;但如果依赖 “通知接口”,加新功能时只需要加个接口实现类就行。

我们还是来看代码示范:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 1. 抽象接口:消息通知(稳定的抽象层)
public interface Notification {
void send(String content);
}

// 2. 低层模块1:短信通知(具体实现)
public class SmsNotification implements Notification {
@Override
public void send(String content) {
System.out.println("通过短信发送:" + content);
}
}

// 3. 低层模块2:邮件通知(新增的具体实现)
public class EmailNotification implements Notification {
@Override
public void send(String content) {
System.out.println("通过邮件发送:" + content);
}
}

// 4. 高层模块:通知服务(依赖抽象接口,不依赖具体实现)
public class NotificationService {
// 构造方法注入抽象接口(而非具体类)
private Notification notification;

public NotificationService(Notification notification) {
this.notification = notification;
}

// 发送消息的核心逻辑(不用改)
public void notifyUser(String content) {
notification.send(content);
}
}

// 测试:高层模块无需修改,就能切换不同的通知方式
public class Test {
public static void main(String[] args) {
// 短信通知
NotificationService smsService = new NotificationService(new SmsNotification());
smsService.notifyUser("您的验证码是123456"); // 输出“通过短信发送:您的验证码是123456”

// 邮件通知(高层模块没改,只换了实现类)
NotificationService emailService = new NotificationService(new EmailNotification());
emailService.notifyUser("您有一封新邮件,请查收"); // 输出“通过邮件发送:您有一封新邮件,请查收”
}
}

在上面的代码中:NotificationService 的notifyUser实际上是调用了Notification的send方法,但是由于它是一个抽象方法,具体的实现都在不同的通知类中,因此当服务方法调用时,只要指派不同的通知类即可。
这里的核心是NotificationService 依赖的只是 Notification 这个抽象接口,这样方便后续的扩展。假设未来如果有了新的通知类型,比如微信通知,那么创建一个WeChatNotification并实现Notification即可。

四、组合 / 聚合原则:优先用组合 / 聚合,少用继承

核心概念:继承的问题在于 “强耦合”—— 子类会依赖父类的实现,如果父类改了,子类可能跟着崩;而组合 / 聚合是 “弱耦合”—— 一个类通过 “包含另一个类的对象” 来使用其功能,双方可以独立变化。所以设计时要优先选组合 / 聚合,实在适合继承(比如子类是父类的 “is-a” 关系)再用继承。

比如 “汽车” 和 “发动机” 的关系:汽车需要发动机才能跑,但汽车不是 “继承” 发动机(因为汽车不是发动机的一种),而是 “组合” 发动机(汽车里包含一个发动机对象);而 “轿车” 和 “汽车” 是 “is-a” 关系,适合用继承。

我们来看代码的示范:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 1. 发动机类(被组合的类)
public class Engine {
// 发动机启动
public void start() {
System.out.println("发动机启动,开始提供动力");
}

// 发动机停止
public void stop() {
System.out.println("发动机停止,动力中断");
}
}

// 2. 汽车类(组合发动机,而非继承)
public class Car {
// 汽车包含一个发动机对象(组合关系:汽车销毁时,发动机也跟着销毁)
private Engine engine;

// 构造方法初始化发动机
public Car() {
this.engine = new Engine();
}

// 汽车启动:调用发动机的方法
public void startCar() {
engine.start();
System.out.println("汽车成功启动,可以行驶");
}

// 汽车停止:调用发动机的方法
public void stopCar() {
engine.stop();
System.out.println("汽车成功停止");
}
}

// 3. 轿车类(继承汽车,因为轿车是汽车的一种)
public class Sedan extends Car {
// 轿车的特有功能:自动泊车
public void autoParking() {
System.out.println("轿车正在自动泊车");
}
}

// 测试:组合和继承的正确使用
public class Test {
public static void main(String[] args) {
Sedan sedan = new Sedan();

// 调用继承自Car的方法(依赖组合的Engine)
sedan.startCar();
// 输出:
// 发动机启动,开始提供动力
// 汽车成功启动,可以行驶

// 调用Sedan的特有方法
sedan.autoParking(); // 输出“轿车正在自动泊车”

sedan.stopCar();
// 输出:
// 发动机停止,动力中断
// 汽车成功停止
}
}

五、接口隔离原则:接口要小而专,不要大而全

核心概念:这条原则是说 —— 不要设计一个 “万能接口”,把所有功能都塞进去,而是要把接口拆成多个 “专用接口”,让类只实现自己需要的接口。 这样能避免 “类实现了接口,但被迫重写不需要的方法”(比如空实现),也能减少接口变动的影响。

比如 “用户系统” 里,普通用户只需要 “登录、注册” 功能,管理员需要 “用户管理、权限管理” 功能。如果设计一个 “UserInterface” 包含所有 4 个方法,普通用户类就得空实现 “用户管理、权限管理”,这就很不合理;拆成两个接口后,各自实现需要的方法即可。

还是来看示范代码:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 1. 拆分成专用接口:普通用户接口
public interface NormalUserService {
void login(String username, String password); // 登录
void register(String username, String password); // 注册
}

// 2. 拆分成专用接口:管理员接口
public interface AdminService {
void manageUser(String userId); // 管理用户
void managePermission(String roleId); // 管理权限
}

// 3. 普通用户类:只实现自己需要的接口
public class NormalUser implements NormalUserService {
@Override
public void login(String username, String password) {
System.out.println(username + "登录成功");
}

@Override
public void register(String username, String password) {
System.out.println(username + "注册成功");
}
}

// 4. 管理员类:实现管理员接口,也可以实现普通用户接口(如果需要)
public class Admin implements NormalUserService, AdminService {
// 实现普通用户接口的方法
@Override
public void login(String username, String password) {
System.out.println("管理员" + username + "登录成功");
}

@Override
public void register(String username, String password) {
// 管理员可能不需要注册,或有特殊逻辑
System.out.println("管理员账号需通过审批注册");
}

// 实现管理员接口的方法
@Override
public void manageUser(String userId) {
System.out.println("成功管理用户:" + userId);
}

@Override
public void managePermission(String roleId) {
System.out.println("成功管理角色权限:" + roleId);
}
}

// 测试:每个类只处理自己需要的功能
public class Test {
public static void main(String[] args) {
NormalUser user = new NormalUser();
user.login("zhangsan", "123456"); // 输出“zhangsan登录成功”
user.register("lisi", "654321"); // 输出“lisi注册成功”

Admin admin = new Admin();
admin.login("admin", "admin123"); // 输出“管理员admin登录成功”
admin.manageUser("1001"); // 输出“成功管理用户:1001”
admin.managePermission("admin_role"); // 输出“成功管理角色权限:admin_role”
}
}

六、最少知识原则:一个类只和 “直接朋友” 通信,别和 “陌生人” 说话

核心概念:也叫 “迪米特法则”,核心是 “降低类之间的耦合”—— 一个类应该只和它的 “直接朋友”(比如成员变量、方法参数、返回值里的类)交互,不要主动去调用 “朋友的朋友” 的方法。这样能减少类之间的依赖,让系统更稳定。

比如 “老板要统计部门的员工数量”:老板的直接朋友是 “部门”,部门的直接朋友是 “员工”。如果老板直接去遍历部门里的员工列表,就是和 “员工”(陌生人)通信了;正确的做法是老板让部门自己统计人数,然后把结果返回给老板。

看下面的代码:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 1. 员工类(部门的直接朋友,老板的陌生人)
public class Employee {
// 员工的基本信息(不需要暴露给老板)
private String name;
private int age;

public Employee(String name, int age) {
this.name = name;
this.age = age;
}
}

// 2. 部门类(老板的直接朋友,员工的直接朋友)
public class Department {
// 部门里有员工列表
private List<Employee> employees;

public Department() {
employees = new ArrayList<>();
// 模拟添加3个员工
employees.add(new Employee("张三", 25));
employees.add(new Employee("李四", 28));
employees.add(new Employee("王五", 30));
}

// 部门自己提供“统计人数”的方法(对外隐藏员工列表)
public int getEmployeeCount() {
return employees.size();
}
}

// 3. 老板类(只和部门通信,不和员工通信)
public class Boss {
// 老板统计部门人数:只调用部门的方法(直接朋友)
public void countDepartmentEmployees(Department department) {
int count = department.getEmployeeCount();
System.out.println("当前部门员工数量:" + count);
}
}

// 测试:符合最少知识原则,耦合度低
public class Test {
public static void main(String[] args) {
Boss boss = new Boss();
Department department = new Department();

// 老板只和部门交互,不知道员工的存在
boss.countDepartmentEmployees(department); // 输出“当前部门员工数量:3”
}
}

写在最后:六大原则不是 “教条”,而是 “工具”

很多人学完这些原则后会陷入 “过度设计” 的误区 —— 为了凑齐原则,写了一堆复杂的接口和类,反而让代码更难维护。

其实这六大原则的核心目标是一致的:降低耦合、提高内聚、让代码更易扩展和维护。实际开发中,不需要强行遵守每一条,而是要根据场景灵活取舍(比如简单的工具类,用继承可能比组合更简单)。

记住:好的设计不是 “符合多少原则”,而是 “能解决当前问题,且能应对未来的合理变化”。