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

#中间件#面试八股

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

结论先行:常说的"7 大原则"是 SOLID 五条(单一职责、开闭、里氏替换、接口隔离、依赖倒置)再加两条补充:迪米特法则与合成复用原则。它们的共同目标只有一个——把依赖指向抽象、把变化关进边界。原则之间会互相拉扯,所以它是权衡工具,不是考核指标。

1. 一张表过完 7 条

缩写原则一句话违反时的典型信号
SRP单一职责(Single Responsibility)一个类只有一个"变化的理由"类名带 And/Manager,方法两百行
OCP开闭(Open-Closed)对扩展开放,对修改关闭每加一个渠道就改 switch
LSP里氏替换(Liskov Substitution)子类必须能替换父类子类实现里抛 UnsupportedOperationException
ISP接口隔离(Interface Segregation)接口要小而专实现类里一堆空方法
DIP依赖倒置(Dependency Inversion)依赖抽象,不依赖实现业务代码里 new 具体实现
LoD迪米特(Law of Demeter)只和直接朋友说话a.getB().getC().doWork()
CRP合成复用(Composite Reuse)优先组合,而非继承为复用代码而继承,继承链四层

2. 依赖倒置:把箭头反过来

   违反 DIP:OrderService ──new──► MysqlOrderDao(编译期焊死)
   遵循 DIP:OrderService ──► OrderRepo(接口)◄──实现── MysqlOrderDao
                                                    ◄── RedisOrderDao(换存储不改业务)

上层模块不再依赖下层实现,两者都依赖抽象——依赖方向被"倒置"了。

// 违反:业务类把具体存储焊死,换库要改业务
class OrderService {
    private final MysqlOrderDao dao = new MysqlOrderDao();
}

// 遵循:面向接口 + 构造注入
interface OrderRepo {
    void save(Order o);
    Order findById(long id);
}

class OrderService {
    private final OrderRepo repo;          // 只认抽象

    OrderService(OrderRepo repo) {         // 由外部注入
        this.repo = repo;
    }

    void place(Order o) {
        repo.save(o);
    }
}

Spring 的构造器注入本质上就是 DIP 的落地:容器负责 new,业务只声明"我需要一个 OrderRepo"。

3. 里氏替换:契约比继承重要

子类型要满足父类型的行为契约:前置条件不能加强、后置条件不能削弱、不抛父类没声明的受检异常。

class Rectangle {
    protected int w, h;
    void setW(int w) { this.w = w; }
    void setH(int h) { this.h = h; }
    int area() { return w * h; }
}

class Square extends Rectangle {
    @Override void setW(int w) { this.w = w; this.h = w; }  // 偷偷改高度
    @Override void setH(int h) { this.w = h; this.h = h; }
}

// 调用方按 Rectangle 契约写:
Rectangle r = new Square();
r.setW(5); r.setH(4);      // 期望 area=20,实际 16 —— 契约被破坏

判断口径很简单:使用者不知道具体子类时,行为是否仍符合预期。

4. 接口隔离:别逼实现类写空方法

// 违反 ISP:胖接口把不相干能力绑在一起,机器人也得实现 eat()
interface Worker { void work(); void eat(); }

// 遵循:按能力拆分,谁需要谁实现
interface Workable { void work(); }
interface Eatable  { void eat(); }

class Robot implements Workable { public void work() { /* ... */ } }

5. 迪米特:只和直接朋友说话

对象的方法里,只应调用:自身、自己的字段、入参、自己创建的对象。典型反例是"链式导航":

// 违反:Service 顺着依赖爬了三层,任一环改结构都要跟着改
double price = order.getUser().getLevel().getDiscount().getRate() * order.getAmount();

// 遵循:让 order 自己算,内部结构对外不可见
double price = order.discountedPrice();

口诀是 Tell, Don't Ask(告诉它做什么,别问它内部长什么样)。

6. 合成复用:继承是强耦合

维度继承(Inheritance)组合(Composition)
绑定时机编译期写死运行时注入
耦合度强,父类改动会波及子类(脆弱基类)弱,只依赖接口
可否替换不能随时换实现
复用粒度整个类按需挑选能力
典型误用class ArrayList01 extends ArrayListnew ArrayList<>() 作为字段

只有当"子类确实是父类的一种"(is-a)时才继承;只是"想复用代码"(has-a)就用组合。

7. 原则打架时怎么选

  • SRP 的"职责"粒度取决于变化频率:同一个类里的两个方法如果总是一起改,就不必拆。
  • DIP 会引入接口,接口多了导航成本变高;边界清晰的核心域值得,一次性脚本不值得。
  • OCP 与 YAGNI 冲突时,以"变化已经发生"为准,不要为想象中的扩展预留插槽。

8. 常见追问

Q:开闭原则是不是"永远不改代码"? 不是。它说的是稳定的骨架不改,扩展靠新增。真要改的是"扩展点"本身的设计,那属于重构,不违背 OCP。

Q:7 条原则里哪条最重要? 工程上最常被违反、收益也最大的是 DIP:它直接决定单测能否替换掉数据库和外部服务。

小结

  • 7 条 = SOLID 五条 + 迪米特 + 合成复用,目标都是依赖抽象、隔离变化。
  • 识别违反比背定义有用:switch 越加越长、空方法一堆、new 写在业务里、链式导航。
  • 原则互相制约,落地时按变化频率和团队成本权衡,不要当 KPI 刷。
笔记加载中…