【壹】设计模式综述:面试的基石
在软件工程中,设计模式(Design Patterns)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。在技术面试中,面试常考的设计者模式往往占据了相当大的比重,尤其是对于中高级Java、前端及架构师岗位。
设计模式并非具体的代码,而是一种思想,一种解决特定问题的模板。面试官考察设计模式,不仅仅是看你是否背熟了代码,更是考察你是否有良好的代码架构意识,是否能在实际业务中灵活运用这些模式来降低耦合、提高扩展性。
⚡ 创建型模式
关注对象的创建机制,改变创建方式可以更好地控制对象的创建过程。包括单例、工厂、抽象工厂、建造者、原型模式。
⚙️ 结构型模式
关注类与对象的组合,通过继承类和组合对象来获得更大的结构。包括适配器、桥接、组合、装饰、外观、享元、代理模式。
🔄 行为型模式
关注对象之间的通信,以及对象如何分配职责。包括模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器。
【贰】单例模式:唯一实例的艺术
单例模式(Singleton Pattern)是面试中出现频率极高的模式,尤其是其各种实现方式的线程安全性讨论。单例模式确保一个类只有一个实例,并提供一个全局访问点。
1. 常见实现方式对比
| 实现方式 | 线程安全 | 延迟加载 | 优缺点分析 |
|---|---|---|---|
| 饿汉式 | 是 | 否 | 简单,类加载即创建,浪费内存,但线程安全。 |
| 懒汉式(同步方法) | 是 | 是 | 效率低,每次调用都同步,不推荐。 |
| 懒汉式(双重检查锁) | 是 | 是 | 推荐。利用volatile防止指令重排,兼顾性能与安全。 |
| 静态内部类 | 是 | 是 | 推荐。利用类加载机制保证线程安全,结构优雅。 |
| 枚举单例 | 是 | 否 | 最推荐。防止反射攻击,防止序列化破坏,简洁高效。 |
2. 代码示例:双重检查锁(DCL)
public class Singleton {
// volatile 防止指令重排
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3. 面试陷阱
- 反射攻击:通过反射调用私有构造函数创建新实例。解决方案:在构造函数中抛出异常,或使用枚举。
- 序列化破坏:反序列化时会创建新对象。解决方案:实现readResolve方法,或使用枚举。
【叁】工厂模式:解耦的利器
工厂模式(Factory Pattern)主要用于创建对象,它提供了一种创建对象的最佳方式。在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
1. 简单工厂模式
虽然简单工厂模式不属于23种设计模式之一(因为它违背了开闭原则),但在面试中常作为切入点。它通过一个工厂类根据传入的参数决定创建哪种产品类的实例。
2. 工厂方法模式
定义一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法模式让类的实例化推迟到子类。
3. 抽象工厂模式
提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。适用于创建一系列相关或依赖的对象。
简单工厂示例
public class SimpleFactory {
public static Product createProduct(String type) {
if ("A".equals(type)) {
return new ProductA();
} else if ("B".equals(type)) {
return new ProductB();
}
return null;
}
}
缺点:增加新产品需要修改工厂类代码,违反开闭原则。
工厂方法示例
public interface ProductFactory {
Product create();
}
public class ConcreteFactoryA implements ProductFactory {
public Product create() {
return new ProductA();
}
}
优点:符合开闭原则,增加新产品只需增加新工厂类。
抽象工厂示例
抽象工厂模式通常用于生产一组相关对象,如UI组件库(Windows风格 vs Mac风格)。每个工厂负责生产一套完整的组件。
public interface AbstractFactory {
Button createButton();
Checkbox createCheckbox();
}
【肆】观察者模式:事件驱动的核心
观察者模式(Observer Pattern)定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。这是Java中非常常用的模式,尤其在Spring框架的事件机制、前端的事件总线中广泛应用。
1. 核心角色
- Subject(目标):知道它的观察者,提供注册和删除观察者的接口。
- Observer(观察者):为那些在目标状态发生改变时需要获得通知的对象定义一个更新接口。
2. 应用场景
在面试中,当被问到“如何实现消息队列”或“如何实现事件通知”时,观察者模式是一个很好的切入点。例如,Spring的ApplicationContextEvent就是基于观察者模式实现的。
3. 代码示例
// 主题接口
public interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
// 观察者接口
public interface Observer {
void update(String message);
}
【伍】策略模式:消除条件判断
策略模式(Strategy Pattern)定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。它解决了多重条件判断语句(if-else或switch-case)带来的代码臃肿问题。
1. 典型场景
电商平台的优惠计算、支付渠道选择、排序算法选择等。如果不用策略模式,代码中会充斥着大量的if-else,难以维护。
2. 实现思路
定义策略接口,每个具体策略实现该接口。上下文类持有策略接口的引用,调用时根据条件注入不同的策略实现。
优点
符合开闭原则,易于扩展新策略;避免使用多重条件判断语句;算法可以互相替换。
缺点
客户端必须知道所有的策略类,并决定使用哪一个;产生大量的策略类对象。
【陆】面试高频问答:面试官在想什么?
开闭原则(Open-Closed Principle)是面向对象设计中最基础的原则。软件实体应当对扩展开放,对修改关闭。也就是说,当需求变化时,我们应该通过添加新代码来扩展功能,而不是修改现有代码。设计模式的核心目的之一就是实现开闭原则。
接口支持多重实现,而抽象类只能单继承。接口定义契约,抽象类定义类型。在微服务架构中,接口更利于解耦和测试。但在需要共享代码时,抽象类更有优势。面试中通常建议优先使用接口,除非需要共享状态或代码。
装饰器模式旨在动态地给对象添加职责,强调功能的增强;代理模式旨在控制对对象的访问,强调权限或延迟加载。虽然两者结构相似,但意图不同。装饰器通常透明地增强功能,而代理通常对客户隐藏真实对象。
不要为了用模式而用模式。当发现代码中存在重复的逻辑、难以扩展的条件判断、或紧耦合的依赖时,考虑引入设计模式。例如,当支付渠道增加时,使用策略模式;当需要创建复杂对象时,使用建造者模式。设计模式是工具,解决实际问题才是目的。