设计模式:GoF 经典 + SOLID 原则实战
学了单例工厂策略,面试能画 UML------但写代码时还是不知道该用哪个。直到有一次 code review,同事说我写的"策略模式"把 3 个 if-else 变成了 5 个类、2 个接口、1 个工厂------过度设计,为了"看起来高级"而不是"解决问题"。后来我用 SOLID 五原则做了一组反例对照表,每次写完代码过一遍,判断标准一下子清晰了。这篇文章把这张表给你。
阅读约 12 分钟 | 系列第 15/17 篇
一、设计模式概述
1.1 三大分类
| 类型 | 说明 | 典型模式 |
|---|---|---|
| 创建型 | 隐藏对象创建逻辑,降低客户端与具体类的耦合 | 单例、简单工厂、工厂方法、抽象工厂、建造者 |
| 结构型 | 处理类或对象的组合,获得更灵活的结构 | 适配器、装饰器、代理、组合、外观 |
| 行为型 | 描述类或对象之间的交互和通信模式 | 观察者、策略、模板方法、状态、发布-订阅 |
1.2 使用原则
- 理解模式的本质和适用场景比记住定义更重要
- 避免过度设计------拒绝不成熟的抽象同样重要
- 模式不是万能的,根据具体场景决定是否使用
二、SOLID 设计原则
设计模式是"术",SOLID 是"道"。不理解 SOLID 就很难判断一个模式用对了还是用错了。
2.1 单一职责原则(SRP)
定义:一个类或模块应有且仅有一个改变的理由。
核心价值:可读性(一眼看清这个类做什么)+ 可测试性(职责单一→Mock 简单)+ 可复用性(小而聚焦的组件更容易被复用)。
2.2 开放-封闭原则(OCP)
定义:对扩展开放,对修改封闭。应通过增加新代码来扩展功能,而非修改现有代码。
实现方式:抽象与接口(定义抽象类或接口描述共同行为)+ 设计模式(策略模式、装饰者模式、工厂方法模式天然支持 OCP)。
2.3 里氏替换原则(LSP)
定义:子类对象必须能够替换父类对象,而不破坏程序的正确性。
违反 LSP 的典型场景:
- 子类重写父类方法但不保持父类契约(如
Rectangle.setWidth/setHeightvsSquare------正方形不能独立设置宽高) - 子类方法抛出父类未声明的异常
- 子类修改了父类方法的语义
实践 :instanceof 判断子类类型往往暗示 LSP 被违反------如果调用方需要知道具体子类才能正确工作,说明子类不能完全替换父类。
2.4 接口隔离原则(ISP)
定义:客户端不应被迫依赖它不使用的方法。多个小接口优于一个大而全的接口。
java
// ❌ 违反 ISP:一个臃肿的接口
interface Worker {
void work();
void eat();
void sleep();
}
// ✅ 遵循 ISP:拆分为小接口
interface Workable { void work(); }
interface Eatable { void eat(); }
interface Sleepable { void sleep(); }
// Robot 只需实现 Workable,无需无意义的 eat() 和 sleep()
class Robot implements Workable { ... }
class Human implements Workable, Eatable, Sleepable { ... }
2.5 依赖倒置原则(DIP)
定义:高层模块不应依赖低层模块,两者都应依赖抽象。抽象不应依赖细节,细节应依赖抽象。
java
// ❌ 违反 DIP:高层直接依赖具体实现
class OrderService {
private MySQLOrderRepository repository = new MySQLOrderRepository();
}
// ✅ 遵循 DIP:依赖注入接口
class OrderService {
private final OrderRepository repository; // 接口
public OrderService(OrderRepository repository) { this.repository = repository; }
}
DIP 是 Spring IoC/DI 的理论根基------Spring 容器通过接口注入具体实现,使高层(OrderService)与底层(MySQLOrderRepository)解耦。当数据源从 MySQL 切换到 PostgreSQL,只需新建一个 Infrastructure 实现类,Domain 和 Application 层完全不感知。
三、创建型模式
3.1 单例模式
确保一个类只有一个实例,并提供全局访问点。四种写法:
饿汉模式(类加载时实例化,线程安全但无法延迟加载):
java
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() { return instance; }
}
懒汉模式(首次获取时实例化,需处理线程安全):
java
public class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) instance = new LazySingleton();
}
}
return instance;
}
}
枚举实现(推荐):天然线程安全 + 防反射攻击 + 防反序列化破坏。
java
public enum EnumSingleton {
INSTANCE;
public void doSomething() { ... }
}
// 使用:EnumSingleton.INSTANCE.doSomething();
3.2 简单工厂模式
定义一个类专门负责创建其他类的实例,根据参数不同返回不同类的实例。
java
public class OperationFactory {
public static Operation createOperation(String operate) {
switch (operate) {
case "+": return new OperationAdd();
case "-": return new OperationSub();
default: return null;
}
}
}
适用 :产品种类较少且不会频繁增加。缺点:新增产品需修改工厂类 switch-case,违反 OCP。
3.3 工厂方法模式
定义一个创建对象的接口,但由子类决定实例化哪个类。
java
public interface IOperationFactory {
Operation getOperation();
}
public class FactoryAdd implements IOperationFactory {
@Override
public Operation getOperation() { return new OperationAdd(); }
}
与简单工厂的关键区别:新增产品不需要修改现有工厂代码------符合 OCP。
3.4 抽象工厂模式
提供接口用于创建一系列相关对象(产品族),而不需要指定具体类。
与工厂方法的区别 :工厂方法关注创建单个对象 ,抽象工厂关注创建一系列相关对象。例如:跨平台 GUI------Windows 工厂生产 WindowsButton + WindowsTextField,Mac 工厂生产 MacButton + MacTextField。
3.5 建造者模式
将一个复杂对象的构建与表示分离,使同样的构建过程可以创建不同的表示。适用:构造函数参数过多(>4 个)、需要链式调用逐步设置属性。
java
Computer computer = new Computer.Builder()
.cpu("Intel i9")
.memory("32GB")
.storage("1TB SSD")
.build();
| 模式 | 核心问题 | 关键区别 |
|---|---|---|
| 简单工厂 | 创建哪种产品 | switch-case 集中创建 |
| 工厂方法 | 哪个工厂创建产品 | 延迟到子类决定,符合 OCP |
| 抽象工厂 | 创建一系列相关产品 | 产品族,工厂生产多个相关对象 |
| 建造者 | 如何一步步构建复杂对象 | 链式调用,参数可选 |
四、结构型模式
4.1 适配器模式
目的:将不兼容的接口转换为客户端期望的接口,使原本不兼容的类可以一起工作。
JDK 实例 :java.util.Arrays#asList()------将数组适配为 List 接口。InputStreamReader------将字节流 InputStream 适配为字符流 Reader。
4.2 装饰器模式
目的:动态地给对象添加额外职责,比继承更灵活。装饰器与目标对象实现同一接口,运行时"包装"目标并在调用前后插入额外行为。
JDK 实例 :BufferedReader(new FileReader(...))------为字符流添加缓冲能力。Java IO 流体系(InputStream → BufferedInputStream → DataInputStream)是装饰器模式的经典应用。
4.3 代理模式
目的:为另一个对象提供替身或占位符以控制访问------代理对象持有目标对象的引用并进行访问控制。
Spring 实例 :AOP 动态代理(JDK 动态代理 / CGLIB),@Transactional 注解的方法通过代理实现事务管理。
适配器 vs 装饰器 vs 代理:都是"包一层",区别在哪?
| 模式 | 目的 | 接口变化 | 关注点 |
|---|---|---|---|
| 适配器 | 接口转换------让不兼容的能一起工作 | 接口改变 | 兼容性 |
| 装饰器 | 功能增强------动态添加行为 | 接口不变 | 扩展性 |
| 代理 | 访问控制------控制对目标对象的访问 | 接口不变 | 控制 |
五、行为型模式
5.1 观察者模式
目的:定义一对多依赖关系,当被观察对象(Subject)状态改变时,所有依赖它的观察者(Observer)自动收到通知。
核心角色 :Subject(维护观察者列表 + 通知逻辑)、Observer(定义更新接口)。JDK 实例 :java.util.EventListener + java.util.Observer(已废弃但仍可理解模式)。
与发布-订阅的区别:观察者模式中 Subject 和 Observer 直接交互(知道彼此存在);发布-订阅通过 Broker/Topic 解耦------发布者和订阅者完全不知道彼此(如 MQ 的消息驱动)。
5.2 策略模式
目的:定义一系列算法,把它们分别封装,并使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。
java
// 策略接口
interface PaymentStrategy {
void pay(BigDecimal amount);
}
class AliPay implements PaymentStrategy { ... }
class WechatPay implements PaymentStrategy { ... }
// 上下文------持有策略引用,运行时切换
class PaymentContext {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy s) { this.strategy = s; }
public void executePay(BigDecimal amount) { strategy.pay(amount); }
}
JDK 实例 :Comparator<T> 接口------不同的排序策略通过 Collections.sort(list, comparator) 运行时注入。Thread 的 Runnable 也是策略模式的体现。
策略 vs 状态 :策略模式是客户端主动切换 算法("我要用支付宝付款"),状态模式是状态自动驱动行为变更(交通灯:绿→黄→红自动流转)。
5.3 模板方法模式
目的:定义算法骨架,将某些步骤延迟到子类实现。子类可以不改变算法结构而重定义特定步骤。
Spring 实例 :JdbcTemplate 的 query() 方法------获取连接→执行查询→映射结果→释放连接是固定骨架,SQL 和行映射器(RowMapper)由调用方提供。AbstractApplicationContext.refresh() 定义容器启动的完整步骤,各子类实现特定步骤。
六、Spring 框架中的设计模式全景
Spring 是目前 Java 生态中应用设计模式最多的框架之一。理解以下映射关系,面试时能清晰展示"我知道它用了什么模式,为什么会这样设计":
| 设计模式 | Spring 中的应用 | 对应组件 |
|---|---|---|
| 单例 | Bean 默认 singleton scope | @Service、@Repository 默认单例 |
| 工厂方法 | BeanFactory / ApplicationContext |
context.getBean("userService") |
| 代理 | AOP 事务管理 | @Transactional 通过代理实现 |
| 模板方法 | 数据访问模板 | JdbcTemplate、RestTemplate |
| 观察者 | Spring Event | @EventListener + ApplicationEventPublisher |
| 策略 | 资源加载 | ResourceLoader 的不同实现 |
| 适配器 | MVC 处理器适配 | HandlerAdapter 适配不同 Controller |
| 依赖注入(DIP 的实践) | IoC 容器 | @Autowired 注入接口,运行时绑定实现 |
核心要点回顾
SOLID 五原则是设计模式的"道"------SRP 让类的职责聚焦、OCP 让系统对扩展开放对修改封闭、LSP 保证子类可以无缝替换父类、ISP 避免臃肿接口强制实现无用方法、DIP 通过"依赖抽象而非具体"成为 Spring IoC 的理论根基。
创建型 四种模式各有分工:单例(全局唯一实例,枚举为最优实现)、工厂三兄弟(简单工厂集中创建→工厂方法符合 OCP→抽象工厂创建产品族)、建造者(链式构建复杂对象)。结构型三种"包装"模式目的截然不同:适配器改变接口保证兼容、装饰器不变接口增强功能、代理控制访问------Java IO 流体系是装饰器的经典应用,Spring AOP 通过代理实现事务管理。
行为型关注对象间的交互:观察者模式通过 Subject→Observer 一对多通知、策略模式在运行时切换算法消除 if-else(JDK 的 Comparator 是典范)、模板方法定义算法骨架让子类填空(JdbcTemplate 是 Spring 中的经典案例)。
设计模式不是学得越多越好------关键是 SOLID 五原则那杆秤,知道"什么时候不用"。收藏这份反例对照表,每次 code review 时翻出来对照。
上一篇 :《容器与K8s》 | 下一篇 :《分库分表实战》 系列合集 :掘金Java合集