设计模式的 7 大基本原则有哪些?


面试考察点

  1. 基础掌握度:面试官不仅仅是想听你背出 7 个原则的名字,更是想知道你能否准确说出每个原则的核心约束是什么,以及它们之间的优先级关系。

  2. 实践意识:光背定义没用,面试官会追问你 "在实际项目中是怎么体现这些原则的",考察你是否真正在用这些思想指导编码。

  3. 设计思维:这 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 实际需求:有时候子类确实需要 "覆盖" 父类的行为

所以这些原则不是法律,而是 指导方针。核心原则只有一个:高内聚、低耦合。所有原则都是为了服务于这个目标。别死板地每条都严格遵守,根据实际场景做取舍。

面试高频追问

  1. 追问:开闭原则和其他 6 个原则是什么关系?

    • 开闭原则是 "目标",其他 6 个是 "手段"。遵循单一职责可以让类更容易扩展;遵循依赖倒置可以让扩展不依赖具体实现;遵循里氏替换可以保证多态的正确性……它们共同支撑起开闭原则。
  2. 追问:单一职责原则和接口隔离原则有什么区别?

    • 单一职责约束的是 ——一个类只有一个变化的原因;接口隔离约束的是 接口——一个接口只定义一组相关的方法。前者从业务角度划分,后者从使用者角度划分。
  3. 追问:你在项目中是怎么应用这些原则的?

    • 这个建议提前准备。比如:"我在设计支付模块时,用依赖倒置原则定义了 PaymentStrategy 接口,具体支付方式(支付宝、微信)作为实现类,通过 Spring 的 DI 注入。这样每次新接入一种支付渠道,只需要新增一个实现类,完全符合开闭原则。"

常见面试变体

  • "SOLID 原则分别是什么?"
  • "开闭原则怎么理解?实际项目中怎么体现?"
  • "里氏替换原则能不能举个例子说明?"
  • "合成复用原则和继承有什么区别?"

记忆口诀

7 大原则开单里依接迪合

  • :开闭原则(总纲)
  • :单一职责
  • :里氏替换
  • :依赖倒置
  • :接口隔离
  • :迪米特
  • :合成复用

谐音记忆:"开单里,依接迪合" —— 开张单子里,按照约定合并(想象开了一家店,签了单子,按约定把账合并)。

总结

7 大原则的核心就是 高内聚、低耦合。开闭原则是总目标,其余 6 个从类、继承、依赖、接口、交互、复用 6 个维度提供具体约束。面试时别逐条背定义,重点说清楚 每个原则解决什么问题、在实际项目中怎么体现,这才是面试官想听的。