设计模式的 7 大基本原则有哪些?
面试考察点
-
基础掌握度:面试官不仅仅是想听你背出 7 个原则的名字,更是想知道你能否准确说出每个原则的核心约束是什么,以及它们之间的优先级关系。
-
实践意识:光背定义没用,面试官会追问你 "在实际项目中是怎么体现这些原则的",考察你是否真正在用这些思想指导编码。
-
设计思维:这 7 个原则之间存在相互关联甚至看似矛盾的地方(比如接口隔离和单一职责的区别),能不能把它们当成一个整体来理解,体现的是设计思维的高度。
核心答案
设计模式的 7 大基本原则 如下:
| 序号 | 原则名称 | 英文缩写 | 一句话概括 |
|---|---|---|---|
| 1 | 开闭原则 | OCP | 对扩展开放,对修改关闭 |
| 2 | 单一职责原则 | SRP | 一个类只做一个事 |
| 3 | 里氏替换原则 | LSP | 子类必须能替换父类 |
| 4 | 依赖倒置原则 | DIP | 面向接口编程,别依赖具体实现 |
| 5 | 接口隔离原则 | ISP | 接口要小而专,别搞大杂烩 |
| 6 | 迪米特法则 | LoD | 最少知道原则,少打听闲事 |
| 7 | 合成复用原则 | CRP | 多用组合,少用继承 |
其中前 5 个合称 SOLID 原则(取每个缩写的首字母)。开闭原则 是总纲,其他 6 个原则都是为了实现开闭原则的手段。
深度解析
一、原则关系全景图
先看一张关系图,把 7 个原则的定位和关系理清楚:
上图展示了 7 大原则的整体关系。可以这样理解:
- 开闭原则是总目标,它是所有设计原则的 "北极星",其他原则都是实现它的具体手段
- 单一职责、里氏替换、依赖倒置 分别从 类的划分、继承的使用、依赖的方向 三个维度来约束设计
- 接口隔离、迪米特法则、合成复用 分别从 接口的粒度、对象间交互、复用方式 三个维度进一步细化约束
理解了这张关系图,你就不会觉得 7 个原则是一盘散沙了。
二、逐个击破
1. 开闭原则(OCP)—— 总纲
对扩展开放,对修改关闭。
这是最核心的原则。什么意思?当需求变化时,你应该通过新增代码来扩展功能,而不是修改已有代码。
// 反例:每次新增折扣类型都要改这个方法(修改老代码 💣)
public class DiscountCalculator {
public double calculate(String type, double price) {
if ("VIP".equals(type)) {
return price * 0.8;
} else if ("SVIP".equals(type)) {
return price * 0.7;
}
// 新加一种会员等级?又得改这里...
return price;
}
}
// 正例:通过扩展来实现,不修改老代码 ✅
public interface DiscountStrategy {
double calculate(double price);
}
public class VipDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price * 0.8;
}
}
public class SvipDiscount implements DiscountStrategy {
@Override
public double calculate(double price) {
return price * 0.7;
}
}
// 新加会员等级?写个新类就行,老代码一行不动
2. 单一职责原则(SRP)
一个类应该只有一个引起它变化的原因。
别把一个类塞成 "全能型选手"。一个类职责越多,改一个功能影响其他功能的风险就越大。
// 反例:一个类既管用户数据又管发送邮件,职责混乱 💣
public class UserService {
public void register(User user) {
// 保存用户
saveUser(user);
// 发送欢迎邮件
sendEmail(user.getEmail(), "欢迎注册");
// 记录日志
writeLog("新用户注册:" + user.getName());
}
}
// 正例:职责拆分,各司其职 ✅
public class UserService {
private EmailService emailService;
private LogService logService;
public void register(User user) {
saveUser(user);
emailService.sendWelcome(user.getEmail());
logService.logRegister(user.getName());
}
}
但也要注意别过度拆分。如果每个方法都抽成一个类,类爆炸了反而更难维护。实际项目中根据业务边界来划分,不能太粗也不能太细。
3. 里氏替换原则(LSP)
所有引用父类的地方必须能透明地使用其子类对象。
说白了就是:继承要靠谱。子类不能破坏父类定义的行为契约。这个原则很多人理解不深,但它特别重要——违反它,多态就废了。
// 经典反例:正方形继承矩形 💣
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int getArea() { return width * height; }
}
public class Square extends Rectangle {
// 正方形长宽必须相等,所以两个 setter 都改成一样的
@Override
public void setWidth(int w) {
this.width = w;
this.height = w; // 破坏了父类的行为契约!
}
@Override
public void setHeight(int h) {
this.width = h;
this.height = h; // 同样破坏了
}
}
// 用 Rectangle 的地方换成 Square,结果就不对了
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(3);
System.out.println(r.getArea()); // 期望 15,实际 9 💥
这个例子特别经典。正方形 "是一种" 矩形在数学上成立,但在代码层面,正方形不能替换矩形,因为它的行为不一样。继承关系不是看现实世界的分类,而是看 行为是否兼容。
4. 依赖倒置原则(DIP)
高层模块不应该依赖低层模块,两者都应该依赖其抽象。
翻译成人话:面向接口编程,别直接依赖具体实现类。 这是 Spring 能火起来的核心哲学之一。
// 反例:高层直接依赖低层具体实现,耦合度高 💣
public class OrderService {
private MySqlOrderDao orderDao = new MySqlOrderDao(); // 直接依赖具体类
public void createOrder(Order order) {
orderDao.insert(order);
}
}
// 正例:双方都依赖抽象(接口),解耦 ✅
public class OrderService {
private OrderDao orderDao; // 依赖接口,不依赖具体实现
public OrderService(OrderDao orderDao) {
this.orderDao = orderDao;
}
public void createOrder(Order order) {
orderDao.insert(order);
}
}
// 现在想换 MongoDB?传个 MongoOrderDao 进去就行,OrderService 一行不改
Spring 的依赖注入(DI)就是依赖倒置原则的最佳实践——你只管声明要什么接口,框架帮你注入具体实现。
5. 接口隔离原则(ISP)
客户端不应该被迫依赖它不使用的方法。
接口要 小而专,别搞一个 "万能接口" 让实现类被迫实现一堆用不到的方法。
// 反例:一个臃肿的大接口 💣
public interface Animal {
void eat();
void fly(); // 狗不会飞,但被迫实现
void swim(); // 鸟不会游泳,但被迫实现
}
public class Dog implements Animal {
@Override public void eat() { /* 正常 */ }
@Override public void fly() { throw new UnsupportedOperationException(); }
@Override public void swim() { /* 狗刨式 */ }
}
// 正例:按能力拆分为小接口 ✅
public interface Eatable { void eat(); }
public interface Flyable { void fly(); }
public interface Swimable { void swim(); }
public class Dog implements Eatable, Swimable {
@Override public void eat() { /* 正常 */ }
@Override public void swim() { /* 狗刨式 */ }
// 不需要 fly,干脆不实现 Flyable,干净!
}
public class Bird implements Eatable, Flyable {
@Override public void eat() { /* 正常 */ }
@Override public void fly() { /* 正常 */ }
}
和单一职责的区别?单一职责是对类的约束(一个类只做一件事),接口隔离是对接口的约束(一个接口只定义一组相关方法)。两者角度不同,但目的一致——都是为了降低耦合。
6. 迪米特法则(LoD)/ 最少知道原则
一个对象应该对其他对象保持最少的了解。
别让一个类 "知道" 太多别的类的内部细节。知道的越多,耦合越紧,改起来越痛苦。
// 反例:过度暴露内部细节 💣
public class Customer {
private Wallet wallet = new Wallet();
public Wallet getWallet() {
return wallet; // 直接把钱包交出去了
}
}
// 调用方需要知道 Wallet 的内部结构
public class Shop {
public void charge(Customer customer, int amount) {
Wallet wallet = customer.getWallet();
if (wallet.getMoney() >= amount) {
wallet.decreaseMoney(amount); // 操作别人的钱包,知道太多了
}
}
}
// 正例:封装内部细节,只暴露必要的方法 ✅
public class Customer {
private Wallet wallet = new Wallet();
// 只暴露 "付款" 这个行为,不暴露钱包
public boolean pay(int amount) {
if (wallet.getMoney() >= amount) {
wallet.decreaseMoney(amount);
return true;
}
return false;
}
}
public class Shop {
public void charge(Customer customer, int amount) {
customer.pay(amount); // 不关心钱包怎么实现的
}
}
实际开发中常见的体现:DTO 只传必要字段、Service 层不暴露 DAO 实现细节、Controller 不直接操作 Repository。
7. 合成复用原则(CRP)
优先使用对象组合,而非继承来达到复用的目的。
继承是强耦合——子类依赖父类的实现细节,父类一改子类可能就挂了。组合是松耦合——通过注入依赖来获取功能,随时可以替换。
// 反例:用继承来复用数据库操作 💣
public class BaseDao {
protected Connection getConnection() { /* 获取连接 */ }
}
public class UserService extends BaseDao {
// 为了复用 getConnection 而继承了 BaseDao
// 但 UserService 和 BaseDao 在概念上根本没有 "is-a" 关系
}
// 正例:用组合来复用 ✅
public class UserService {
private DatabaseHelper dbHelper; // 组合:注入进来
public UserService(DatabaseHelper dbHelper) {
this.dbHelper = dbHelper;
}
public void createUser(User user) {
Connection conn = dbHelper.getConnection();
// ...
}
}
Java 里 String 不可变就是一个好例子——它没有通过继承来获得各种能力,而是通过组合(final char[]、不可变设计)来保证安全。
三、原则之间的优先级和平衡
实际开发中,这 7 个原则经常会 "打架"。比如:
- 单一职责 vs 合成复用:类拆得太细,组合关系就特别多,代码反而复杂了
- 接口隔离 vs 迪米特法则:接口拆得太小,客户端要依赖一堆小接口,也不简洁
- 里氏替换 vs 实际需求:有时候子类确实需要 "覆盖" 父类的行为
所以这些原则不是法律,而是 指导方针。核心原则只有一个:高内聚、低耦合。所有原则都是为了服务于这个目标。别死板地每条都严格遵守,根据实际场景做取舍。
面试高频追问
-
追问:开闭原则和其他 6 个原则是什么关系?
- 开闭原则是 "目标",其他 6 个是 "手段"。遵循单一职责可以让类更容易扩展;遵循依赖倒置可以让扩展不依赖具体实现;遵循里氏替换可以保证多态的正确性……它们共同支撑起开闭原则。
-
追问:单一职责原则和接口隔离原则有什么区别?
- 单一职责约束的是 类——一个类只有一个变化的原因;接口隔离约束的是 接口——一个接口只定义一组相关的方法。前者从业务角度划分,后者从使用者角度划分。
-
追问:你在项目中是怎么应用这些原则的?
- 这个建议提前准备。比如:"我在设计支付模块时,用依赖倒置原则定义了
PaymentStrategy接口,具体支付方式(支付宝、微信)作为实现类,通过 Spring 的 DI 注入。这样每次新接入一种支付渠道,只需要新增一个实现类,完全符合开闭原则。"
- 这个建议提前准备。比如:"我在设计支付模块时,用依赖倒置原则定义了
常见面试变体
- "SOLID 原则分别是什么?"
- "开闭原则怎么理解?实际项目中怎么体现?"
- "里氏替换原则能不能举个例子说明?"
- "合成复用原则和继承有什么区别?"
记忆口诀
7 大原则:开单里依接迪合
- 开:开闭原则(总纲)
- 单:单一职责
- 里:里氏替换
- 依:依赖倒置
- 接:接口隔离
- 迪:迪米特
- 合:合成复用
谐音记忆:"开单里,依接迪合" —— 开张单子里,按照约定合并(想象开了一家店,签了单子,按约定把账合并)。
总结
7 大原则的核心就是 高内聚、低耦合。开闭原则是总目标,其余 6 个从类、继承、依赖、接口、交互、复用 6 个维度提供具体约束。面试时别逐条背定义,重点说清楚 每个原则解决什么问题、在实际项目中怎么体现,这才是面试官想听的。
