设计模式
1. 创建者模式
1.单例模式(Singleton Pattern)
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
1.1 背景与动机
假设我们有一个全局配置类Config
,该类会保存一些系统级别的配置参数。每次需要访问这些配置时,我们都会创建一个新的Config
对象。这样的问题是,每次获取配置对象时,都会浪费内存并且产生额外的性能开销。而且,多个实例的创建也会导致程序出现不一致的行为,特别是在多线程环境下。
public class Config {
private String setting;
public Config(String setting) {
this.setting = setting;
}
public String getSetting() {
return setting;
}
}
// 使用
public class App {
public static void main(String[] args) {
Config config1 = new Config("Setting 1");
Config config2 = new Config("Setting 2");
System.out.println(config1.getSetting());
System.out.println(config2.getSetting());
}
}
问题:每次创建Config
实例都需要额外的内存开销,而且它们并没有共享状态,导致配置对象的重复创建。
单例模式的核心思想是确保某个类只有一个实例,并提供一个全局访问点来获取该实例。这样可以避免重复创建对象,保证了全局配置的一致性。
1.2 模式定义与结构
单例模式确保某个类只有一个实例,并提供全局访问点。
UML 类图:
+------------------+
| Singleton |
+------------------+
| - instance: Singleton |
| + getInstance(): Singleton |
+------------------+
角色说明:
- Singleton(单例类):类本身维护唯一的实例,并提供一个获取实例的方法。
1.3 单例模式的实现
1. 饿汉式
类加载就会导致该单实例对象被创建。
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
// 私有构造函数
}
public static Singleton getInstance() {
return instance;
}
}
public class Main {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // true
}
}
/**
*饿汉式
*
在静态代码块中创建该类对象
*/
public class Singleton {
//私有构造方法
private Singleton () ()
//在成员位置创建该类的对象
private static Singleton instance;
static {
instance = new Singleton () ;
//对外提供静态方法获取该对象
public static Singleton getInstance ()
}
该方式在成员位置声明SingLeton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。所以存在内存浪费问题。
2. 懒汉式
类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建。
public class Singleton {
private static Singleton instance;
private Singleton() {
// 私有构造函数,防止外部实例化
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Main {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // true
}
}
//懒汉式(线程安全)
public class Singleton {
private static Singleton instance;
private Singleton() {
// 私有构造函数
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Main {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2); // true
}
}
对于 getInstance ()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式
public class Singleton {
//私有构造方法
private Singleton () {}
//在多线程的情况下,可能会出现空指针问题,出现问题的原因是J从在买例化对象的时候会进行优化和指令車排序操作
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getinstance () {
//第一次判断,如果instance不为nul1.不进入抢锁阶段,直接返回实例
if (instance = null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是杏为nu11
if (instance == null) {
instance = new Singleton () ;
}
return instance;
}
}
/*
*
** 静态内部类方式
*/
public class Singleton {
//私有构造方法
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类单例模式中实例由内部类创建,由于 JVA 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被「static修饰,保证只被实例化一次,并且严格保证实例化顺序。
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
1.4 设计原则分析
- 单一职责原则 (SRP):单例模式的实例化与业务逻辑是分开的,实例只负责提供一个全局访问点。
- 开闭原则 (OCP):单例类一旦创建,不需要修改代码,新的实例化方式可以通过扩展来实现。
- 依赖倒置原则 (DIP):客户端依赖于
Singleton
的抽象,而不直接依赖于具体的实例化过程。
1.5 实际应用场景
-
日志管理:日志类通常只需要一个实例来管理日志的输出,避免重复创建日志实例。
-
配置类:很多系统的配置参数只需要加载一次,单例模式可以确保配置类只有一个实例。
-
数据库连接池:在高并发的系统中,单例模式可以用来创建唯一的数据库连接池,避免每次请求都创建新连接。
1.6 开源示例
Spring Framework:Spring的ApplicationContext
通常是单例的,它维护了整个应用的配置和资源。每次获取ApplicationContext
时,都是获取同一个实例。
public class SingletonContext {
private static SingletonContext instance;
private SingletonContext() {
// 初始化配置
}
public static SingletonContext getInstance() {
if (instance == null) {
synchronized (SingletonContext.class) {
if (instance == null) {
instance = new SingletonContext();
}
}
}
return instance;
}
}
H2 Database:H2数据库内的连接管理器采用了单例模式,确保整个应用只有一个数据库连接管理器实例。
public class H2ConnectionManager {
private static H2ConnectionManager instance;
private H2ConnectionManager() {
// 初始化连接
}
public static synchronized H2ConnectionManager getInstance() {
if (instance == null) {
instance = new H2ConnectionManager();
}
return instance;
}
}
1.7 优缺点分析
优点:
- 全局唯一实例,节省内存资源。
- 可以控制全局状态,保证数据一致性。
缺点:
- 在多线程环境下,如果实现不当,可能导致线程安全问题。
- 可能会造成类加载时的性能开销,尤其在懒汉式中。
1.8 破坏单例模式
//通过反射破坏单例模式
public static void main(String[] args) throws Exception{
Class<Singleton> singletonClass = Singleton.class;
Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton1= (Singleton)constructor.newInstance();
Singleton singleton2= (Singleton)constructor.newInstance();
System.out.println(singleton1 == singleton2);
}
//通过序列化和反序列化破坏单例
public static void main(String[] args) throws Exception{
Singleton instance = Singleton.getInstance();
String jsonString = JSON.toJSONString(instance);
Singleton singleton = JSON.parseObject(jsonString, Singleton.class);
System.out.println(instance == singleton);
}
1.9 问题反思与解答
问题:如何处理多线程环境下的单例模式实现?
- 反思:在多线程环境下,懒汉式的
getInstance()
方法可能会导致多个实例的创建。解决方法包括使用双重检查锁定(DCL)或volatile
关键字,确保只有一个线程能够创建实例。
1.10 适用性分析
适用场景:
- 全局共享的对象:需要在整个应用中只存在一个实例的情况,如数据库连接池、线程池、配置管理器等。
- 控制对象创建次数:当某个对象的创建开销较大或者其状态需要共享时,单例模式非常适合。
不适用场景:
- 如果类的实例会频繁改变,或者不需要全局唯一实例时,单例模式就不适用。
- 在需要频繁创建新实例的场景下,使用单例模式会降低系统的灵活性。
1.11 常见误区
-
误区1:滥用单例模式:很多开发者习惯性地使用单例模式,而不是根据实际情况决定是否使用。特别是在功能模块并不需要共享状态时,使用单例会导致不必要的全局共享。
-
误区2:不考虑线程安全:懒汉式实现未考虑多线程环境的同步问题,可能会导致多个实例的创建。应使用
volatile
关键字或双重检查锁定来保证线程安全。 -
误区3:单例类不能被扩展:单例模式本身并不禁止扩展,但很多开发者在设计时忽略了扩展性。如果将单例类设计得过于紧耦合,可能会限制系统的扩展性。
1.12 模式变种
-
双重检查锁定(DCL):双重检查锁定是懒汉式单例模式的一种优化方法,通过减少同步块的范围,提升性能。在多线程环境下可以保证只有第一次调用时需要同步,避免多次加锁的性能开销。
-
枚举单例:Java中通过
enum
可以非常优雅地实现单例模式。枚举单例是线程安全的,并且可以防止反射破坏单例。public enum Singleton { INSTANCE; public void someMethod() { // do something } }
1.13 性能考虑
-
内存开销:单例模式通过保证只有一个实例来减少内存开销。但在实现时,如果采用了懒汉式的同步代码块,可能会导致每次访问都进行锁操作,影响性能。
-
创建时间:虽然单例模式减少了内存使用,但它可能增加了类加载时间,特别是在懒加载方式下,第一次访问实例时需要进行较多的判断和同步操作。
-
优化方法:
-
对于懒汉式单例,使用双重检查锁定和
volatile
可以减少不必要的同步操作,提升性能。 -
饿汉式单例创建时会在类加载时初始化对象,可能增加类加载时间,但后续访问非常快速,适合需要尽早初始化的场景。
-
2. 工厂方法模式(Factory Method Pattern)
2.1. 背景与动机
假设我们有一个系统需要支持不同类型的文档处理(例如:PDF文档、Word文档和Excel文档)。如果没有使用工厂模式,我们可能会通过直接实例化具体的类来处理文档:
public class DocumentProcessor {
public void processDocument(String type) {
if (type.equals("PDF")) {
new PDFProcessor().process();
} else if (type.equals("Word")) {
new WordProcessor().process();
} else if (type.equals("Excel")) {
new ExcelProcessor().process();
}
}
}
问题:如果添加新的文档类型,我们需要修改DocumentProcessor
类的代码,这违反了开闭原则(OCP)。并且,如果不同的文档类型具有复杂的创建过程,代码会变得冗长且难以维护。
工厂方法模式通过定义一个创建对象的接口,但将具体的对象创建交给子类去实现,从而避免了直接在代码中创建具体对象的硬耦合。
2.2 模式定义与结构
定义:工厂方法模式定义了一个接口用于创建对象,但由子类决定要实例化的类。工厂方法将对象的创建过程延迟到子类。
+--------------------------+ +------------------------+
| Creator |<>-----| Product |
+--------------------------+ +------------------------+
| + factoryMethod(): Product| | |
| + someOperation(): void | +------------------------+
+--------------------------+ ^
^ |
| |
+---------------------+ |
| ConcreteCreator | |
+---------------------+ |
| + factoryMethod() | |
| + someOperation() | |
+---------------------+ |
+----------------------+
| ConcreteProduct |
+----------------------+
角色说明:
-
Creator(创建者):定义了工厂方法接口,通常包含一个抽象的工厂方法
factoryMethod()
,以及可以依赖工厂方法的其他功能。 -
ConcreteCreator(具体创建者):实现
factoryMethod()
,返回特定的Product
对象。 -
Product(产品):定义了工厂方法所创建对象的接口。
-
ConcreteProduct(具体产品):具体的产品实现。
2.3 代码示例
// 抽象产品
interface Logger {
void log(String message);
}
// 具体产品
class MySqlLogger implements Logger { /*...*/ }
class OracleLogger implements Logger { /*...*/ }
// 抽象工厂
interface LoggerFactory {
Logger createLogger();
}
// 具体工厂
class MySqlLoggerFactory implements LoggerFactory {
public Logger createLogger() { return new MySqlLogger(); }
}
class OracleLoggerFactory implements LoggerFactory {
public Logger createLogger() { return new OracleLogger(); }
}
// 使用
LoggerFactory factory = new MySqlLoggerFactory();
Logger logger = factory.createLogger();
2.4 设计原则分析
- 单一职责原则 (SRP):每个类都只负责创建一种类型的文档处理器,并封装创建过程。
- 开闭原则 (OCP):通过扩展
DocumentCreator
来支持新的文档类型,而不需要修改已有的Creator
类。 - 依赖倒置原则 (DIP):客户端代码依赖于
DocumentCreator
的抽象接口,而不是具体的产品类。
2.5 使用场景
-
日志记录:通过工厂方法模式创建不同类型的日志记录器,例如:
FileLogger
、DatabaseLogger
等。 -
数据库连接池:不同类型的数据库连接(MySQL、Oracle、PostgreSQL等)可以通过工厂方法来创建。
-
图形库:绘制不同形状的图形(圆形、矩形、三角形等)时,可以使用工厂方法来创建图形对象。
2.6 开源示例
-
Spring 的
FactoryBean
是一个经典的工厂方法模式应用。它通过定义getObject
方法,将实例化的细节交给实现类,从而灵活地创建复杂的对象。public interface FactoryBean<T> { // 工厂方法:返回需要的 Bean 实例 T getObject() throws Exception; // 判断是否为单例 boolean isSingleton(); }
public abstract class AbstractFactoryBean<T> implements FactoryBean<T> { private T singletonInstance; @Override public final T getObject() throws Exception { if (isSingleton()) { if (this.singletonInstance == null) { this.singletonInstance = createInstance(); } return this.singletonInstance; } return createInstance(); } protected abstract T createInstance() throws Exception; }
FactoryBean
定义了创建对象的接口。子类通过实现createInstance
方法,决定具体的实例化逻辑。这是典型的工厂方法模式,扩展了对象创建的灵活性。 -
JDBC 的
DriverManager
用于根据 URL 动态加载不同的数据库驱动。虽然它没有明确的子类,但仍体现了工厂方法模式的思想public class DriverManager { public static Connection getConnection(String url, String user, String password) throws SQLException { for (Driver driver : registeredDrivers) { Connection conn = driver.connect(url, info); if (conn != null) { return conn; } } throw new SQLException("No suitable driver found for " + url); } }
DriverManager
根据 URL 动态选择合适的Driver
,并调用其connect
方法来创建Connection
对象。各个数据库驱动(例如MySQLDriver
,OracleDriver
)是Driver
接口的实现类,它们的connect
方法是具体的工厂方法 -
Apache Commons Logging 的
LogFactory
提供了工厂方法模式的完整实现,通过它的子类创建不同的日志实现。public abstract class LogFactory { public abstract Log getInstance(String name); public static LogFactory getFactory() { // 动态加载具体的 LogFactory 实现 return (LogFactory) createFactory(); } }
public class Log4jFactory extends LogFactory { @Override public Log getInstance(String name) { return new Log4jLogger(name); } }
LogFactory
是工厂方法的抽象定义,提供了getInstance
工厂方法。不同的子类(如Log4jFactory
和Jdk14LoggerFactory
)实现了创建不同日志实现的逻辑。 -
Java 中的
ExecutorService
接口和Executors
类展示了工厂方法模式的应用,它允许用户根据需要创建不同类型的线程池。public interface ExecutorService { void submit(Runnable task); } public class FixedThreadPoolExecutor implements ExecutorService { private final int poolSize; public FixedThreadPoolExecutor(int poolSize) { this.poolSize = poolSize; } @Override public void submit(Runnable task) { // 线程池执行任务 } } public class Executors { public static ExecutorService newFixedThreadPool(int poolSize) { return new FixedThreadPoolExecutor(poolSize); } }
ExecutorService
定义了用于提交任务的submit
方法。Executors
类提供了工厂方法newFixedThreadPool
,根据用户需求创建不同类型的ExecutorService
实例。
2.7 优缺点分析
优点:
- 提高了代码的可扩展性:新增产品类时,只需要创建新的工厂类,不需要修改现有代码。
- 通过工厂方法隔离了产品的具体实现,降低了耦合度。
缺点:
-
需要创建额外的工厂类,增加了系统的复杂度。
-
在产品类非常多的情况下,工厂类的数量也会暴增,增加了管理的难度。
2.8 问题反思与解答
问题:如果需要支持大量的产品类,如何避免工厂类数量的膨胀?
- 反思:可以通过抽象工厂模式来进一步封装产品的创建过程,或者使用反射机制和配置文件来动态加载类,减少工厂类的数量。
2.9 适用性分析
适用场景:
- 需要创建不同类型的产品对象时,通过工厂方法模式可以避免使用
if-else
等条件语句来选择不同的产品。 - 当对象创建过程复杂,或者不同产品之间的创建过程差异较大时,工厂方法模式能有效地封装创建逻辑。
不适用场景:
-
如果产品对象创建过程简单,且产品种类较少,可以直接使用简单的构造函数来创建对象,避免引入额外的工厂类。
-
如果产品变化频繁,使用工厂方法可能会导致大量的工厂类扩展。
2.10 常见误区
-
误区1:滥用工厂模式:如果产品种类少且创建过程简单,强行引入工厂方法模式会增加系统复杂度。
-
误区2:工厂方法过于庞大:有些设计者可能会把多个工厂方法集中到一个工厂类中,导致工厂类非常庞大,这样会失去工厂模式的优势。
2.11 模式变种
-
抽象工厂模式:当产品族(多个相关的产品)需要一起创建时,可以使用抽象工厂模式。每个工厂方法负责创建一类产品对象。
-
静态工厂方法:如果产品的创建不依赖于实例化,可以将工厂方法做成静态方法。
2.12 性能考虑
-
性能开销:工厂方法的性能开销主要体现在创建工厂对象上,如果工厂类的创建过于复杂,可能会影响性能。
-
优化方法:可以通过延迟加载(懒加载)来优化工厂的创建过程,尤其是在工厂方法不需要每次都调用的情况下。
3. 抽象工厂模式(Abstract Factory Pattern)
3.1 背景与动机
假设你在开发一个跨平台的图形界面框架,支持 Windows 和 Mac 两种操作系统,并且每个平台有不同的按钮和对话框样式。我们可以为每个平台编写不同的类来创建控件,如:
// Windows 系统的按钮
public class WindowsButton implements Button {
@Override
public void render() {
System.out.println("Rendering Windows Button");
}
}
// Mac 系统的按钮
public class MacButton implements Button {
@Override
public void render() {
System.out.println("Rendering Mac Button");
}
}
然后,我们需要在不同平台之间切换这些控件。假设我们写了一个 UIFactory
类来根据平台返回不同的控件:
public class UIFactory {
public Button createButton(String platform) {
if (platform.equals("Windows")) {
return new WindowsButton();
} else if (platform.equals("Mac")) {
return new MacButton();
}
return null;
}
}
问题:这个方法有几个问题:
- 硬编码了平台类型,违反了开闭原则(OCP),需要修改代码才能增加对新平台的支持。
- 如果有多个控件需要处理(例如:Button、Dialog),我们需要写很多
if-else
语句,导致代码臃肿且不易扩展。
抽象工厂模式通过将对象的创建逻辑提取到一个抽象工厂接口中,不同的子类提供不同平台的具体实现。这样,我们可以轻松扩展平台和控件,而无需修改现有代码。
3.2 模式定义与结构
定义:抽象工厂模式提供一个接口,用于创建一系列相关或依赖的对象,而无需指定它们的具体类。每个工厂实现都负责创建一系列相关的产品。
+--------------------+ +---------------------+
| AbstractFactory |<>----| AbstractProduct |
+--------------------+ +---------------------+
| + createButton() | | |
| + createDialog() | +---------------------+
+--------------------+ ^
^ |
| |
+-------------------+ +-------------------+
| ConcreteFactory | | ConcreteProduct |
+-------------------+ +-------------------+
| + createButton() | | ConcreteButton |
| + createDialog() | | ConcreteDialog |
+-------------------+ +-------------------+
角色说明:
- AbstractFactory(抽象工厂):声明创建抽象产品的接口,通常包括多个创建方法。
- ConcreteFactory(具体工厂):实现抽象工厂接口,具体负责创建不同的产品对象。
- AbstractProduct(抽象产品):声明一个产品的接口。
- ConcreteProduct(具体产品):实现抽象产品接口的具体类。
3.3 代码示例
// 抽象产品族
interface Button { void render(); }
interface Checkbox { void check(); }
// 具体产品族(Windows 风格)
class WinButton implements Button { public void render() { /*Windows 样式按钮*/ } }
class WinCheckbox implements Checkbox { public void check() { /*Windows 样式复选框*/ } }
// 具体产品族(Mac 风格)
class MacButton implements Button { public void render() { /*Mac 样式按钮*/ } }
class MacCheckbox implements Checkbox { public void check() { /*Mac 样式复选框*/ } }
// 抽象工厂(生产一组配套产品)
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
// 具体工厂(Windows 家族)
class WinFactory implements GUIFactory {
public Button createButton() { return new WinButton(); }
public Checkbox createCheckbox() { return new WinCheckbox(); }
}
// 具体工厂(Mac 家族)
class MacFactory implements GUIFactory {
public Button createButton() { return new MacButton(); }
public Checkbox createCheckbox() { return new MacCheckbox(); }
}
// 使用
GUIFactory factory = new MacFactory();
Button button = factory.createButton();
Checkbox checkbox = factory.createCheckbox();
3.4 设计原则分析
- 单一职责原则 (SRP):每个工厂类负责创建与其平台相关的控件,符合单一职责原则。
- 开闭原则 (OCP):通过扩展新的工厂来支持新的操作系统,而不需要修改现有代码。
- 依赖倒置原则 (DIP):客户端依赖于抽象工厂和抽象产品,而不是具体的实现类。
3.5 使用场景
-
跨平台应用:例如,跨平台的GUI工具库,如Qt(C++)或Java的Swing,使用抽象工厂来创建不同操作系统的UI控件。
-
主题或皮肤系统:不同的主题或皮肤可能包含多个组件(按钮、对话框等),抽象工厂可以用来创建一系列风格一致的控件。
-
数据库连接池:不同类型的数据库(如MySQL、PostgreSQL)可以通过不同的工厂来创建数据库连接对象。
3.6 开源示例
-
Java AWT 和 Swing:Java的
AbstractFactory
模式可以在其ComponentFactory
和不同平台的具体工厂(如WindowsFactory
、MacFactory
)中看到。public interface Component { void paint(); } public class Button implements Component { @Override public void paint() { System.out.println("Rendering a button"); } } public interface ComponentFactory { Component createComponent(); } public class WindowsComponentFactory implements ComponentFactory { @Override public Component createComponent() { return new Button(); // For Windows, we create a button } }
-
Java 的 JDBC 驱动程序提供了对不同数据库(如 MySQL、Oracle、PostgreSQL 等)的支持。每种数据库驱动程序都需要不同的实现,因此通常会通过抽象工厂模式来管理数据库连接。
// 抽象产品:数据库连接 public interface Connection { void connect(); } // 具体产品:MySQL数据库连接 public class MySQLConnection implements Connection { @Override public void connect() { System.out.println("Connecting to MySQL database"); } } // 具体产品:PostgreSQL数据库连接 public class PostgreSQLConnection implements Connection { @Override public void connect() { System.out.println("Connecting to PostgreSQL database"); } } // 抽象工厂:数据库连接工厂 public interface DatabaseFactory { Connection createConnection(); } // 具体工厂:MySQL工厂 public class MySQLFactory implements DatabaseFactory { @Override public Connection createConnection() { return new MySQLConnection(); } } // 具体工厂:PostgreSQL工厂 public class PostgreSQLFactory implements DatabaseFactory { @Override public Connection createConnection() { return new PostgreSQLConnection(); } }
public class DatabaseClient { public static void main(String[] args) { DatabaseFactory factory; // 根据需要选择不同的工厂 if ("mysql".equals(args[0])) { factory = new MySQLFactory(); } else { factory = new PostgreSQLFactory(); } Connection connection = factory.createConnection(); connection.connect(); } }
在这个例子中,我们通过抽象工厂
DatabaseFactory
来创建不同类型的数据库连接。具体的工厂如MySQLFactory
和PostgreSQLFactory
决定了创建哪种数据库连接。客户端只需要依赖于工厂接口,隐藏了具体的数据库连接细节 -
在 Apache Kafka 中,生产者和消费者对象的创建涉及到多个相关组件,如序列化器、压缩器等。这些组件通常是通过抽象工厂模式进行创建的,以支持不同的配置和扩展性。
public interface Producer { void sendMessage(String message); } public class KafkaProducer implements Producer { @Override public void sendMessage(String message) { System.out.println("Sending message to Kafka: " + message); } } public interface ProducerFactory { Producer createProducer(); } public class KafkaProducerFactory implements ProducerFactory { @Override public Producer createProducer() { return new KafkaProducer(); } }
3.7 优缺点分析
优点:
- 扩展性强:增加新产品只需新增工厂类,而不需要修改已有代码。
- 解耦:客户端代码依赖于抽象的工厂接口,而不需要依赖于具体产品类,降低了耦合度。
缺点:
-
类的数量增多:为了每个产品族创建多个具体工厂,导致类的数量增多。
-
复杂性提高:如果产品族变化频繁,可能需要频繁修改工厂类,导致系统的复杂度增加。
3.8 问题反思与解答
问题:如何避免工厂类过多导致复杂度上升?
- 反思:如果不同产品之间差异不大,可以考虑使用简单工厂模式来减少工厂类的数量。另一方面,也可以考虑通过配置文件或者反射机制来动态加载产品和工厂类。
3.9 适用性分析
适用场景:
- 当系统需要创建多个类型的相关产品,并且这些产品的创建过程存在平台或主题差异时,可以使用抽象工厂模式。
- 如果产品之间需要保持一致性(例如,按钮、菜单、对话框等组件),抽象工厂模式是一个不错的选择。
不适用场景:
- 如果产品之间没有太大差异,或者产品种类较少,可以使用简单工厂模式,而不必引入抽象工厂。
3.10 常见误区
- 误区1:忽视工厂类之间的层次关系:抽象工厂通常是用于产品族的创建,不应将它误用为单一产品的工厂。
- 误区2:过度设计:对于简单的产品类,可以直接使用简单工厂模式,不必使用抽象工厂模式。
3.11 与其他设计模式的关系
-
工厂方法模式:抽象工厂模式是工厂方法模式的一个扩展,工厂方法模式只关心一个产品的创建,而抽象工厂模式关心一系列相关产品的创建。
-
建造者模式:建造者模式与抽象工厂模式的不同之处在于,建造者关注如何一步步构建产品,而抽象工厂模式关注一组相关产品的创建。
3.12 性能考虑
- 性能影响:抽象工厂模式本身不会带来明显的性能开销,但可能会增加类的数量。性能影响更多地取决于具体工厂的实现方式和产品的创建过程。
- 优化建议:可以通过缓存机制或者对象池技术来减少工厂的创建开销。
4. 建造者模式(Builder Pattern)
4.1 背景与动机
假设你需要创建一个复杂的 Computer
对象,包含多个可选配置,如 CPU、内存、硬盘和显示器等。在没有使用建造者模式的情况下,你可能会遇到如下问题:
public class Computer {
private String CPU;
private String RAM;
private String storage;
private String display;
public Computer(String CPU, String RAM, String storage, String display) {
this.CPU = CPU;
this.RAM = RAM;
this.storage = storage;
this.display = display;
}
// Getter methods
public String getCPU() {
return CPU;
}
public String getRAM() {
return RAM;
}
public String getStorage() {
return storage;
}
public String getDisplay() {
return display;
}
@Override
public String toString() {
return "Computer [CPU=" + CPU + ", RAM=" + RAM + ", Storage=" + storage + ", Display=" + display + "]";
}
}
假设我们要创建多个 Computer
对象,但每个对象有不同的配置。每次创建对象时,我们都需要提供所有的参数,且容易出现代码重复和不易管理的情况。例如:
// 创建配置为高端游戏机的Computer
Computer gamingComputer = new Computer("Intel i9", "32GB", "2TB SSD", "4K");
// 创建配置为普通办公机的Computer
Computer officeComputer = new Computer("Intel i5", "8GB", "500GB HDD", "1080p");
问题:
- 构造函数参数繁琐:当产品的配置变得复杂时,构造函数参数会越来越多,导致代码混乱。
- 难以扩展:如果以后需要修改配置项或添加新配置,构造函数的参数列表会变得更长,增加了修改的难度。
建造者模式通过将对象的构建过程与表示分离,使得同样的构建过程可以创建不同的表示。通过逐步构建对象,避免了长列表的构造函数,并能够灵活定制产品的各个部分。
####4.2 模式定义与结构
定义:建造者模式将一个复杂对象的构建过程与表示分离,使得同样的构建过程可以创建不同的表示。
+---------------------+
| Director |
+---------------------+
| - builder |
+---------------------+
| + construct() |
+---------------------+
^
|
+---------------------+
| Builder |
+---------------------+
| + buildPartA() |
| + buildPartB() |
| + buildPartC() |
| + getResult() |
+---------------------+
^
|
+--------------------------+
| ConcreteBuilder |
+--------------------------+
| + buildPartA() |
| + buildPartB() |
| + buildPartC() |
| + getResult() |
+--------------------------+
^
|
+---------------------+
| Product |
+---------------------+
| - partA |
| - partB |
| - partC |
+---------------------+
| + show() |
+---------------------+
角色说明:
- Director(指挥者):负责构建复杂对象的步骤,调用建造者的构建方法。
- Builder(建造者):抽象类,定义构建各个部件的接口。
- ConcreteBuilder(具体建造者):实现 Builder 接口,逐步构建产品的各个部件。
- Product(产品):最终构建出来的复杂对象。
4.3 代码示例
建造者模式通过分步骤创建一个复杂对象,将对象的创建过程与其表示分离。该模式适用于需要一步步构建复杂对象的情况。
// 1. 产品类(Computer)
public class Computer {
private final String cpu; // 必选
private final String ram; // 必选
private final String storage; // 可选
private final String gpu; // 可选
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.gpu = builder.gpu;
}
// 2. 静态内部Builder类
public static class Builder {
private final String cpu; // 必选参数
private final String ram; // 必选参数
private String storage = "1TB HDD"; // 可选参数(默认值)
private String gpu = "Integrated"; // 可选参数(默认值)
// 必选参数通过构造方法传入
public Builder(String cpu, String ram) {
this.cpu = cpu;
this.ram = ram;
}
// 可选参数通过链式方法设置
public Builder storage(String storage) {
this.storage = storage;
return this;
}
public Builder gpu(String gpu) {
this.gpu = gpu;
return this;
}
// 构建最终对象
public Computer build() {
return new Computer(this);
}
}
// 省略 getter 方法...
}
Computer highEndPC = new Computer.Builder("Intel i9", "32GB DDR5")
.storage("2TB NVMe SSD")
.gpu("NVIDIA RTX 4090")
.build();
Computer basicPC = new Computer.Builder("Ryzen 5", "16GB DDR4")
.build(); // 使用默认的存储和GPU
4.4 设计原则分析
- 单一职责原则 (SRP):每个建造者类负责构建一个特定的产品,而产品类仅负责数据存储。
- 开闭原则 (OCP):可以通过扩展 Builder 类来支持新类型的产品,无需修改已有代码。
- 依赖倒置原则 (DIP):指挥者依赖于 Builder 的抽象接口,而不是具体的建造者实现。
4.5 使用场景
-
复杂对象的创建:例如,生成一个复杂的网页,网页中包含多个部分(如导航栏、内容区、侧边栏等),每个部分可以使用不同的配置。
-
产品定制化:在制造业中,产品可能有很多定制化的选项,如手机的配置、汽车的选配项等。
-
多步骤的对象构建过程:例如,生成一张复杂的报告或导出一份文件,需要多个步骤。
4.6 开源示例
- OkHttp 的
Request
类使用建造者模式来构造 HTTP 请求。
// 源码路径:okhttp/src/main/java/okhttp3/Request.java
public final class Request {
private final HttpUrl url;
private final String method;
private final Headers headers;
private final RequestBody body;
// 其他字段...
// 私有构造方法,只能通过 Builder 构造
private Request(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = builder.headers.build();
this.body = builder.body;
}
// 内部 Builder 类
public static class Builder {
private HttpUrl url;
private String method = "GET";
private Headers.Builder headers;
private RequestBody body;
public Builder() {
headers = new Headers.Builder();
}
public Builder url(HttpUrl url) {
this.url = url;
return this;
}
public Builder header(String name, String value) {
headers.set(name, value);
return this;
}
public Builder post(RequestBody body) {
this.method = "POST";
this.body = body;
return this;
}
// 构建 Request 对象
public Request build() {
if (url == null) throw new IllegalStateException("url == null");
return new Request(this);
}
}
}
// 构建一个 POST 请求
Request request = new Request.Builder()
.url("https://api.example.com/data")
.header("Content-Type", "application/json")
.post(RequestBody.create(jsonData, MediaType.get("application/json")))
.build();
设计优势:
- 支持链式调用,灵活设置请求参数(URL、Header、Body 等)。
- 参数校验在
build()
方法中完成(如检查 URL 是否为空)。
- Guava 的
ImmutableList
使用建造者模式构造不可变集合。
// 源码路径:guava/guava/src/com/google/common/collect/ImmutableList.java
public abstract class ImmutableList<E> extends ImmutableCollection<E> implements List<E> {
// 内部 Builder 类
public static final class Builder<E> extends ImmutableCollection.Builder<E> {
private Object[] contents;
private int size;
public Builder() {
this(4); // 默认初始容量
}
Builder(int initialCapacity) {
contents = new Object[initialCapacity];
size = 0;
}
// 添加元素
public Builder<E> add(E element) {
ensureCapacity(size + 1);
contents[size++] = element;
return this;
}
// 构建不可变列表
public ImmutableList<E> build() {
return (size == 0)
? ImmutableList.<E>of()
: new RegularImmutableList<E>(Arrays.copyOf(contents, size));
}
}
}
ImmutableList<String> list = new ImmutableList.Builder<String>()
.add("Java")
.add("Python")
.add("Go")
.build();
设计优势:
-
支持动态添加元素,最终生成不可变对象(线程安全)。
-
内部自动处理容量扩展(
ensureCapacity
方法)。
- Spring 的
UriComponentsBuilder
使用建造者模式构造 URL。
// 源码路径:spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java
public class UriComponentsBuilder implements UriBuilder, Cloneable {
private String scheme;
private String userInfo;
private String host;
private int port = -1;
private String path;
// 其他字段...
// 静态工厂方法创建 Builder
public static UriComponentsBuilder fromUriString(String uri) {
UriComponentsBuilder builder = new UriComponentsBuilder();
// 解析 URI 并初始化字段...
return builder;
}
// 链式方法设置参数
public UriComponentsBuilder scheme(String scheme) {
this.scheme = scheme;
return this;
}
public UriComponentsBuilder path(String path) {
this.path = path;
return this;
}
// 构建 UriComponents 对象
public UriComponents build() {
return build(false);
}
}
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com")
.scheme("http")
.path("/api/users")
.queryParam("name", "Alice")
.build();
设计优势:
- 分步骤构造复杂的 URI(协议、路径、查询参数等)。
- 支持从字符串解析初始配置。
- Lombok 的
@Builder
注解自动生成建造者模式的代码。
import lombok.Builder;
import lombok.ToString;
@Builder
@ToString
public class User {
private final String id;
private final String name;
@Builder.Default private int age = 18;
private String email;
}
public class User {
private final String id;
private final String name;
private int age;
private String email;
// 自动生成的 Builder 类
public static class UserBuilder {
private String id;
private String name;
private int age = 18; // 默认值
private String email;
public UserBuilder id(String id) { ... }
public UserBuilder name(String name) { ... }
public UserBuilder age(int age) { ... }
public UserBuilder email(String email) { ... }
public User build() {
return new User(this);
}
}
}
User user = User.builder()
.id("123")
.name("Alice")
.email("alice@example.com")
.build();
设计优势:
- 通过注解自动生成样板代码,减少手动编写工作量。
- 支持默认值和链式调用。
4.7 适用性分析
适用场景:
-
当对象的构建过程复杂且需要多步骤操作时。
-
当对象的构建需要多个配置项,而这些配置项可能具有不同的组合时。
-
当构建的过程可能变化或需要灵活扩展时。
替代方案:
- 如果对象的构建过程简单,可以使用工厂模式来代替建造者模式。
4.8 常见误区
-
过度使用建造者模式:如果构建过程简单,使用建造者模式会使代码复杂化,适得其反。
-
建造者和指挥者的混淆:需要确保建造者只关注构建过程,指挥者则负责调用建造者的具体方法。
4.9 模式变种
-
多建造者模式:通过不同的建造者可以创建不同类型的对象,适用于更复杂的对象创建场景。
-
建造者和工厂的结合:可以结合工厂模式来创建某些固定的部件,而使用建造者模式来构建复杂的对象。
4.10 性能考虑
建造者模式本质上通过逐步构建复杂对象来优化代码的可维护性和扩展性。虽然它能提高代码的灵活性和清晰度,但在性能方面需要考虑以下几点:
- 内存开销:
- 在构建对象的过程中,可能会创建多个中间对象。例如,在建造者模式中,每个构建步骤可能都涉及到对同一个产品对象的不同部件进行设置。随着构建步骤的增加,内存开销会增加。
- 每个具体建造者都可能会创建一个
Product
对象,并且每个产品对象的创建过程都可能在多个步骤之间进行,导致额外的内存消耗。
- 构建过程的时间开销:
- 建造者模式的构建过程通常需要多个步骤来完成,每个步骤可能会涉及计算或赋值操作。如果产品的构建过程非常复杂,那么这些操作可能会对性能造成一定影响。
- 在一些场景中,构建过程可能涉及到计算密集型的操作,或者有较多的输入输出操作,这时,建造者模式的性能瓶颈可能会更加明显。
- 多次创建与销毁对象:
- 每次使用建造者时,都会创建一个新的
Product
实例,并且在构建过程中,每个步骤都会影响Product
的状态。虽然这是实现灵活性的关键,但它也可能会带来一定的性能损耗,尤其是在构建过程比较复杂时。
- 每次使用建造者时,都会创建一个新的
- 优化建议:
- 延迟加载:如果构建过程中的某些部件可以延迟创建,可以通过懒加载(Lazy Initialization)来减少初始构建时的开销。只有在真正需要时才创建对象,避免不必要的内存消耗。
- 对象池:对于需要反复创建的对象,可以使用对象池来复用已创建的对象,减少频繁的内存分配和回收带来的开销。
- 减少建造者类的数量:在某些场景下,使用多个建造者可能会导致大量类的创建,增加维护成本。可以通过组合模式或者通过配置来减少建造者类的数量。
4.11 性能优化
假设我们要创建一个非常复杂的 Computer
对象,且构建过程中需要涉及多个步骤和部件。为了提高性能,可以考虑以下优化策略:
- 使用 懒加载 延迟加载一些构建步骤,避免在初始阶段就加载所有信息。
public class Computer {
private String CPU;
private String RAM;
private String storage;
private String display;
private boolean cpuLoaded = false;
private boolean ramLoaded = false;
public void loadCPU() {
if (!cpuLoaded) {
// 模拟延迟加载
this.CPU = "Intel i9";
cpuLoaded = true;
}
}
public void loadRAM() {
if (!ramLoaded) {
// 模拟延迟加载
this.RAM = "32GB";
ramLoaded = true;
}
}
// 其他构建部分同理...
@Override
public String toString() {
return "Computer [CPU=" + CPU + ", RAM=" + RAM + ", Storage=" + storage + ", Display=" + display + "]";
}
}
使用 对象池 来复用已经创建的产品对象,减少每次创建和销毁对象的开销。
public class ComputerPool {
private static final List<Computer> pool = new ArrayList<>();
public static Computer getComputer() {
if (pool.isEmpty()) {
return new Computer();
} else {
return pool.remove(pool.size() - 1);
}
}
public static void returnToPool(Computer computer) {
pool.add(computer);
}
}
- 优化 构建过程,减少不必要的步骤,例如缓存某些计算结果或者避免重复计算。
性能评估与权衡
在实际应用中,建造者模式的性能瓶颈通常出现在以下几种情况下:
- 大量构建步骤:每个构建步骤都可能涉及到赋值、方法调用等操作,这些操作会在复杂的对象构建过程中产生积累的性能开销。
- 复杂对象的构建:如果产品的构建过程非常复杂(例如需要从外部数据库加载数据,或者需要进行复杂计算),建造者模式的性能开销会变得更加明显。
因此,在选择建造者模式时,需要权衡其带来的灵活性和性能开销。如果产品构建过程较简单,且性能要求较高,可能不需要使用建造者模式,而可以选择其他设计模式,如工厂模式或简单的构造函数。反之,当需要逐步构建复杂对象时,建造者模式的灵活性和可扩展性将带来更多的优势。
5. 原型模式(Prototype Pattern)
5.1 背景与动机
假设我们需要创建一个非常复杂的对象,比如一个图形对象,它有许多属性和状态。通常情况下,创建这样的对象需要执行一系列步骤,可能还需要依赖一些外部的资源。如果每次都要从头开始构建对象,这不仅费时,而且容易出错。
public class ComplexObject {
private String name;
private int value;
public ComplexObject(String name, int value) {
this.name = name;
this.value = value;
}
public ComplexObject clone() {
return new ComplexObject(this.name, this.value);
}
}
问题:
- 直接通过构造函数进行复制,往往需要手动复制对象的每个属性。随着对象变得越来越复杂,代码变得冗长且易出错。
通过原型模式,我们可以通过克隆现有对象来创建新的对象,避免重复创建的麻烦。它通过提供一个克隆接口来实现对象的复制。
5.2 模式定义与结构
原型模式是一种创建型设计模式,允许通过复制现有的实例来创建新的对象,而无需重新构造它们。该模式通过提供一个 clone()
方法,让对象的副本能够被创建。
+---------------------+ +----------------------+
| Prototype |<>-----| ConcretePrototype |
+---------------------+ +----------------------+
| + clone() | | - name: String |
+---------------------+ | - value: int |
| + clone() |
+----------------------+
角色说明:
- Prototype:声明一个
clone()
方法,返回一个新的Prototype
对象。 - ConcretePrototype:实现
clone()
方法,复制自身的状态并返回一个新的实例。
5.3 源码示例
原型模式通过复制现有对象来创建新对象。它用于避免创建新对象的开销,尤其是当对象的创建过程复杂或资源消耗较大时。
// 1. 定义原型接口(或抽象类)
public abstract class Shape implements Cloneable {
protected String type;
protected String color;
public abstract void draw();
// 实现克隆方法
@Override
public Shape clone() {
try {
return (Shape) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 不会发生
}
}
// Getter/Setter
public String getType() { return type; }
public void setColor(String color) { this.color = color; }
}
// 2. 具体原型类
public class Circle extends Shape {
public Circle() {
type = "Circle";
}
@Override
public void draw() {
System.out.println("Drawing a " + color + " " + type);
}
}
public class Rectangle extends Shape {
public Rectangle() {
type = "Rectangle";
}
@Override
public void draw() {
System.out.println("Drawing a " + color + " " + type);
}
}
// 3. 客户端使用
public class Client {
public static void main(String[] args) {
// 创建原型对象
Shape circlePrototype = new Circle();
circlePrototype.setColor("Red");
// 通过克隆创建新对象
Shape redCircle = circlePrototype.clone();
Shape blueCircle = circlePrototype.clone();
blueCircle.setColor("Blue");
redCircle.draw(); // Output: Drawing a Red Circle
blueCircle.draw(); // Output: Drawing a Blue Circle
}
}
深拷贝优化
默认的 clone()
方法是浅拷贝。如果对象包含引用类型属性,需手动实现深拷贝:
public class ComplexShape extends Shape implements Cloneable {
private List<String> coordinates;
public ComplexShape() {
type = "ComplexShape";
coordinates = new ArrayList<>();
}
public void addCoordinate(String coord) {
coordinates.add(coord);
}
@Override
public ComplexShape clone() {
ComplexShape clone = (ComplexShape) super.clone();
// 深拷贝引用类型
clone.coordinates = new ArrayList<>(this.coordinates);
return clone;
}
}
5.4 设计原则分析
- 单一职责原则 (SRP):原型模式本身并不违反单一职责原则。每个对象只负责自己的克隆,而不涉及其他复杂的职责。
- 开闭原则 (OCP):通过原型模式,我们可以增加不同的具体原型类而不需要修改已有的代码。每个
ConcretePrototype
可以在不修改其他代码的情况下扩展。 - 依赖倒置原则 (DIP):在使用原型模式时,客户端依赖于抽象的
Prototype
类,而不是具体的ConcretePrototype
类,从而降低了代码的耦合度。
5.5 实际应用场景
-
对象克隆:当对象的构建成本高,并且需要创建多个相似对象时,使用原型模式可以通过克隆现有对象来提高效率。例如,图形编辑器、CAD系统等。
-
避免重复的初始化操作:在一些需要进行复杂初始化的对象中,通过原型模式可以避免多次重复创建。
-
缓存系统:如果我们需要创建多个相同的对象,可以通过原型模式和缓存结合,避免每次都重新创建对象。
5.6 开源示例
- 在 Git 中,
Object
是一个用来表示不同对象(如提交、文件、树等)的抽象类。每个对象在 Git 中有一个唯一的 ID,可以通过对象克隆来实现版本控制和差异追踪。
public class GitObject implements Cloneable {
private String objectID;
public GitObject(String objectID) {
this.objectID = objectID;
}
@Override
public GitObject clone() {
return new GitObject(this.objectID);
}
@Override
public String toString() {
return "GitObject{objectID='" + objectID + "'}";
}
}
- 在 JDK 的
ArrayList
中,有clone()
方法实现对象的克隆。
public class MyArrayList implements Cloneable {
private String[] items;
public MyArrayList(String[] items) {
this.items = items;
}
@Override
public MyArrayList clone() {
return new MyArrayList(this.items.clone());
}
}
- Apache Commons Lang 库提供了
SerializationUtils.clone()
方法,这实际上使用了原型模式的概念。它通过序列化和反序列化技术来实现对象的深拷贝。
import org.apache.commons.lang3.SerializationUtils;
import java.io.Serializable;
public class Person implements Serializable {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static void main(String[] args) {
Person original = new Person("John");
Person clone = SerializationUtils.clone(original);
System.out.println(original.getName()); // John
System.out.println(clone.getName()); // John
}
}
- MyBatis 在一些内部缓存和对象复制的场景中也使用了原型模式。虽然它没有直接展示原型模式的代码,但 MyBatis 通过使用
clone()
方法来生成查询结果的深拷贝,以避免修改查询结果对原始数据产生影响。
public class DefaultResultHandler implements ResultHandler {
public void handleResult(ResultContext context) {
// 假设通过 clone 方法进行深拷贝
Object result = context.getResultObject();
if (result instanceof Cloneable) {
result = ((Cloneable) result).clone(); // 深拷贝
}
}
}
- JDK 的
Object
类提供了clone()
方法,这是原型模式的基础实现。尽管它通常用于复制简单对象,但在许多复杂对象中,开发者可以根据需要重写clone()
方法来实现深拷贝。
public class MyClass implements Cloneable {
private int id;
private String name;
public MyClass(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone(); // 使用原型模式进行浅拷贝
}
}
5.7 优缺点分析
优点:
- 提高效率:通过克隆现有对象来创建新对象,避免重复的初始化过程。
- 简化创建过程:不需要每次都重新构建对象,可以通过现有对象来复制,简化代码。
缺点:
- 复杂性:需要实现
clone()
方法,并考虑深拷贝和浅拷贝的问题,可能会导致代码复杂度增加。 - 资源消耗:某些复杂对象的克隆可能会导致额外的内存和计算开销,尤其是在对象较大或嵌套对象较多时。
5.8 问题反思与解答
如何判断使用原型模式?
-
如果对象的构建过程非常复杂,且需要多次创建相似的对象,那么可以考虑使用原型模式。
-
如果对象需要支持深拷贝或浅拷贝,并且需要快速复制对象的状态,原型模式可以有效简化这一过程。
深拷贝和浅拷贝的区别?
-
浅拷贝:只复制对象的引用,不复制引用对象本身。适用于不需要修改引用对象的情况。
-
深拷贝:复制对象及其所有引用的对象。适用于需要完全独立的副本的情况。
5.9 适用性分析
适用场景:
- 当需要创建一个新对象,且它的构建过程繁琐时,使用原型模式能够高效克隆对象。
- 当需要频繁复制某些对象时,原型模式能够有效减少对象创建的开销。
不适用场景:
- 如果对象的构建过程相对简单且不涉及大量的属性,使用原型模式可能会增加不必要的复杂度。
- 如果对象的生命周期很短,且每次创建时都不需要复用现有对象,那么使用原型模式就没有太大意义。
5.10 常见误区
-
误区 1
:原型模式适用于所有对象的创建。
- 实际上,原型模式适用于对象创建非常复杂的场景。如果对象的构建很简单,使用原型模式会带来额外的复杂性。
-
误区 2
:所有对象都需要实现clone()方法。
- 不是所有对象都需要
clone()
方法,只有那些需要克隆操作的对象才需要实现此方法。
- 不是所有对象都需要
5.11 与其他设计模式的关系
-
工厂方法模式:原型模式和工厂方法模式都可以用于对象的创建。不同的是,工厂方法是通过方法创建新对象,而原型模式则是通过克隆已有对象来创建新对象。
-
享元模式:当多个对象共享相同的状态时,享元模式和原型模式可以结合使用,优化内存消耗。
5.12 性能考虑
- 性能评估:克隆对象可能会有一定的性能开销,特别是在对象较大或需要深拷贝时。通过优化
clone()
方法或选择合适的拷贝方式,可以减少性能开销。
2. 结构模式
结构型设计模式主要关注如何将类和对象组合成更大的结构,以实现更复杂的功能。通过这些模式,我们可以有效地组织系统中的类和对象,提高系统的灵活性和可维护性。本文将详细介绍七种常见的结构型设计模式,并通过代码示例展示它们的实际应用。
1. 适配器模式(Adapter Pattern)
1.1 背景与动机
问题:
在开发中,常常会遇到旧代码或第三方库中的类与当前系统的需求不兼容的情况。为了能够重新使用这些现有的类或系统,我们需要将它们转换成符合当前接口的形式。最常见的场景是,当我们无法修改现有类的代码时(比如使用了第三方库),但又需要通过它们来完成某些功能。
问题点:
- 接口不兼容:
如果新代码与现有代码有不同的接口,直接调用可能会导致错误或性能问题。 - 扩展性问题:
如果新需求需要修改现有的代码,但又不能修改第三方库或已有代码,这时就需要一个灵活的方式来进行适配。
解决方案:
适配器模式通过引入一个适配器类来解决这一问题。它充当一个“中介”,将现有类的接口转换为所需要的目标接口。
1.2 模式定义与结构
适配器模式是一种结构型设计模式,它允许将一个接口转换为客户端所期望的另一种接口。适配器模式通常用于解决接口不兼容的问题,它通过创建一个适配器类,将不同的接口适配成需要的接口。
+-------------------+ +------------------+
| Target |<--------| Adapter |
|-------------------| |------------------|
| + request() | | + request() |
+-------------------+ +------------------+
^
|
+---------------------+
| Adaptee |
|---------------------|
| + specificRequest() |
+---------------------+
主要角色:
- Target (目标接口):
定义客户端期望的接口,通常是系统或应用程序需要对外暴露的接口。 - Adapter (适配器):
适配器类将Adaptee
的接口转换成Target
的接口。它实现了目标接口,并将请求委派给Adaptee
对象。 - Adaptee (被适配者):
需要被适配的现有类,它可能有一个与目标接口不兼容的方法。
1.3 源码示例
假设有一个需要连接到现有服务接口的应用程序,但是该接口的返回类型与应用程序不兼容。通过适配器,我们可以将其转换为所需的接口。
// 目标接口
interface PaymentProcessor {
void processPayment(double amount);
}
// 被适配者
class OldPaymentSystem {
public void pay(double amount) {
System.out.println("Processing payment of $" + amount + " through old system.");
}
}
// 适配器
class PaymentAdapter implements PaymentProcessor {
private OldPaymentSystem oldPaymentSystem;
public PaymentAdapter(OldPaymentSystem oldPaymentSystem) {
this.oldPaymentSystem = oldPaymentSystem;
}
@Override
public void processPayment(double amount) {
oldPaymentSystem.pay(amount); // 将请求委派给被适配者
}
}
// 客户端代码
public class AdapterPatternDemo {
public static void main(String[] args) {
OldPaymentSystem oldPaymentSystem = new OldPaymentSystem();
PaymentProcessor paymentProcessor = new PaymentAdapter(oldPaymentSystem);
paymentProcessor.processPayment(100.0); // 输出:Processing payment of $100.0 through old system.
}
}
Processing payment of $100.0 through old system.
1.4 设计原则分析
- 开闭原则 (OCP):
适配器模式允许我们通过增加新的适配器类来支持新的目标接口,而不需要修改现有的类,符合开闭原则。 - 单一职责原则 (SRP):
适配器类专门负责将不兼容的接口适配成目标接口,职责单一,易于维护。 - 依赖倒置原则 (DIP):
客户端代码依赖于抽象的目标接口,而不是具体的实现类。
1.5 实际应用场景
- 与遗留系统兼容:
当我们需要集成一个老旧的系统或第三方库,而它们的接口不兼容时,可以使用适配器模式。 - 不同接口的兼容性:
在开发过程中,可能会遇到多个接口需要兼容的问题,适配器模式可以帮助我们将不同的接口统一成所需的接口。 - 模块化设计:
如果系统中的模块需要对外提供一致的接口,但是它们内部实现不同,适配器可以帮助我们将不同实现的模块统一成相同的接口。
1.6 开源示例
- 在 Spring 框架中,Spring 提供了适配器类,使得不同类型的数据源(如
DriverManagerDataSource
、BasicDataSource
等)能够统一适配并被应用于 JDBC 操作。Spring 的DataSource
是一个典型的适配器模式应用,通过不同的实现类适配不同的数据库连接池。
public class DatabaseAdapter implements DataSource {
private final BasicDataSource dataSource;
public DatabaseAdapter(String url, String username, String password) {
dataSource = new BasicDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
}
@Override
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
// 其他方法实现
}
- 假设我们需要为不同的数据库(MySQL、PostgreSQL)提供统一的接口,适配器模式可以帮助我们将各个数据库的 API 适配到统一接口上。
public interface Database {
void connect();
void executeQuery(String query);
void close();
}
public class MySQLAdapter implements Database {
private MySQLConnection mysqlConnection;
public MySQLAdapter(MySQLConnection mysqlConnection) {
this.mysqlConnection = mysqlConnection;
}
@Override
public void connect() {
mysqlConnection.connect();
}
@Override
public void executeQuery(String query) {
mysqlConnection.executeQuery(query);
}
@Override
public void close() {
mysqlConnection.close();
}
}
public class PostgreSQLAdapter implements Database {
private PostgreSQLConnection postgresConnection;
public PostgreSQLAdapter(PostgreSQLConnection postgresConnection) {
this.postgresConnection = postgresConnection;
}
@Override
public void connect() {
postgresConnection.connect();
}
@Override
public void executeQuery(String query) {
postgresConnection.executeQuery(query);
}
@Override
public void close() {
postgresConnection.close();
}
}
1.7 优缺点分析
优点:
- 接口转换:
可以通过适配器模式将不兼容的接口统一为可使用的接口,避免了直接修改类的代码。 - 灵活性高:
可以在不修改现有系统的情况下添加适配器,从而与新的系统或库进行兼容。
缺点:
- 增加了系统的复杂性:
引入适配器类会增加类的数量,可能导致系统复杂度上升。 - 适配器过多:
如果系统需要适配的接口过多,可能会导致适配器类的数量激增,降低代码的可维护性。
1.8 问题反思与解答
- 什么时候不使用适配器模式?
如果接口本身非常简单,或者可以直接修改现有类的代码,那么使用适配器模式可能是不必要的。 - 适配器设计过于复杂如何优化?
保持适配器类的职责单一,并尽量避免在适配器中实现过多的逻辑。
1.9 适用性分析
适用场景:
- 需要将现有类的接口转换成目标接口。
- 现有类的接口不兼容,但我们无法修改它们。
- 需要对多种不兼容接口进行适配时。
替代方案:
- 装饰模式:
如果目标是对现有类进行功能扩展而非接口转换,可以考虑使用装饰模式。
1.10 常见误区
- 适配器滥用:
不要把所有不兼容的接口都使用适配器模式处理。如果接口比较简单,直接继承或实现目标接口可能更简洁。 - 过多适配器导致混乱:
适配器类的数量应该控制在合理范围内,避免过多适配器增加系统的复杂性。
1.11 模式变种
- 类适配器:
通过继承来实现适配,将适配器类作为目标类的子类。适用于需要对现有类进行简单修改的场景。 - 对象适配器:
通过组合方式,将适配器类与被适配的类组合在一起,适用于不能修改现有类的场景。
1.12 与其他设计模式的关系
- 与桥接模式:
两者都旨在将抽象与实现分离,桥接模式通过组合实现类来解耦,而适配器通过转换接口来解耦。 - 与装饰模式:
装饰模式侧重于扩展对象的功能,而适配器模式则关注于接口转换。
1.13 性能考虑
- 内存消耗:
适配器模式引入了适配器类,可能导致一定的内存开销。 - 性能优化:
避免过多适配器的创建,保持适配器代码的简洁和高效,减少不必要的对象创建。
2. 桥接模式(Bridge Pattern)
2.1 背景与动机
为什么需要桥接模式?
在实际开发中,随着需求的不断增加,某些功能需要跨多个维度进行扩展。例如,对于一个图形绘制系统,我们可能需要同时支持多种形状(圆形、矩形)和多种渲染方式(像素渲染、矢量渲染)。如果我们使用传统继承,会遇到以下问题:
- 类爆炸问题:每新增一种形状或一种渲染方式,都需要添加多个子类,导致类数量急剧增长。
- 高耦合问题:形状和渲染方式被紧密绑定在一起,无法单独扩展某一维度。
假设你需要开发一个图形绘制程序,支持不同形状(如圆形和矩形)以及不同的绘制实现(如像素绘制和矢量绘制)。直观的实现方式是为每种形状创建不同的子类,例如:
// 每种形状和渲染方式的组合需要一个类
public class PixelCircle {
public void draw() {
System.out.println("Drawing Circle with Pixel Rendering");
}
}
public class VectorRectangle {
public void draw() {
System.out.println("Drawing Rectangle with Vector Rendering");
}
}
问题:
-
如果需要新增一种形状(例如三角形)和一种新的渲染方式(例如纹理渲染),需要新增以下类:
PixelTriangle
VectorTriangle
TextureTriangle
类数量按
形状 × 渲染方式
的笛卡尔积增长,扩展性极差。
2.2 模式定义与结构
桥接模式将抽象部分与实现部分分离,使它们可以独立地变化。它主要用于解耦抽象和具体实现,从而增强系统的灵活性。
+-----------------+ +------------------+
| Abstraction |<>----->| Implementor |
+-----------------+ +------------------+
| + operation() | | + operationImp() |
+-----------------+ +------------------+
^
|
+-------------------+
| RefinedAbstraction|
+-------------------+
+------------------+
| ConcreteImplementor |
+------------------+
角色说明:
- Abstraction(抽象类):定义高层抽象接口,持有
Implementor
的引用。 - RefinedAbstraction(扩展抽象类):具体实现抽象类中的方法。
- Implementor(实现接口):定义具体的实现接口。
- ConcreteImplementor(具体实现类):具体实现
Implementor
接口中的方法。
2.3 源码示例
// 实现接口
public interface DrawingAPI {
void drawCircle();
void drawRectangle();
}
// 具体实现类:像素绘制
public class PixelDrawingAPI implements DrawingAPI {
@Override
public void drawCircle() {
System.out.println("Drawing Circle with Pixel Rendering");
}
@Override
public void drawRectangle() {
System.out.println("Drawing Rectangle with Pixel Rendering");
}
}
// 抽象类
public abstract class Shape {
protected DrawingAPI drawingAPI;
protected Shape(DrawingAPI drawingAPI) {
this.drawingAPI = drawingAPI;
}
public abstract void draw();
}
// 扩展抽象类:圆形
public class Circle extends Shape {
public Circle(DrawingAPI drawingAPI) {
super(drawingAPI);
}
@Override
public void draw() {
drawingAPI.drawCircle();
}
}
// 测试客户端
public class Client {
public static void main(String[] args) {
Shape circle = new Circle(new PixelDrawingAPI());
circle.draw();
}
}
2.4 设计原则分析
-
单一职责原则 (SRP):形状类负责描述形状,绘制实现类负责具体的绘制逻辑,职责明确。
-
开闭原则 (OCP):可以方便地增加新的形状和绘制实现,而无需修改现有代码。
-
依赖倒置原则 (DIP):高层模块(如
Shape
)依赖于抽象的DrawingAPI
,而不是具体实现。
2.5 实际应用场景
- 跨平台开发:桥接模式可以用于跨平台的 GUI 框架,每个平台的绘图实现可以通过桥接模式适配。
- 数据库访问层:不同数据库(如 MySQL 和 PostgreSQL)可以使用不同的实现,通过桥接模式解耦数据库操作和具体的数据库实现。
- 日志系统:日志系统可以通过桥接模式实现不同的日志输出(如文件日志、控制台日志、远程日志等)。
2.6 开源示例
- JLF4J 是一个著名的日志门面框架,它使用桥接模式将日志抽象(API 层)与具体实现(如 Log4j、Logback 等)分离,使得用户可以轻松切换日志实现。
//抽象部分:`Logger` 接口
public interface Logger {
void log(String message);
}
//具体日志实现
public class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println("ConsoleLogger: " + message);
}
}
public class FileLogger implements Logger {
@Override
public void log(String message) {
// 模拟将日志写入文件
System.out.println("FileLogger: " + message);
}
}
//使用抽象接口的类
public abstract class LoggerBridge {
protected Logger logger;
public LoggerBridge(Logger logger) {
this.logger = logger;
}
public abstract void log(String message);
}
public class AdvancedLogger extends LoggerBridge {
public AdvancedLogger(Logger logger) {
super(logger);
}
@Override
public void log(String message) {
logger.log("[Advanced Log] " + message);
}
}
抽象层是 SLF4J 的 API(如 org.slf4j.Logger
)实现层包括各种日志框架(如 Log4j、Logback)。桥接部分是适配代码,负责将 SLF4J 的 API 调用委托给具体日志实现。
- JDBC 是 Java 提供的数据库连接 API,它使用桥接模式将数据库操作的抽象(JDBC API)与具体实现(如 MySQL PostgreSQL 驱动)分离 。
public interface Connection {
void connect();
Statement createStatement();
}
public interface Statement {
void execute(String query);
}
//具体数据库驱动
public class MySQLConnection implements Connection {
@Override
public void connect() {
System.out.println("Connected to MySQL database.");
}
@Override
public Statement createStatement() {
return new MySQLStatement();
}
}
public class PostgreSQLConnection implements Connection {
@Override
public void connect() {
System.out.println("Connected to PostgreSQL database.");
}
@Override
public Statement createStatement() {
return new PostgreSQLStatement();
}
}
public class MySQLStatement implements Statement {
@Override
public void execute(String query) {
System.out.println("Executing query on MySQL: " + query);
}
}
public class PostgreSQLStatement implements Statement {
@Override
public void execute(String query) {
System.out.println("Executing query on PostgreSQL: " + query);
}
}
public class DatabaseBridge {
private Connection connection;
public DatabaseBridge(Connection connection) {
this.connection = connection;
}
public void performQuery(String query) {
connection.connect();
Statement statement = connection.createStatement();
statement.execute(query);
}
}
//桥接部分:统一的数据库操作类
public class DatabaseBridge {
private Connection connection;
public DatabaseBridge(Connection connection) {
this.connection = connection;
}
public void performQuery(String query) {
connection.connect();
Statement statement = connection.createStatement();
statement.execute(query);
}
}
public class Client {
public static void main(String[] args) {
Connection mysqlConnection = new MySQLConnection();
Connection postgresConnection = new PostgreSQLConnection();
DatabaseBridge mysqlBridge = new DatabaseBridge(mysqlConnection);
DatabaseBridge postgresBridge = new DatabaseBridge(postgresConnection);
mysqlBridge.performQuery("SELECT * FROM users;");
postgresBridge.performQuery("SELECT * FROM orders;");
}
}
- Kafka 的客户端 API 是桥接模式的一个实际应用。在 Kafka 中,抽象层定义了消息生产和消费的接口,具体实现则提供了针对不同协议和环境的支持。
//生产者和消费者接口
public interface MessageProducer {
void send(String topic, String message);
}
public interface MessageConsumer {
void consume(String topic);
}
//Kafka 的具体生产者和消费者
public class KafkaProducer implements MessageProducer {
@Override
public void send(String topic, String message) {
System.out.println("Sending message to Kafka topic: " + topic + ", message: " + message);
}
}
public class KafkaConsumer implements MessageConsumer {
@Override
public void consume(String topic) {
System.out.println("Consuming messages from Kafka topic: " + topic);
}
}
//使用抽象接口的客户端
public class MessagingBridge {
private MessageProducer producer;
private MessageConsumer consumer;
public MessagingBridge(MessageProducer producer, MessageConsumer consumer) {
this.producer = producer;
this.consumer = consumer;
}
public void sendMessage(String topic, String message) {
producer.send(topic, message);
}
public void consumeMessages(String topic) {
consumer.consume(topic);
}
}
public class Client {
public static void main(String[] args) {
MessageProducer kafkaProducer = new KafkaProducer();
MessageConsumer kafkaConsumer = new KafkaConsumer();
MessagingBridge messagingBridge = new MessagingBridge(kafkaProducer, kafkaConsumer);
messagingBridge.sendMessage("test-topic", "Hello, Kafka!");
messagingBridge.consumeMessages("test-topic");
}
}
2.7 优缺点分析
优点:
- 减少子类数量:通过组合关系代替继承,避免类爆炸。
- 提高扩展性:形状和渲染方式可以独立扩展。
缺点:
- 增加系统复杂性:引入额外的抽象层,理解成本更高。
- 性能问题:在高性能场景中,抽象层可能引入轻微延迟。
2.8 常见误区
-
误用场景:对不需要扩展的单一维度场景滥用桥接模式,导致过度设计。
-
过多抽象:不必要的抽象增加理解成本。
2.9 问题反思与解答
- 如何避免复杂性问题?
- 在需要跨多个维度扩展时使用桥接模式,避免过度设计。
- 性能问题的优化
- 缓存实现结果,减少跨层调用的次数。
2.10 性能考虑
-
延迟调用:桥接模式增加了抽象层,可能导致延迟调用问题。
-
优化建议:在性能敏感场景中,预加载实现类或使用缓存。
2.11 与其他设计模式的关系
- 与适配器模式的关系:适配器是将已有类转换为目标接口,桥接是解耦抽象与实现。
- 与装饰者模式的关系:装饰者为对象动态添加功能,桥接是解耦功能和实现。
3. 组合模式(Composite Pattern)
3.1 背景与动机
在实际开发中,经常会遇到需要处理“部分-整体”关系的问题,例如:
-
文件系统中,文件夹和文件之间具有层次结构,文件夹可以包含文件或其他文件夹。
-
GUI 开发中,容器组件可以包含其他组件或容器。
问题:如果直接用代码表示这些层次结构,会遇到以下挑战:
-
处理方式不统一:对于文件和文件夹,我们需要分别定义操作逻辑,导致客户端代码复杂且难以维护。
-
扩展困难:每当层级关系或类型变化时,需要修改多个地方的逻辑。
// 文件类 public class File { private String name; public File(String name) { this.name = name; } public void display() { System.out.println("File: " + name); } } // 文件夹类 public class Folder { private String name; private List<File> files = new ArrayList<>(); private List<Folder> folders = new ArrayList<>(); public Folder(String name) { this.name = name; } public void addFile(File file) { files.add(file); } public void addFolder(Folder folder) { folders.add(folder); } public void display() { System.out.println("Folder: " + name); for (File file : files) { file.display(); } for (Folder folder : folders) { folder.display(); } } }
问题分析:
- 客户端复杂:需要分别处理文件和文件夹,难以统一操作。
- 层级嵌套问题:文件夹中的文件夹需要递归处理,逻辑复杂且容易出错。
3.2 模式定义与结构
组合模式通过将对象组织成树形结构来表示“部分-整体”的层次结构。客户端可以以统一的方式处理单个对象和对象组合。
+----------------+
| Component |
+----------------+
| + operation() |
+----------------+
^
|
+-------------+-------------+
| |
+----------------+ +-------------------+
| Leaf | | Composite |
+----------------+ +-------------------+
| + operation() | | + operation() |
+----------------+ | + add(Component) |
| + remove(Component)|
+-------------------+
角色说明
- Component:定义统一的接口。
- Leaf:叶子节点,表示最小单位。
- Composite:组合节点,包含子节点并实现相关操作。
3.3 源码示例
组合模式允许将对象组合成树形结构以表示“部分-整体”的层次结构。它主要用于处理树形结构的数据,例如文件系统或组织结构图。
// 抽象组件
interface FileSystemComponent {
void display(int indent);
}
// 叶子节点:文件
class File implements FileSystemComponent {
private String name;
public File(String name) {
this.name = name;
}
@Override
public void display(int indent) {
System.out.println(" ".repeat(indent) + "📄 " + name);
}
}
// 复合节点:目录
class Directory implements FileSystemComponent {
private String name;
private List<FileSystemComponent> children = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void add(FileSystemComponent component) {
children.add(component);
}
@Override
public void display(int indent) {
System.out.println(" ".repeat(indent) + "📁 " + name);
for (FileSystemComponent child : children) {
child.display(indent + 2);
}
}
}
// 客户端使用
public class CompositeDemo {
public static void main(String[] args) {
Directory root = new Directory("根目录");
Directory docs = new Directory("文档");
docs.add(new File("简历.pdf"));
docs.add(new File("报告.doc"));
Directory images = new Directory("图片");
images.add(new File("photo1.jpg"));
images.add(new File("photo2.png"));
root.add(docs);
root.add(images);
root.add(new File("说明.txt"));
root.display(0);
}
}
📁 根目录
📁 文档
📄 简历.pdf
📄 报告.doc
📁 图片
📄 photo1.jpg
📄 photo2.png
📄 说明.txt
3.4 设计原则分析
- 单一职责原则:叶子节点和组合节点分别处理自己的职责。
- 开闭原则:可以通过添加新的叶子节点或组合节点扩展系统,无需修改现有代码。
- 依赖倒置原则:客户端依赖于抽象接口
Component
。
3.5 使用场景
-
文件系统:文件和文件夹的层次结构。
-
图形系统:图形对象的树形结构(如 GUI 系统)。
-
组织结构:公司员工的层级管理(如经理和员工的层次关系)。
3.6 开源示例
- JDK 的
javax.swing.tree.DefaultMutableTreeNode
使用了 组合模式 来实现树形结构。
public class DefaultMutableTreeNode implements MutableTreeNode {
protected MutableTreeNode parent;
protected Vector children;
// 添加子节点(Composite)
public void insert(MutableTreeNode newChild, int childIndex) {
if (children == null) {
children = new Vector();
}
children.insertElementAt(newChild, childIndex);
newChild.setParent(this);
}
// 删除子节点
public void remove(int childIndex) {
MutableTreeNode child = (MutableTreeNode) getChildAt(childIndex);
children.removeElementAt(childIndex);
child.setParent(null);
}
// 叶子判断
public boolean isLeaf() {
return (getChildCount() == 0);
}
// 获取子节点
public TreeNode getChildAt(int index) {
if (children == null) {
throw new ArrayIndexOutOfBoundsException("Node has no children");
}
return (TreeNode) children.elementAt(index);
}
// 获取子节点数量
public int getChildCount() {
return (children == null) ? 0 : children.size();
}
}
DefaultMutableTreeNode
作为 组件(Component),既可以是 叶子节点(Leaf),也可以是 组合节点(Composite)。insert()
方法允许组合多个TreeNode
,实现树状结构。getChildAt()
和getChildCount()
让父节点可以访问子节点,实现 递归 结构。
2.Apache Commons Collections 提供了 CompositeCollection
,它将多个 Collection
组合成一个整体,使其表现得像单个 Collection
,实现 组合模式。
public class CompositeCollection<E> extends AbstractCollection<E> {
private Collection<E>[] collections;
// 添加集合(组合多个集合)
public synchronized void addComposited(Collection<E>... comps) {
for (Collection<E> comp : comps) {
collections.add(comp);
}
}
// 遍历所有集合中的元素
public Iterator<E> iterator() {
return new CompositeIterator<>(collections);
}
// 计算总元素个数
public int size() {
int size = 0;
for (Collection<E> coll : collections) {
size += coll.size();
}
return size;
}
}
CompositeCollection
作为 组合节点(Composite),它包含多个 Collection
,使多个集合表现得像单个集合。
addComposited()
让多个 Collection
组合在一起。
iterator()
遍历所有子集合,实现统一的操作方式,符合 组合模式的递归结构。
- Spring Security 的
SecurityConfigurer
采用 组合模式 来组织多个SecurityConfigurer
,并让它们协同工作。
public abstract class SecurityConfigurerAdapter<O, B extends SecurityBuilder<O>> implements SecurityConfigurer<O, B> {
private B securityBuilder;
// 让 SecurityConfigurer 组合在一起
public void setBuilder(B builder) {
this.securityBuilder = builder;
}
// 所有 SecurityConfigurer 共享 configure() 方法
public void configure(B builder) throws Exception {
this.securityBuilder = builder;
}
}
SecurityConfigurerAdapter
作为 组件(Component),它可以被不同的 SecurityConfigurer
继承和组合。
setBuilder()
让 SecurityConfigurer
之间可以 动态组合。
configure()
允许多个 SecurityConfigurer
协同配置,类似树状结构。
3.7 优缺点分析
优点
- 简化客户端操作:统一接口,客户端无需区分单个对象和组合对象。
- 支持灵活扩展:可以轻松添加新的叶子节点或组合节点。
缺点
- 过度设计:对于层次结构较少的场景,可能显得复杂。
- 性能问题:组合对象的操作可能需要递归,性能可能受影响。
3.8 问题反思与解答
- 如何避免过度设计?
- 仅在需要处理复杂层次结构时使用组合模式。
- 递归性能问题
- 使用缓存优化递归操作。
3.9 适用性分析
-
适合场景:存在树形层次结构,且需要统一处理叶子节点和组合节点的场景。
-
替代方案:对于简单层次结构,可以直接使用普通类实现。
3.10 常见误区
- 滥用组合模式:在层次关系简单时使用组合模式,导致不必要的复杂性。
- 不必要的统一操作:过度统一操作可能限制具体类型的能力。
3.11 与其他设计模式的关系
-
与装饰者模式的关系:装饰者模式通常是动态地添加功能,组合模式表示的是静态层次结构。
-
与责任链模式的关系:两者都可以处理对象树,但目的不同。
3.12 性能考虑
- 递归调用:对复杂层次结构的组合对象,递归可能影响性能。
- 优化建议:缓存递归结果,或者采用迭代方式遍历树形结构。
4. 装饰模式(Decorator Pattern)
4.1 背景与动机
在开发中,我们经常需要动态地为一个对象添加新功能,同时不希望修改其定义。例如:
- 图形界面组件:一个按钮可能需要同时具有边框、阴影和颜色样式。
- 文件操作:一个文件输入流可能需要添加加密、压缩等功能。
问题:如果直接修改类以添加新功能,会导致以下问题:
-
类爆炸:每种功能组合都需要创建一个新的子类。
-
违反开闭原则:添加新功能需要修改现有代码,容易引入错误。
// 抽象组件 public interface Coffee { String getDescription(); double getCost(); } // 基本咖啡 public class BasicCoffee implements Coffee { @Override public String getDescription() { return "Basic Coffee"; } @Override public double getCost() { return 5.0; } } // 加奶的咖啡 public class MilkCoffee extends BasicCoffee { @Override public String getDescription() { return super.getDescription() + " + Milk"; } @Override public double getCost() { return super.getCost() + 1.5; } } // 加糖的咖啡 public class SugarCoffee extends BasicCoffee { @Override public String getDescription() { return super.getDescription() + " + Sugar"; } @Override public double getCost() { return super.getCost() + 0.5; } }
问题分析:
-
类数量增加:每添加一种功能组合(如“加奶加糖的咖啡”),就需要新建一个子类。
-
难以扩展:新增功能(如“加巧克力”)需要修改现有类,违反开闭原则。
-
4.2 模式定义与结构
装饰模式允许动态地将责任附加到对象上。它主要用于扩展对象的功能而不修改其结构。装饰模式可以在运行时添加额外的功能。
+------------------+
| Component |
+------------------+
| + operation() |
+------------------+
^
|
+-------------+------------+
| |
+----------------+ +-------------------+
| ConcreteComp | | Decorator |
+----------------+ +-------------------+
| + operation() | | + operation() |
+----------------+ | + addBehavior() |
+-------------------+
^
|
+--------------------+
| ConcreteDecorator |
+--------------------+
| + addBehavior() |
+--------------------+
角色说明
- Component:抽象组件,定义对象的公共接口。
- ConcreteComponent:具体组件,表示被装饰的基本对象。
- Decorator:装饰器抽象类,持有一个
Component
对象。 - ConcreteDecorator:具体装饰器,实现具体的附加功能。
4.4 源码示例
咖啡订单系统
// 抽象组件
public interface Coffee {
String getDescription();
double getCost();
}
// 具体组件
public class BasicCoffee implements Coffee {
@Override
public String getDescription() {
return "Basic Coffee";
}
@Override
public double getCost() {
return 5.0;
}
}
// 抽象装饰器
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double getCost() {
return coffee.getCost();
}
}
// 具体装饰器:加奶
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + " + Milk";
}
@Override
public double getCost() {
return super.getCost() + 1.5;
}
}
// 具体装饰器:加糖
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + " + Sugar";
}
@Override
public double getCost() {
return super.getCost() + 0.5;
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Coffee coffee = new BasicCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription());
System.out.println("Cost: " + coffee.getCost());
}
}
Basic Coffee + Milk + Sugar
Cost: 7.0
4.5 设计原则分析
- 开闭原则:可以通过添加新的装饰器扩展功能,而无需修改已有类。
- 单一职责原则:每个装饰器类只负责一种功能的添加。
- 依赖倒置原则:客户端依赖抽象的
Component
接口,而不是具体实现。
4.6 实际应用场景
- 文本编辑器:为文本动态添加样式(如加粗、倾斜、下划线)。
- 日志系统:为日志添加额外功能(如格式化、加密)。
- 数据流处理:Java IO 类库中的流处理。
4.7 开源示例
- JDK 的
java.io
包大量使用了装饰模式,比如BufferedInputStream
、DataInputStream
等都基于InputStream
进行功能增强。
public class BufferedInputStream extends FilterInputStream {
protected byte buf[];
protected int count, pos;
public BufferedInputStream(InputStream in) {
super(in);
buf = new byte[8192]; // 默认 8KB 缓冲区
}
@Override
public int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count) return -1;
}
return buf[pos++] & 0xff;
}
private void fill() throws IOException {
count = in.read(buf, 0, buf.length);
pos = 0;
}
}
BufferedInputStream
继承FilterInputStream
,并在read()
方法里 增强了输入流的功能(使用缓冲区)。BufferedInputStream
包装(Decorator)了InputStream
,使原始InputStream
具有缓存能力,而无需修改原始InputStream
的代码。- 这种 增强式扩展 典型地符合装饰模式。
- Spring AOP 采用了 装饰模式 来 增强方法功能(如事务、日志、权限等),代理对象在调用方法时,动态添加增强逻辑。
Spring 的 ProxyFactory
允许我们使用 JDK 动态代理 或 CGLIB 来装饰对象。
public class ProxyFactory extends AdvisedSupport {
public Object getProxy() {
return createAopProxy().getProxy();
}
protected final AopProxy createAopProxy() {
return (this.targetClass != null) ?
new CglibAopProxy(this) : new JdkDynamicAopProxy(this);
}
}
在 JdkDynamicAopProxy
里,Spring 利用 JDK 动态代理增强原始方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MethodInvocation invocation = new ReflectiveMethodInvocation(target, method, args, interceptors);
return invocation.proceed();
}
-
ProxyFactory
创建代理对象(装饰器),代理对象包装了目标对象,并在invoke()
里增强了方法逻辑(比如日志、事务)。 -
增强功能与核心业务解耦,可以动态添加事务、日志等功能,而不影响原始业务代码。
- Logback 采用装饰模式 增强日志功能,比如
Appender
允许添加 过滤器(Filter),对日志进行筛选和格式化。
public abstract class AppenderBase<E> implements Appender<E> {
protected List<Filter<E>> filterList;
public void doAppend(E event) {
if (!isStarted()) return;
for (Filter<E> f : filterList) {
if (f.decide(event) == FilterReply.DENY) return;
}
append(event);
}
protected abstract void append(E event);
}
AppenderBase
装饰了日志输出的功能,允许对日志进行 过滤 处理。
Filter
作为装饰器,可以动态添加到 AppenderBase
,比如:
appender.addFilter(new MyLogFilter());
这样在 不修改 Appender 代码 的情况下,日志功能得到了增强。
4.在 javax.servlet.Filter
体系中,Filter 作为装饰器,可以动态增强 HTTP 请求和响应的处理逻辑。
public class CompressionFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
CompressionResponseWrapper responseWrapper = new CompressionResponseWrapper(response);
chain.doFilter(request, responseWrapper);
}
}
CompressionFilter
包装了 ServletResponse
,为其增加压缩功能。
FilterChain
允许多个 Filter
级联使用,类似装饰模式的 链式增强。
4.8 优缺点分析
优点
- 灵活性高:可以动态添加或组合功能。
- 遵循开闭原则:扩展功能无需修改原有代码。
缺点
- 对象数量增加:每个装饰器都需要创建一个新对象。
- 调试困难:复杂的装饰器链可能难以追踪。
4.9 问题反思与解答
- 如何避免装饰器链过长?
- 控制装饰器的数量,或通过配置集中管理。
- 性能问题:
- 装饰器链过长可能影响性能,可以使用组合模式优化。
4.10 适用性分析
- 适合场景:需要动态为对象添加功能,而不想修改对象的定义。
- 替代方案:对于功能固定的场景,可以直接使用子类。
4.11 常见误区
- 滥用装饰模式:即使功能简单,也使用多个装饰器。
- 与子类混淆:装饰器不是子类,不能直接替代继承。
4.12 与其他设计模式的关系
- 与代理模式的关系:代理模式关注控制访问,装饰模式关注扩展功能。
- 与责任链模式的关系:装饰模式应用功能,责任链模式传递请求。
4.13 性能考虑
- 对象创建成本:装饰器链增加了对象数量。
- 优化建议:对装饰器链进行缓存或合并,减少额外的对象开销。
5. 外观模式(Facade Pattern)
5.1 背景与动机
当系统变得复杂且包含多个子系统时,客户端需要与这些子系统交互,这会导致以下问题:
- 耦合性高:客户端需要直接依赖多个子系统的接口。
- 使用复杂:客户端需要了解子系统的详细实现,增加了学习成本。
- 维护困难:子系统发生变化时,客户端可能需要随之修改。
问题:如何为客户端提供一个简单的接口,以简化子系统的使用,并减少客户端与子系统之间的耦合?
// 照明子系统
class LightingSystem {
void setBrightness(int level) {
System.out.println("灯光亮度设置为:" + level + "%");
}
void setColorTemperature(int kelvin) {
System.out.println("色温设置为:" + kelvin + "K");
}
}
// 空调子系统
class AirConditioningSystem {
void setTemperature(double celsius) {
System.out.println("空调温度设置为:" + celsius + "℃");
}
void setMode(String mode) {
System.out.println("空调模式:" + mode);
}
}
// 影音子系统
class EntertainmentSystem {
void startMovieMode() {
System.out.println("启动影院模式:\n- 降下投影幕布\n- 打开4K投影仪\n- 开启环绕音响");
}
void stopMovieMode() {
System.out.println("关闭影院设备");
}
}
// 智能家居外观接口
class SmartHomeFacade {
private final LightingSystem lighting;
private final AirConditioningSystem ac;
private final EntertainmentSystem entertainment;
public SmartHomeFacade() {
this.lighting = new LightingSystem();
this.ac = new AirConditioningSystem();
this.entertainment = new EntertainmentSystem();
}
// 一键观影模式
public void startMovieNight() {
lighting.setBrightness(20);
lighting.setColorTemperature(2700);
ac.setTemperature(22.5);
ac.setMode("静音");
entertainment.startMovieMode();
}
// 一键离家模式
public void activateAwayMode() {
lighting.setBrightness(0);
ac.setTemperature(28);
entertainment.stopMovieMode();
System.out.println("安防系统已启动");
}
}
public class Client {
public static void main(String[] args) {
SmartHomeFacade smartHome = new SmartHomeFacade();
// 观影模式一键启动
smartHome.startMovieNight();
System.out.println("\n====== 三小时后 ======\n");
// 离家模式一键设置
smartHome.activateAwayMode();
}
}
灯光亮度设置为:20%
色温设置为:2700K
空调温度设置为:22.5℃
空调模式:静音
启动影院模式:
- 降下投影幕布
- 打开4K投影仪
- 开启环绕音响
====== 三小时后 ======
灯光亮度设置为:0%
空调温度设置为:28.0℃
关闭影院设备
安防系统已启动
问题分析:
- 客户端复杂性:客户端必须直接与每个子系统交互,代码变得复杂。
- 耦合性高:客户端依赖多个子系统,任何子系统变化都会影响客户端。
- 缺乏一致性:子系统的接口风格可能不统一,增加了使用难度。
5.2 模式定义与结构
外观模式为子系统中的一组接口提供一个统一的高层接口。它主要用于简化复杂子系统的使用,使得客户端可以更方便地使用系统。
+---------+ +-----------------+
| Client | -----> | Facade |
+---------+ +-----------------+
|
+-----------------------+----------------------+
| | |
+-------------+ +-------------+ +-------------+
| Subsystem A | | Subsystem B | | Subsystem C |
+-------------+ +-------------+ +-------------+
角色说明
- Client(客户端):通过外观对象与子系统交互。
- Facade(外观):定义一个高层接口,用于简化客户端的调用。
- Subsystem(子系统):实现子系统的具体逻辑,客户端无需直接使用。
5.3 源码示例
家庭影院系统
// 子系统 1:灯光控制
class Light {
public void on() {
System.out.println("Light is ON");
}
public void off() {
System.out.println("Light is OFF");
}
}
// 子系统 2:音响控制
class SoundSystem {
public void play() {
System.out.println("SoundSystem is PLAYING");
}
public void stop() {
System.out.println("SoundSystem is STOPPED");
}
}
// 子系统 3:屏幕控制
class Screen {
public void down() {
System.out.println("Screen is DOWN");
}
public void up() {
System.out.println("Screen is UP");
}
}
// 外观类
class HomeTheaterFacade {
private Light light;
private SoundSystem soundSystem;
private Screen screen;
public HomeTheaterFacade(Light light, SoundSystem soundSystem, Screen screen) {
this.light = light;
this.soundSystem = soundSystem;
this.screen = screen;
}
public void startMovie() {
System.out.println("Starting movie...");
light.off();
screen.down();
soundSystem.play();
}
public void endMovie() {
System.out.println("Ending movie...");
light.on();
screen.up();
soundSystem.stop();
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Light light = new Light();
SoundSystem soundSystem = new SoundSystem();
Screen screen = new Screen();
HomeTheaterFacade homeTheater = new HomeTheaterFacade(light, soundSystem, screen);
homeTheater.startMovie();
homeTheater.endMovie();
}
}
Starting movie...
Light is OFF
Screen is DOWN
SoundSystem is PLAYING
Ending movie...
Light is ON
Screen is UP
SoundSystem is STOPPED
5.4 设计原则分析
- 单一职责原则:外观类负责将客户端与子系统解耦,简化调用逻辑。
- 迪米特法则(最少知识原则):客户端只需要知道外观类,而无需了解子系统的细节。
- 开闭原则:可以通过扩展外观类增加新功能,而无需修改子系统。
5.5 实际应用场景
- 模块化系统:例如微服务架构中,通过网关提供统一的接口。
- 复杂操作封装:如数据库连接池管理。
- 跨平台兼容:隐藏平台差异,通过统一接口调用。
5.6 开源示例
1.Spring 提供了 JdbcTemplate
作为 数据库访问的外观(Facade),它封装了 Connection、Statement、ResultSet的处理逻辑,让开发者无需关心底层 JDBC API 的复杂性。
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
@Override
public <T> T query(String sql, ResultSetExtractor<T> rse) throws DataAccessException {
return execute((Connection con) -> {
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
return rse.extractData(rs);
});
}
@Override
public int update(String sql) throws DataAccessException {
return execute((Connection con) -> {
PreparedStatement ps = con.prepareStatement(sql);
return ps.executeUpdate();
});
}
}
JdbcTemplate
封装了 Connection、Statement、ResultSet 相关的数据库操作,提供了更简洁的query()
、update()
等方法,避免直接操作 JDBC API。- 客户端 只需调用
JdbcTemplate
,无需关心底层 SQL 执行细节,极大简化了数据库访问逻辑。
2.Netty 通过 ChannelPipeline
封装了多个 Handler 的管理,它充当了外观,简化了用户操作 Netty 事件处理链的方式。
public interface ChannelPipeline extends Iterable<Entry<String, ChannelHandler>> {
ChannelPipeline addLast(String name, ChannelHandler handler);
ChannelPipeline remove(ChannelHandler handler);
ChannelHandlerContext context(ChannelHandler handler);
void fireChannelRead(Object msg);
}
ChannelPipeline
封装了多个ChannelHandler
,简化了外部访问 Netty 事件处理机制的方式。- 调用者 无需直接操作
ChannelHandler
逻辑,只需调用addLast()
、remove()
等方法,Netty 会自动管理处理链。
- MyBatis 的
SqlSession
充当了 数据库操作的外观(Facade),封装了 Executor、MappedStatement、Configuration 等组件,让用户能够通过一个简单接口执行 SQL 语句。
public interface SqlSession extends Closeable {
<T> T selectOne(String statement, Object parameter);
<E> List<E> selectList(String statement, Object parameter);
int insert(String statement, Object parameter);
int update(String statement, Object parameter);
int delete(String statement, Object parameter);
}
-
SqlSession
封装了 SQL 执行流程(Executor、Configuration、Transaction),调用者不需要关心数据库连接、SQL 解析等细节。 -
对外暴露
selectOne()
、update()
等方法,简化数据库操作。
4.SLF4J(Simple Logging Facade for Java)是一个典型的 外观模式,它提供了统一的日志接口,让用户可以使用不同的日志实现(Logback、Log4j、JUL)。
public interface Logger {
void info(String msg);
void debug(String msg);
void error(String msg);
}
public class LoggerFactory {
public static Logger getLogger(Class<?> clazz) {
return new Slf4jLoggerAdapter(clazz);
}
}
LoggerFactory
封装了日志框架的创建逻辑,让用户可以通过getLogger()
获取日志对象,而无需关心具体的日志实现。- 对外提供统一接口 (
Logger
),底层可以适配 Logback、Log4j 等日志框架。
5.Spring CacheManager
作为缓存系统的外观,它封装了 多个缓存实现(Redis、EhCache、Guava 等),让用户可以通过统一 API 操作缓存,而无需关心底层存储方式。
public interface CacheManager {
Cache getCache(String name);
}
public class ConcurrentMapCacheManager implements CacheManager {
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>();
@Override
public Cache getCache(String name) {
return cacheMap.computeIfAbsent(name, ConcurrentMapCache::new);
}
}
CacheManager
提供了统一的getCache()
方法,无论底层使用的是 Redis、EhCache 还是 Guava,都可以通过相同接口访问。- 客户端 只需调用
CacheManager.getCache()
,无需直接操作 Redis、EhCache API。
5.7 优缺点分析
优点
- 简化使用:隐藏子系统的复杂性,提供统一接口。
- 降低耦合:客户端与子系统解耦,增强代码可维护性。
缺点
- 单点问题:外观类可能成为系统中的单点,过于复杂时影响维护。
- 可能导致封装过度:外观类可能屏蔽了部分子系统的高级功能。
5.8 问题反思与解答
-
外观类过于复杂:
- 将功能拆分为多个外观类,每个外观类管理一个子系统。
-
子系统直接调用问题:
- 强制子系统只能通过外观类访问,避免直接调用。
5.9 适用性分析
-
适合场景:需要简化复杂子系统的使用。
-
替代方案:对于较简单的场景,可以直接使用子系统而不需要外观。
5.10 常见误区
- 滥用外观模式:即使系统很简单,也使用外观类,导致过度封装。
- 过于依赖外观类:忽略了子系统的高级功能。
5.11 与其他设计模式的关系
- 与适配器模式的关系:适配器模式关注接口兼容,外观模式关注简化使用。
- 与中介者模式的关系:中介者模式集中管理对象交互,而外观模式提供简化接口。
5.12 性能考虑
- 方法调用开销:外观模式可能引入额外的方法调用。
- 优化建议:确保外观类仅封装必要的功能,避免不必要的复杂性。
6. 享元模式(Flyweight Pattern)
6.1 背景与动机
为什么会出现这个设计模式?
在某些场景中,系统可能需要大量的小对象来表示数据。这些对象往往具有相似的属性,会导致以下问题:
- 内存浪费:重复存储相同或类似的数据。
- 性能下降:频繁创建和销毁对象,增加了系统的内存开销和垃圾回收负担。
- 难以维护:大量重复对象的存在会增加系统复杂性。
问题:如何通过共享技术减少这些对象的内存开销?
以下示例模拟一个绘图应用,每次绘制时都会创建独立的对象,导致内存浪费。
// 森林中每棵树都需要一个独立的对象
class Tree {
private String type;
private String color;
public Tree(String type, String color) {
this.type = type;
this.color = color;
}
public void draw(int x, int y) {
System.out.println("Drawing a " + type + " tree with color " + color + " at (" + x + ", " + y + ")");
}
}
// 客户端代码
public class Forest {
private List<Tree> trees = new ArrayList<>();
public void plantTree(String type, String color, int x, int y) {
Tree tree = new Tree(type, color);
tree.draw(x, y);
trees.add(tree);
}
public static void main(String[] args) {
Forest forest = new Forest();
forest.plantTree("Oak", "Green", 10, 20);
forest.plantTree("Oak", "Green", 30, 40);
forest.plantTree("Pine", "Dark Green", 50, 60);
}
}
问题分析:
- 内存浪费:
Tree
对象中包含重复数据,如树的类型和颜色。 - 性能问题:每次绘制都需要创建新的对象,频繁的内存分配和回收增加了性能开销。
6.2 模式定义与结构
享元模式通过共享技术有效支持大量细粒度对象的复用。将对象的状态分为 内部状态(可共享)和 外部状态(每次变化),通过共享内部状态来减少内存使用。
+---------+ +-----------------+
| Client | -------> | Flyweight |
+---------+ +-----------------+
/ \
+----------------+ +----------------+
| ConcreteFlyweight UnsharedFlyweight |
+-----------------+ +-----------------+
角色说明
- Flyweight(享元):定义共享对象的接口。
- ConcreteFlyweight(具体享元):实现共享对象,存储可共享的内部状态。
- UnsharedFlyweight(非共享享元):不支持共享的对象。
- FlyweightFactory(享元工厂):负责创建和管理享元对象。
- Client(客户端):使用享元对象。
6.3 源码示例
优化森林绘图
// 享元类
interface Tree {
void draw(int x, int y);
}
// 具体享元类
class TreeType implements Tree {
private String type;
private String color;
public TreeType(String type, String color) {
this.type = type;
this.color = color;
}
@Override
public void draw(int x, int y) {
System.out.println("Drawing a " + type + " tree with color " + color + " at (" + x + ", " + y + ")");
}
}
// 享元工厂
class TreeFactory {
private static final Map<String, Tree> treeMap = new HashMap<>();
public static Tree getTree(String type, String color) {
String key = type + ":" + color;
if (!treeMap.containsKey(key)) {
treeMap.put(key, new TreeType(type, color));
System.out.println("Creating new TreeType: " + key);
}
return treeMap.get(key);
}
}
// 客户端代码
public class Forest {
private List<TreePosition> trees = new ArrayList<>();
public void plantTree(String type, String color, int x, int y) {
Tree tree = TreeFactory.getTree(type, color);
trees.add(new TreePosition(tree, x, y));
}
public void drawForest() {
for (TreePosition tree : trees) {
tree.draw();
}
}
public static void main(String[] args) {
Forest forest = new Forest();
forest.plantTree("Oak", "Green", 10, 20);
forest.plantTree("Oak", "Green", 30, 40);
forest.plantTree("Pine", "Dark Green", 50, 60);
forest.drawForest();
}
}
// 非共享享元类
class TreePosition {
private Tree tree;
private int x, y;
public TreePosition(Tree tree, int x, int y) {
this.tree = tree;
this.x = x;
this.y = y;
}
public void draw() {
tree.draw(x, y);
}
}
Creating new TreeType: Oak:Green
Creating new TreeType: Pine:Dark Green
Drawing a Oak tree with color Green at (10, 20)
Drawing a Oak tree with color Green at (30, 40)
Drawing a Pine tree with color Dark Green at (50, 60)
优化结果:
- 内存效率提升:相同类型的树对象被复用,不再重复创建。
- 性能优化:减少了频繁的对象创建开销。
6.4 设计原则分析
- 单一职责原则:享元模式将对象的内部状态与外部状态分离,使得每个对象只关注自身的状态管理。
- 开闭原则:通过工厂管理享元对象的创建和共享,可以扩展新的享元对象,而不影响已有代码。
- 迪米特法则:客户端仅与享元工厂和共享对象交互,而无需了解对象的内部实现细节。
6.5 实际应用场景
- 文本编辑器:字符的字体和样式可以共享。
- 游戏开发:同类对象(如树木、敌人)可以复用共享的资源。
- 缓存系统:共享重复的缓存对象。
6.6 开源示例
1.JDK 的 ThreadPoolExecutor
采用 享元模式 复用线程,避免频繁创建和销毁线程。
public class ThreadPoolExecutor extends AbstractExecutorService {
private final BlockingQueue<Runnable> workQueue; // 任务队列(享元池)
private final HashSet<Worker> workers = new HashSet<>(); // 线程池
private final ReentrantLock mainLock = new ReentrantLock();
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
if (workers.size() < corePoolSize) {
addWorker(command);
} else {
workQueue.offer(command); // 任务进入队列(享元池)
}
}
}
- 线程池 复用线程对象,避免重复创建和销毁线程,大大提高性能。
workQueue
存储待执行任务(享元对象),减少重复创建任务实例的开销。- 共享
Worker
线程实例,减少系统开销。
2.MyBatis 的二级缓存实现是一个典型的享元模式应用,多个 SqlSession
会话共享同一个缓存对象,避免了重复查询数据,从而提高了性能。MyBatis 提供了一个可以被多个会话共享的缓存机制,通常使用内存缓存(如 EhCache
)来保存查询结果。
MyBatis 缓存的核心概念
- 一级缓存:每个
SqlSession
内部都有一个缓存,用于缓存当前会话中执行的 SQL 查询结果,SqlSession
关闭时会清除一级缓存。 - 二级缓存:不同
SqlSession
之间可以共享缓存,二级缓存是跨SqlSession
的,通常被配置为一个全局的缓存。
示例:MyBatis 二级缓存配置与实现
在 mybatis-config.xml
中启用二级缓存:
<configuration>
<!-- 启用二级缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<mappers>
<mapper resource="com/example/mapper/UserMapper.xml"/>
</mappers>
</configuration>
在映射文件 UserMapper.xml
中配置缓存:
<mapper namespace="com.example.mapper.UserMapper">
<!-- 启用二级缓存 -->
<cache/>
<select id="findUserById" resultType="com.example.model.User">
SELECT id, name, age FROM users WHERE id = #{id}
</select>
</mapper>
代码示例:共享缓存
假设有两个 SqlSession
对象,其中一个执行了查询并缓存了结果,另一个执行相同的查询时将从缓存中获取结果,而不是重新查询数据库。
public class MyBatisCacheExample {
public static void main(String[] args) {
// 获取 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = MyBatisUtil.getSqlSessionFactory();
// 第一个 SqlSession 查询
try (SqlSession session1 = sqlSessionFactory.openSession()) {
UserMapper userMapper1 = session1.getMapper(UserMapper.class);
User user1 = userMapper1.findUserById(1);
System.out.println(user1);
}
// 第二个 SqlSession 查询,应该从缓存中获取
try (SqlSession session2 = sqlSessionFactory.openSession()) {
UserMapper userMapper2 = session2.getMapper(UserMapper.class);
User user2 = userMapper2.findUserById(1);
System.out.println(user2);
}
}
}
User{id=1, name='John', age=30} // 第一次查询,缓存未命中
User{id=1, name='John', age=30} // 第二次查询,缓存命中
分析
- 在
session1
执行查询时,User
对象的查询结果被存入了缓存(在 MyBatis 中通常是PerpetualCache
)。 - 在
session2
执行查询时,缓存命中,直接返回查询结果,而不是再次访问数据库。
优点
- 减少数据库访问:通过二级缓存,多个会话共享查询结果,避免了重复查询。
- 提高性能:缓存机制提高了查询效率,减少了数据库的负担。
缺点
-
缓存失效:如果数据被修改,缓存中的数据需要失效,否则可能会读取到过期数据。
-
内存开销:缓存需要占用内存,过多的缓存数据可能导致内存压力。
3.JDK 的 String
类实现了 享元模式,相同字符串被存储在字符串常量池(String Pool)中,避免重复创建对象。
public final class String {
private final char value[];
public static String valueOf(char data[]) {
return new String(data);
}
public String intern() {
return StringPool.add(this); // 共享字符串对象
}
}
-
相同的字符串 被存入 字符串常量池,如果已有相同字符串,则返回池中的实例,避免重复创建对象。
-
intern()
方法用于 将字符串放入常量池,以便共享。
4.Tomcat 的 DataSource
复用数据库连接对象,避免频繁创建和关闭数据库连接,提高系统性能。
public class DataSource {
private final GenericObjectPool<Connection> connectionPool;
public Connection getConnection() throws SQLException {
return connectionPool.borrowObject(); // 复用连接
}
public void returnConnection(Connection conn) {
connectionPool.returnObject(conn); // 归还连接
}
}
connectionPool
作为享元池,复用数据库连接,避免重复创建和销毁Connection
。borrowObject()
从连接池获取已有的Connection
,returnObject()
归还连接
5.Netty 的 ByteBuf
采用 享元模式 复用缓冲区,避免频繁分配和释放内存。
public abstract class AbstractByteBufAllocator implements ByteBufAllocator {
private final PoolArena<ByteBuf>[] arenas;
public ByteBuf buffer(int initialCapacity) {
return arenas[0].allocate(initialCapacity); // 复用 ByteBuf
}
}
-
ByteBuf
采用对象池(arenas)存储缓冲区,避免重复创建对象。 -
allocate()
方法从池中取出ByteBuf
,如果池中没有,则创建新对象。
6.7 优缺点分析
优点
- 减少内存使用:通过共享减少重复对象的数量。
- 提高性能:减少对象创建和垃圾回收的开销。
缺点
- 代码复杂性增加:需要分离内部状态和外部状态。
- 线程安全问题:共享对象可能引发多线程访问冲突。
6.8 问题反思与解答
- 如何选择内部和外部状态?
- 内部状态应为共享部分,外部状态则为变化部分。
- 享元对象的线程安全性问题如何解决?
- 使用线程安全的数据结构或同步机制。
6.9 常见误区
- 所有对象都使用享元:不适合高动态性的对象。
- 忽视线程安全:在多线程环境中滥用共享对象,可能引发数据冲突。
6.10 与其他设计模式的关系
- 与工厂模式的关系:享元模式依赖工厂模式创建和管理共享对象。
- 与单例模式的关系:共享对象的管理逻辑可以结合单例模式实现。
6.11 性能考虑
-
内存节约:减少对象数量以降低内存开销。
-
共享管理开销:共享池的管理会增加一定的复杂度和运行时开销。
7. 代理模式(Proxy Pattern)
7.1 背景与动机
代理模式的出现主要是为了解决在不改变目标对象的情况下,对目标对象进行某些额外的操作。通常,代理模式用于控制对某个对象的访问,或者为某个对象添加额外的功能。它通过创建一个代理类来间接访问真实对象,常见的使用场景包括:延迟加载、权限控制、日志记录、缓存、网络请求等。
没有代理模式时,我们可能会在每个调用中直接操作目标对象。比如,想要对某个对象的访问进行权限控制,或者想在每次调用时记录日志,但直接修改目标类的代码可能会导致代码的混乱和重复工作。
public class RealSubject {
public void request() {
System.out.println("Request made to RealSubject");
}
}
public class Client {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
realSubject.request(); // 直接调用,无法控制或修改行为
}
}
问题:
- 如果需要在
request
方法前后做一些权限检查或日志记录,直接修改RealSubject
类会导致代码侵入性较强,难以维护。 - 每次都要对
RealSubject
进行修改,且可能影响到其他地方的使用。
解决方式:
通过引入代理类来解耦业务逻辑和额外的功能,代理类可以在不改变目标类的情况下,插入所需的功能。
7.2 模式定义与结构
代理模式定义了一个代理类,作为真实对象的替代者,代理对象通过委托来访问真实对象,甚至可以在调用真实对象的方法之前或之后添加额外的功能。
+------------------+ +-------------------+
| Subject |<-------| Proxy |
+------------------+ +-------------------+
| + request() | | + request() |
+------------------+ +-------------------+
^
|
+-------------------+
| RealSubject |
+-------------------+
| + request() |
+-------------------+
7.3 代码示例
静态代理通常是通过在编译时就确定代理类来实现的。代理类直接实现与目标类相同的接口,并在接口方法中调用目标类的相应方法。
// 目标类
public class RealSubject {
public void request() {
System.out.println("RealSubject: Handling request");
}
}
// 代理类
public class ProxySubject {
private RealSubject realSubject;
public ProxySubject(RealSubject realSubject) {
this.realSubject = realSubject;
}
public void request() {
System.out.println("Proxy: Pre-processing before calling real subject");
realSubject.request();
System.out.println("Proxy: Post-processing after calling real subject");
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
ProxySubject proxy = new ProxySubject(realSubject);
proxy.request(); // 通过代理类调用 request 方法
}
}
Proxy: Pre-processing before calling real subject
RealSubject: Handling request
Proxy: Post-processing after calling real subject
说明:
- 在客户端调用代理对象时,代理类可以在请求前后加入额外的操作,如权限检查、日志记录等。
- 代理类充当了一个“中介者”的角色,处理了与目标类的交互。
动态代理
Java 提供了 Proxy
类和 InvocationHandler
接口来实现动态代理。动态代理允许我们在运行时为某个对象创建代理类,而不需要提前编写代理类。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 目标接口
public interface Subject {
void request();
}
// 目标类
public class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject: Handling request");
}
}
// 动态代理类
public class ProxyHandler implements InvocationHandler {
private Object target;
public ProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Proxy: Pre-processing before calling real subject");
Object result = method.invoke(target, args);
System.out.println("Proxy: Post-processing after calling real subject");
return result;
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
Subject proxy = (Subject) Proxy.newProxyInstance(
realSubject.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(),
new ProxyHandler(realSubject)
);
proxy.request(); // 通过动态代理调用 request 方法
}
}
Proxy: Pre-processing before calling real subject
RealSubject: Handling request
Proxy: Post-processing after calling real subject
说明:
ProxyHandler
类通过InvocationHandler
接口来拦截方法调用,允许在方法调用前后插入额外的逻辑。- 使用
Proxy.newProxyInstance()
来创建代理类,可以在运行时动态生成代理对象,减少了硬编码的代理类的数量。
7.4 设计原则分析
- 单一职责原则 (SRP):代理类负责对目标类的调用进行封装,避免在目标类中加入额外的功能,保持了目标类的单一职责。
- 开闭原则 (OCP):代理模式遵循开闭原则,代理类可以在不修改目标类代码的情况下增加功能。
- 依赖倒置原则 (DIP):客户端依赖于抽象的
Subject
接口,而不是具体的目标类和代理类,降低了耦合度。
7.5 实际应用场景
- 延迟加载:当需要延迟某些资源的加载时,可以使用代理模式。例如,数据库查询时,只有在真正需要数据时才执行查询。
- 权限控制:通过代理类进行权限控制,可以在调用目标方法之前进行权限验证。
- 日志记录:使用代理类在方法执行前后记录日志,而不需要修改目标类。
7.6 开源示例
1.Spring AOP 是基于代理模式实现的,通过动态代理为目标对象添加横切功能(如事务管理、日志记录等)。下面是一个使用 Spring AOP 进行事务管理的例子。
// 目标类
public class UserService {
public void addUser() {
System.out.println("User added");
}
}
// 切面类
@Aspect
public class TransactionAspect {
@Before("execution(* UserService.*(..))")
public void beginTransaction() {
System.out.println("Transaction started");
}
@After("execution(* UserService.*(..))")
public void commitTransaction() {
System.out.println("Transaction committed");
}
}
通过代理模式,Spring 在调用 UserService
方法时,能够在调用前后插入事务处理逻辑。
7.7 优缺点分析
- 优点
- 可以控制对真实对象的访问,例如延迟加载、权限验证、日志记录等。
- 代理类解耦了目标类和附加功能,使得目标类更加简洁,功能的扩展更加灵活。
- 缺点
- 增加了系统的复杂度,代理类和目标类之间的关系需要更清晰的设计。
- 可能导致性能下降,尤其是当代理逻辑较为复杂时,性能开销较大。
7.8 问题反思与解答
问题:代理模式在性能上的开销如何避免?
解答:代理模式的性能开销通常来自于方法调用的拦截和额外逻辑。如果代理逻辑较为复杂,可能会对性能产生影响。为了减少这种开销,可以通过缓存、异步处理等方式来优化性能。
7.9 适用性分析
- 适合使用代理模式的场景
- 需要为某个对象添加额外功能时(如缓存、日志、安全等)。
- 想在访问对象前后插入一些额外的处理时。
- 替代方案:如果不需要在方法调用前后进行处理,可以直接使用对象,不必引入代理模式。
7.10 常见误区
- 误区一:过度使用代理模式,导致系统复杂度增加。在不需要额外功能的情况下,不必引入代理。
- 误区二:在性能关键的代码中使用代理模式时,忽视了代理的性能开销。
7.11 模式变种
- CGLIB 代理:在某些情况下,Java 动态代理无法处理非接口的类,这时可以使用 CGLIB 代理,它通过字节码生成技术动态生成目标类的子类。
- JDK 动态代理:基于接口的代理,只能代理实现了接口的类,灵活且功能强大。
7.12 与其他设计模式的关系
- 与装饰模式的关系:代理模式和装饰模式都通过包裹目标对象来增强功能,但装饰模式通常是增加功能,而代理模式更多用于控制对目标对象的访问。
- 与适配器模式的关系:代理模式和适配器模式都通过封装原有对象进行方法调用,但适配器模式是为了让两个不兼容的接口工作在一起,而代理模式则是为了增加对目标对象的控制。
7.13 性能考虑
- 性能影响:代理模式可能带来一定的性能损耗,尤其是在使用动态代理时。每次调用代理对象的
invoke
方法都会增加一次方法调用,可能会影响性能。 - 优化方法:可以通过缓存、异步处理或者减少代理类的使用次数来优化性能。
3. 行为型设计模式
行为型设计模式主要关注对象之间的交互和责任分配。它们帮助我们更好地分配和管理对象的职责,使得系统的设计更具灵活性和可扩展性。本文将详细介绍十一种常见的行为型设计模式,并通过代码示例展示它们的实际应用。
1. 责任链模式(Chain of Responsibility Pattern)
1.1 背景与动机
责任链模式的出现是为了避免请求发送者与请求处理者之间的耦合。它通过将多个处理对象组织成一条链,每个处理者对请求进行处理或者传递给链上的下一个处理者。这样发送者无需知道请求的具体处理者,降低了系统的耦合度。
经典问题:
没有使用责任链模式时,通常会采用嵌套的 if-else
或者 switch
来判断并处理请求。每次添加新的处理逻辑时,都需要修改现有的代码,这不仅增加了耦合度,还使得系统变得不易扩展。
public class Handler {
public void handleRequest(String requestType) {
if ("TypeA".equals(requestType)) {
System.out.println("Handler: Handling request of TypeA");
} else if ("TypeB".equals(requestType)) {
System.out.println("Handler: Handling request of TypeB");
} else {
System.out.println("Handler: Unknown request type");
}
}
}
public class Client {
public static void main(String[] args) {
Handler handler = new Handler();
handler.handleRequest("TypeA"); // 请求直接处理,没有灵活性
handler.handleRequest("TypeB");
}
}
public class Handler {
public void handleRequest(String requestType) {
if ("TypeA".equals(requestType)) {
System.out.println("Handler: Handling request of TypeA");
} else if ("TypeB".equals(requestType)) {
System.out.println("Handler: Handling request of TypeB");
} else {
System.out.println("Handler: Unknown request type");
}
}
}
public class Client {
public static void main(String[] args) {
Handler handler = new Handler();
handler.handleRequest("TypeA"); // 请求直接处理,没有灵活性
handler.handleRequest("TypeB");
}
}
问题:
- 如果需要添加新的处理类型,每次都要修改
Handler
类,违反了开闭原则。 - 每次请求都需要判断所有可能的处理类型,导致代码复杂且不易维护。
1.2 模式定义与结构
责任链模式允许将请求沿着处理链传递,直到找到一个处理该请求的对象为止。它主要用于解耦请求的发送者和接收者,从而使得请求的处理更加灵活。
+----------------+ +---------------------+
| Handler |<--------->| ConcreteHandler |
|----------------| |---------------------|
| - nextHandler | | - nextHandler |
|----------------| |---------------------|
| + setNextHandler() | | + handleRequest() |
| + handleRequest() | +---------------------+
+----------------+
|
v
+------------------+
| Client |
|------------------|
| + handleRequest() |
+------------------+
- Handler:这是一个抽象类或接口,定义了处理请求的接口。它可以有一个
nextHandler
属性,用于将请求传递给链上的下一个处理者。每个处理者都需要实现handleRequest()
方法。 - ConcreteHandler:继承自
Handler
类,表示具体的请求处理者。每个ConcreteHandler
都可以处理某种类型的请求,并决定是否将请求传递给下一个处理者。如果当前处理者可以处理该请求,它将执行处理逻辑;否则,将请求传递给链中的下一个处理者。 - Client:客户端负责启动请求并将其传递给责任链的第一个处理者。它不需要关心请求如何被处理,只需要将请求交给责任链中的第一个处理者。
1.3 代码示例
// 1. 定义抽象处理器
abstract class Logger {
protected LogLevel level;
protected Logger next;
public void setNext(Logger next) {
this.next = next;
}
public void log(LogLevel level, String message) {
if (this.level.getLevel() <= level.getLevel()) {
writeMessage(message);
}
if (next != null) {
next.log(level, message);
}
}
abstract protected void writeMessage(String message);
}
// 2. 具体处理器
class ConsoleLogger extends Logger {
public ConsoleLogger(LogLevel level) {
this.level = level;
}
@Override
protected void writeMessage(String message) {
System.out.println("ConsoleLogger: " + message);
}
}
class FileLogger extends Logger {
public FileLogger(LogLevel level) {
this.level = level;
}
@Override
protected void writeMessage(String message) {
System.out.println("FileLogger: " + message);
}
}
// 3. 日志级别枚举
enum LogLevel {
DEBUG(1), INFO(2), WARNING(3), ERROR(4);
private int level;
LogLevel(int level) { this.level = level; }
public int getLevel() { return level; }
}
// 4. 客户端使用
public class Client {
public static void main(String[] args) {
Logger consoleLogger = new ConsoleLogger(LogLevel.DEBUG);
Logger fileLogger = new FileLogger(LogLevel.INFO);
// 构建责任链
consoleLogger.setNext(fileLogger);
// 发送日志请求
consoleLogger.log(LogLevel.INFO, "This is an info message");
}
}
1.4 设计原则分析
- 开闭原则:责任链模式允许添加新的处理对象时无需修改现有代码,通过扩展链条来增加处理逻辑,符合开闭原则。
- 单一职责原则:每个处理者对象只负责特定的请求类型,符合单一职责原则。
1.5 实际应用场景
-
事件处理系统:例如 GUI 事件处理程序,当用户点击按钮时,可以依次通过不同的事件处理器链进行处理。
-
日志处理系统:不同级别的日志处理器(如 Debug、Info、Error)可以通过责任链模式处理日志。
-
审批流程:在公司内部审批流程中,审批请求会依次经过不同层级的审批人员处理,直到最终结果。
1.6 开源示例
1.Java Servlet 过滤器 是 责任链模式的经典实现,多个 Filter
形成处理链,依次处理请求。
public interface Filter {
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
}
public class FilterChain {
private List<Filter> filters = new ArrayList<>();
private int currentIndex = 0;
public void doFilter(ServletRequest request, ServletResponse response) {
if (currentIndex < filters.size()) {
filters.get(currentIndex++).doFilter(request, response, this);
}
}
}
-
FilterChain
管理多个Filter
,形成处理链。 -
doFilter()
依次调用每个Filter
,直到请求被某个Filter
处理或执行完成。
FilterChain chain = new FilterChain();
chain.addFilter(new AuthFilter());
chain.addFilter(new LogFilter());
chain.doFilter(request, response);
2.Spring Security 采用 责任链模式 处理用户认证、授权等安全操作。
public class FilterChainProxy implements Filter {
private List<SecurityFilterChain> filterChains;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
for (SecurityFilterChain filterChain : filterChains) {
if (filterChain.matches(request)) {
filterChain.doFilter(request, response, chain);
return;
}
}
chain.doFilter(request, response);
}
}
-
FilterChainProxy
依次调用SecurityFilterChain
处理请求,如果当前FilterChain
不能处理,则交给下一个。 -
不同
FilterChain
负责不同的安全检查,如认证、权限校验等。
filterChainProxy.addFilter(new AuthenticationFilter());
filterChainProxy.addFilter(new AuthorizationFilter());
filterChainProxy.doFilter(request, response);
- Netty 的
ChannelPipeline
采用 责任链模式 处理 网络数据读写。
public class DefaultChannelPipeline implements ChannelPipeline {
private ChannelHandlerContext head;
private ChannelHandlerContext tail;
@Override
public ChannelPipeline addLast(ChannelHandler handler) {
ChannelHandlerContext ctx = new ChannelHandlerContext(handler);
tail.next = ctx;
tail = ctx;
return this;
}
@Override
public void fireChannelRead(Object msg) {
head.invokeChannelRead(msg);
}
}
-
ChannelPipeline
管理多个ChannelHandler
形成责任链,每个Handler
负责特定的网络处理,如编解码、日志、加密等。 -
fireChannelRead()
从头开始调用ChannelHandler
处理数据。
pipeline.addLast(new DecoderHandler());
pipeline.addLast(new BusinessLogicHandler());
pipeline.fireChannelRead(msg);
- Slf4j 的
Logger
采用 责任链模式 依次处理日志请求,如 日志级别筛选、格式化、持久化。
public abstract class Logger {
protected Logger nextLogger;
public void setNextLogger(Logger nextLogger) {
this.nextLogger = nextLogger;
}
public void logMessage(String level, String message) {
if (canHandle(level)) {
write(message);
} else if (nextLogger != null) {
nextLogger.logMessage(level, message);
}
}
protected abstract boolean canHandle(String level);
protected abstract void write(String message);
}
-
Logger
形成责任链,按日志级别依次处理日志。 -
不同的
Logger
处理不同级别的日志,如 DEBUG、INFO、ERROR。5.Spring AOP 使用 责任链模式 依次调用多个拦截器(
MethodInterceptor
)。
public class MethodInterceptorChain {
private List<MethodInterceptor> interceptors = new ArrayList<>();
private int index = 0;
public Object invoke(MethodInvocation invocation) throws Throwable {
if (index < interceptors.size()) {
return interceptors.get(index++).invoke(invocation, this);
}
return invocation.proceed();
}
}
-
拦截器链(
MethodInterceptorChain
)管理多个拦截器,如 事务管理、权限校验、日志记录。 -
拦截器依次调用,执行增强逻辑。
chain.addInterceptor(new TransactionInterceptor());
chain.addInterceptor(new LoggingInterceptor());
chain.invoke(methodInvocation);
1.7 优缺点分析
- 优点
- 可以动态地改变请求的处理逻辑,增加新的处理对象非常方便。
- 处理链的扩展与修改对现有代码的影响较小,符合开闭原则。
- 缺点
- 如果链的处理对象过多,可能导致请求处理的过程过长,影响性能。
- 链的遍历可能导致某些请求无法得到处理,增加了调试的复杂度。
1.8 问题反思与解答
问题:责任链模式处理的链条过长,性能如何优化?
解答:当责任链的处理节点过多时,可以考虑对请求进行缓存或者批处理,从而减少每次处理链条的开销。另外,避免链条过长,通过设计更精细的责任划分,可以提高系统性能。
1.9 适用性分析
- 适合使用责任链模式的场景:
- 请求有多个处理者时,希望将请求处理逻辑解耦。
- 需要动态地添加或修改请求的处理者时。
- 替代方案:如果请求处理比较简单,且不需要动态配置处理者,则可以直接使用
if-else
语句或者策略模式。
1.10 常见误区
- 误区一:使用责任链模式时,设计的责任链过长,导致性能问题。
- 误区二:责任链的传递过于依赖链上的处理者,导致代码逻辑复杂,维护难度增加。
1.11 模式变种
- 多级责任链:处理链条中可能存在多个层级,某个请求可能需要传递到多个层级进行处理。
- 异步责任链:当每个处理者的操作时间较长时,可以考虑使用异步方式执行责任链,从而避免请求的阻塞。
1.12 与其他设计模式的关系
- 与策略模式的关系:策略模式与责任链模式都允许在运行时选择不同的策略或处理方法,责任链模式更加注重请求的顺序处理,而策略模式则关注于选择具体的处理策略。
- 与中介者模式的关系:中介者模式关注于多个对象之间的交互,而责任链模式关注于单一请求的逐步处理。两者的处理方式有所不同。
1.13 性能考虑
- 性能影响:当责任链的长度较长时,可能会对性能产生一定影响。为了提高性能,可以考虑引入缓存机制或者将某些不需要处理的请求提前排除。
- 优化方法:优化责任链的构建方式,避免不必要的链条传递,可以通过优先级、条件判断等手段来减少无效的处理。
2. 命令模式(Command Pattern)
2.1 背景与动机
在传统编程中,操作通常是由方法调用来直接执行的,但在复杂的系统中,可能需要将请求发送、操作和请求的处理过程分离开。将操作与执行者解耦有助于提高系统的灵活性,尤其是在系统中存在动态变化时。
常见问题:
- 直接调用方法时,可能导致代码高度耦合,无法灵活地添加、修改或删除操作。
- 有些操作需要在特定的时机或条件下执行,无法简单地通过方法调用来实现。
通过命令模式,我们可以将请求封装为命令对象,将请求的发起与请求的执行解耦,便于后续对操作的管理和扩展。
假设有一个文本编辑器,我们希望能够撤销和重做操作。直接调用方法会导致界面与业务逻辑的耦合,无法轻松实现撤销和重做操作。命令模式可以帮助将每个操作封装成命令对象,从而使得系统更具扩展性。
2.2 模式定义与结构
定义: 命令模式将请求封装为一个对象,从而使得用户可以使用不同的请求对客户进行参数化;将请求排队或记录请求日志,以及支持撤销操作。
+-------------------+
| Command |<-------------------------+
|-------------------| |
| + execute() | |
+-------------------+ |
| |
v |
+-------------------+ +--------------------------+
| ConcreteCommand |<----->| Invoker |
|-------------------| |--------------------------|
| - receiver | | - command |
| + execute() | | + invoke() |
+-------------------+ +--------------------------+
| |
v |
+-------------------+ |
| Receiver |<-----------------------+
|-------------------|
| + action() |
+-------------------+
结构说明:
- Command:命令接口,声明了执行操作的方法
execute()
。 - ConcreteCommand:实现命令接口,定义了具体的请求处理逻辑。它会持有对
Receiver
的引用,并调用其方法执行操作。 - Receiver:接收者,实际执行命令的具体操作。命令对象将请求转发给接收者,由接收者执行相应的操作。
- Invoker:调用者,负责调用命令的
execute()
方法。它持有一个命令对象,并在合适的时机触发命令的执行。
2.3 源码示例
基本命令模式
// Command Interface
//撤销操作
public interface Command {
void execute();
void undo();
}
// Receiver
public class Fan {
public void turnOn() {
System.out.println("Fan is ON");
}
public void turnOff() {
System.out.println("Fan is OFF");
}
}
// ConcreteCommand
public class FanCommand implements Command {
private Fan fan;
public FanCommand(Fan fan) {
this.fan = fan;
}
@Override
public void execute() {
fan.turnOn();
}
@Override
public void undo() {
fan.turnOff();
}
}
// Invoker
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
public void pressUndo() {
command.undo();
}
}
// Client
public class Client {
public static void main(String[] args) {
Fan fan = new Fan();
Command fanOn = new FanCommand(fan);
RemoteControl remote = new RemoteControl();
remote.setCommand(fanOn);
remote.pressButton(); // Fan is ON
remote.pressUndo(); // Fan is OFF
}
}
2.4 设计原则分析
命令模式与几个设计原则有紧密的关系,具体分析如下:
- 单一职责原则(SRP):命令模式将请求的发送者和请求的处理者解耦,每个类的职责变得更加单一。发送者不再关心请求的执行细节,处理者专注于具体的操作。
- 开闭原则(OCP):命令模式使得新的命令可以通过增加新的命令类来扩展,而不需要修改现有的类。每个命令都是独立的,符合开闭原则。
- 依赖倒置原则(DIP):客户端(Invoker)依赖于抽象的命令接口,而不是具体的命令实现。命令对象的实现依赖于接收者,而不是硬编码的操作逻辑。
2.5 设计原则分析
命令模式与几个设计原则有紧密的关系,具体分析如下:
- 单一职责原则(SRP):命令模式将请求的发送者和请求的处理者解耦,每个类的职责变得更加单一。发送者不再关心请求的执行细节,处理者专注于具体的操作。
- 开闭原则(OCP):命令模式使得新的命令可以通过增加新的命令类来扩展,而不需要修改现有的类。每个命令都是独立的,符合开闭原则。
- 依赖倒置原则(DIP):客户端(Invoker)依赖于抽象的命令接口,而不是具体的命令实现。命令对象的实现依赖于接收者,而不是硬编码的操作逻辑。
2.6 开源示例
- 在 JDK 中,
Runnable
接口是 命令模式的典型实现,将任务封装成对象,交由Thread
执行。
public interface Runnable {
void run();
}
public class Thread {
private Runnable task;
public Thread(Runnable task) {
this.task = task;
}
public void start() {
new Thread(task).start();
}
}
-
Runnable
封装任务(命令),由Thread
执行,实现请求与执行者的解耦。 -
支持多线程任务管理。
- Spring
TaskScheduler
采用 命令模式 调度任务,允许定时执行封装的命令对象。
public interface TaskScheduler {
ScheduledFuture<?> schedule(Runnable task, Trigger trigger);
}
public class ConcurrentTaskScheduler implements TaskScheduler {
@Override
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
return executorService.schedule(task, trigger.getNextExecutionTime(), TimeUnit.MILLISECONDS);
}
}
-
TaskScheduler
封装任务(命令),并在指定时间执行,实现 任务调度解耦。 -
支持定时执行、周期执行等功能。
- Netty 的
ChannelHandler
采用 命令模式 处理网络事件,请求被封装成命令后交由ChannelPipeline
处理。
public class CommandHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
Command command = CommandFactory.getCommand(msg);
command.execute(ctx);
}
}
-
网络数据封装成命令对象,然后交由
execute()
方法处理。 -
支持扩展新命令,而无需修改
CommandHandler
。
- JUnit 使用 命令模式 封装测试用例,每个
TestCase
都是一个命令对象。
public abstract class TestCase {
public abstract void execute();
}
public class SampleTest extends TestCase {
@Override
public void execute() {
System.out.println("Running test case...");
}
}
-
TestCase
封装测试逻辑,由TestRunner
统一执行。 -
可以批量执行多个测试用例。
- 一些 文本编辑器(如 VS Code、IntelliJ IDEA)使用 命令模式管理撤销/重做操作。
public interface Command {
void execute();
void undo();
}
public class PasteCommand implements Command {
private String clipboard;
private TextEditor editor;
public PasteCommand(TextEditor editor, String clipboard) {
this.editor = editor;
this.clipboard = clipboard;
}
@Override
public void execute() {
editor.insert(clipboard);
}
@Override
public void undo() {
editor.delete(clipboard.length());
}
}
-
Command
封装撤销/重做逻辑,undo()
回滚操作。 -
支持撤销/重做操作历史记录
- Kafka 的
Consumer
采用 命令模式 处理消息,消息封装成ConsumerRecord
,交由Consumer
处理。
public interface MessageHandler {
void handleMessage(ConsumerRecord<String, String> record);
}
public class LogMessageHandler implements MessageHandler {
@Override
public void handleMessage(ConsumerRecord<String, String> record) {
System.out.println("Processing message: " + record.value());
}
}
- 每条消息封装成
ConsumerRecord
,交由handleMessage()
执行。 - 支持不同消息类型的处理逻辑。
2.7 优缺点分析
优点:
- 将请求与执行解耦,灵活性强。
- 可以轻松添加新的命令而不影响其他部分。
- 支持撤销操作,便于回滚状态。
缺点:
- 可能需要定义很多命令类,增加系统的复杂度。
- 对于简单的命令可能过于冗长,不适用于简单场景。
2.8 适用性分析
命令模式适用于以下场景:
- 当请求的发起者与请求的执行者之间存在解耦的需求时。
- 当需要支持撤销和重做操作时。
- 当需要扩展更多操作时,而不需要修改现有代码。
2.9 常见误区
- 过度设计:命令模式并非适用于所有情况,尤其是在操作非常简单的场景中,过度使用可能会引入不必要的复杂度。
- 过多命令类:为了应对不同的操作,可能会创建过多的命令类,导致代码冗长,设计复杂。
2.10 模式变种
- 链式命令模式:命令对象不仅能执行自己的操作,还能调用链上的其他命令来执行操作。
- 宏命令模式:将多个命令封装在一个命令对象中,适用于需要将多个操作作为一个整体来执行的场景。
2.11 与其他设计模式的关系
- 责任链模式:命令模式与责任链模式类似,都是将请求与处理分离,命令模式是通过封装请求来实现,而责任链是通过链式结构来传递请求。
- 模板方法模式:命令模式中的
execute()
方法和模板方法模式中的钩子方法非常类似,都是定义了流程的骨架,并由子类实现具体的步骤。
2.12 性能考虑
命令模式本身对性能的影响较小,因为它的核心是封装操作并将其传递给执行者,性能瓶颈通常不会出现在命令模式本身。不过,如果命令对象过于复杂,或需要频繁地创建大量命令对象时,可能会带来一定的性能开销。
3. 解释器模式(Interpreter Pattern)
3.1 背景与动机
在实际开发中,我们经常需要解析并处理特定的规则或语言,例如:
- 搜索引擎的查询语句解析:将用户输入的复杂查询语句解析为逻辑表达式。
- 脚本语言:例如 SQL 或正则表达式的解析与执行。
- 数学表达式:如计算器中对输入表达式的解析。
问题:
假设我们在实现一个简单规则引擎,用于匹配用户输入的查询条件,规则包括逻辑操作符(AND
、OR
)和比较操作符(=
、>
)。如果直接将解析逻辑与应用逻辑耦合,不仅会导致代码臃肿,而且扩展新的规则(如 NOT
操作)会非常困难。
解决方案:
通过解释器模式,将语法规则的解析和执行逻辑抽象为一组类,使用递归结构解析复杂表达式,从而使得语法规则的扩展更为灵活。
3.2 模式定义与结构
解释器模式为某个语言定义了一个解释器,为该语言的句子定义其语法表示,并提供一个解释器来解析该语言的句子。
+------------------+
| Expression |<----------------+
|------------------| |
| + interpret() | |
+------------------+ |
/\ |
/ \ |
+-------------+ +--------------------+
| TerminalExp | | NonTerminalExp |
|-------------| |--------------------|
| + interpret() | | + interpret() |
+-------------+ +--------------------+
| |
+-------------------------+
|
+----------------+
| ConcreteExp |
|----------------|
| + interpret() |
+----------------+
主要角色:
- Expression (抽象表达式):
定义解释器接口,声明解释操作interpret()
。 - TerminalExpression (终结符表达式):
表示语法规则中的基本单元,实现具体的解释逻辑。 - NonTerminalExpression (非终结符表达式):
表示语法规则的复杂结构,通常通过递归调用其他表达式的interpret()
方法完成解析。 - Context (上下文):
提供解释器需要的外部信息。
3.3 源码示例
简单数学表达式解释器
// 抽象表达式接口
interface Expression {
int interpret();
}
// 数字终结符表达式
class NumberExpression implements Expression {
private final int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret() {
return number;
}
}
// 加法非终结符表达式
class AdditionExpression implements Expression {
private final Expression leftExpression;
private final Expression rightExpression;
public AdditionExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
@Override
public int interpret() {
return leftExpression.interpret() + rightExpression.interpret();
}
}
// 减法非终结符表达式
class SubtractionExpression implements Expression {
private final Expression leftExpression;
private final Expression rightExpression;
public SubtractionExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
@Override
public int interpret() {
return leftExpression.interpret() - rightExpression.interpret();
}
}
// 客户端
public class InterpreterDemo {
public static void main(String[] args) {
// 解释 5 + 10 - 2
Expression five = new NumberExpression(5);
Expression ten = new NumberExpression(10);
Expression two = new NumberExpression(2);
Expression addition = new AdditionExpression(five, ten);
Expression subtraction = new SubtractionExpression(addition, two);
System.out.println("Result: " + subtraction.interpret()); // 输出:13
}
}
3.4 设计原则分析
- 单一职责原则:
每个表达式类只负责解析某一类语法规则,职责明确。 - 开闭原则:
通过增加新的表达式类,可以扩展语法规则,而无需修改现有代码。 - 组合复用原则:
使用递归组合表达式,可以解析任意复杂的语法结构。
3.5 实际应用场景
- 编译器和解释器:
用于编程语言的语法解析和执行。 - 规则引擎:
用于动态规则匹配,例如权限规则或业务规则。 - 搜索引擎:
解析用户输入的查询语句。
3.6 开源示例
- Spring 提供了功能强大的表达式语言,用于动态解析和执行表达式。
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class SpELDemo {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();
System.out.println(message); // 输出:Hello World!
Expression mathExp = parser.parseExpression("10 * 2 + 1");
Integer result = (Integer) mathExp.getValue();
System.out.println("Result: " + result); // 输出:21
}
}
2.ANTLR 是一种流行的语法解析框架,用于编译器或 DSL 的开发。
import org.antlr.v4.runtime.*;
public class ANTLRExample {
public static void main(String[] args) {
String input = "a AND b OR c";
CharStream charStream = CharStreams.fromString(input);
// ANTLR Lexer and Parser (具体实现省略)
System.out.println("Parsed Expression Successfully!");
}
}
3.7 优缺点分析
优点:
- 语法规则封装清晰,便于扩展和维护。
- 灵活组合表达式,适用于解析复杂规则。
缺点:
- 类的数量可能迅速增加,导致代码复杂度提升。
- 对于复杂的语法规则,性能可能不够高效。
3.8 问题反思与解答
- 如何优化类数量膨胀的问题?
- 可以通过工具生成表达式类。
- 使用嵌套类或匿名类减少类的数量。
- 如何提高性能?
- 对解析结果进行缓存。
- 采用中间表示(如抽象语法树)提高执行效率。
3.9 适用性分析
- 适用场景:
- 需要解析和执行固定规则的场景。
- 规则频繁变化且扩展需求较大的场景。
- 不适用场景:
- 规则过于复杂,直接使用状态机或生成工具更高效。
3.10 常见误区
- 低估类的膨胀问题:
忽视语法规则扩展后类的数量问题,导致维护困难。 - 过度使用:
对简单问题引入解释器模式,反而增加了复杂性。
3.11 性能考虑
解释器模式频繁调用 interpret()
方法,可能导致性能瓶颈。在高频解析场景下,应考虑:
- 缓存中间结果。
- 使用中间表示优化解析性能。
- 使用工具生成解析代码,避免重复计算。
4. 备忘录模式(Memento Pattern)
4.1 背景与动机
在软件开发中,我们经常需要提供撤销或恢复功能,典型场景包括:
- 文本编辑器:支持撤销和重做功能。
- 游戏存档:玩家可以保存游戏进度并随时恢复。
- 数据库事务管理:记录事务状态,以便在出错时回滚。
问题:
如果直接将对象的状态存储在外部,例如客户端代码中,这会破坏对象的封装性。此外,手动记录和恢复状态可能导致实现复杂、错误率高。
解决方案:
备忘录模式提供了一种机制,可以在不破坏对象封装性的前提下捕获和恢复其内部状态,简化了状态保存和恢复的实现。
4.2 模式定义与结构
备忘录模式通过备忘录对象存储另一个对象的状态,以便在未来可以恢复对象的状态。
+------------------+
| Originator |<----------------+
|------------------| |
| - state: String | |
|------------------| |
| + createMemento(): Memento |
| + restoreMemento(m: Memento) |
+------------------+ |
| |
| |
+------------------+ +------------------+
| Memento | | Caretaker |
|------------------| |------------------|
| - state: String | | - memento: Memento|
|------------------| |------------------|
| + getState(): String | + setMemento() |
|------------------| | + getMemento() |
+------------------+ +------------------+
主要角色:
- Originator (发起人):
定义要保存状态的对象,提供创建和恢复备忘录的接口。 - Memento (备忘录):
用于存储 Originator 的内部状态。 - Caretaker (管理者):
管理备忘录对象的保存和恢复,不对备忘录的内容进行操作。
4.3 源码示例
文本编辑器的撤销功能
// 备忘录类
class Memento {
private final String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
// 发起人类
class Originator {
private String state;
public void setState(String state) {
System.out.println("Setting state to: " + state);
this.state = state;
}
public String getState() {
return state;
}
public Memento saveStateToMemento() {
System.out.println("Saving state to Memento.");
return new Memento(state);
}
public void restoreStateFromMemento(Memento memento) {
state = memento.getState();
System.out.println("Restored state from Memento: " + state);
}
}
// 管理者类
class Caretaker {
private final List<Memento> mementoList = new ArrayList<>();
public void add(Memento memento) {
mementoList.add(memento);
}
public Memento get(int index) {
return mementoList.get(index);
}
}
// 客户端
public class MementoDemo {
public static void main(String[] args) {
Originator originator = new Originator();
Caretaker caretaker = new Caretaker();
originator.setState("State #1");
caretaker.add(originator.saveStateToMemento());
originator.setState("State #2");
caretaker.add(originator.saveStateToMemento());
originator.setState("State #3");
System.out.println("Current state: " + originator.getState());
originator.restoreStateFromMemento(caretaker.get(0));
originator.restoreStateFromMemento(caretaker.get(1));
}
}
Setting state to: State #1
Saving state to Memento.
Setting state to: State #2
Saving state to Memento.
Setting state to: State #3
Current state: State #3
Restored state from Memento: State #1
Restored state from Memento: State #2
4.4 设计原则分析
- 单一职责原则:
每个类只负责自己的特定功能:Originator 保存状态,Memento 存储状态,Caretaker 管理备忘录。 - 封装性:
Originator 的内部状态只通过 Memento 暴露给 Caretaker,确保了状态的安全性。
4.5 实际应用场景
- 文本编辑器的撤销功能:
保存编辑状态,支持用户撤销和恢复。 - 游戏存档:
保存玩家的进度,以便中断后继续游戏。 - 数据库事务:
在事务回滚时恢复到之前的状态。
4.6 开源示例
1.Java 的 Serializable
接口允许对象 保存当前状态,并在需要时恢复。
import java.io.*;
class Originator implements Serializable {
private String state;
public Originator(String state) {
this.state = state;
}
public void setState(String state) {
this.state = state;
}
public String getState() {
return state;
}
public void saveToFile(String filename) throws IOException {
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))) {
out.writeObject(this);
}
}
public static Originator restoreFromFile(String filename) throws IOException, ClassNotFoundException {
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename))) {
return (Originator) in.readObject();
}
}
}
// 使用示例
public class Main {
public static void main(String[] args) throws Exception {
Originator origin = new Originator("Initial State");
origin.saveToFile("backup.ser");
origin.setState("Modified State");
Originator restored = Originator.restoreFromFile("backup.ser");
System.out.println(restored.getState()); // 输出: Initial State
}
}
Serializable
序列化对象状态,并存储到文件中,实现备份功能。restoreFromFile
恢复原始状态,实现撤销/回滚操作。
- Spring 事务管理使用 备忘录模式 记录数据库状态,在事务失败时回滚。
@Transactional
public void transferMoney(Account from, Account to, double amount) {
from.debit(amount);
to.credit(amount);
if (amount > from.getBalance()) {
throw new RuntimeException("Insufficient funds");
}
}
-
事务提交前存储数据库状态(快照)。
-
事务失败时,回滚到之前的状态。
- IntelliJ IDEA 采用 备忘录模式 记录文本编辑状态,支持撤销和重做。
public class TextEditor {
private String content;
private Stack<String> history = new Stack<>();
public void write(String newText) {
history.push(content);
content = newText;
}
public void undo() {
if (!history.isEmpty()) {
content = history.pop();
}
}
public String getContent() {
return content;
}
}
-
每次修改文本前,将当前内容存入历史栈。
-
撤销操作时,从历史栈恢复上一个状态。
4.游戏引擎(如 Unity、Unreal Engine)采用 备忘录模式 实现存档功能。
class GameState {
private int level;
private int health;
public GameState(int level, int health) {
this.level = level;
this.health = health;
}
public Memento save() {
return new Memento(level, health);
}
public void restore(Memento memento) {
this.level = memento.getLevel();
this.health = memento.getHealth();
}
}
class Memento {
private final int level;
private final int health;
public Memento(int level, int health) {
this.level = level;
this.health = health;
}
public int getLevel() { return level; }
public int getHealth() { return health; }
}
// 使用示例
public class Main {
public static void main(String[] args) {
GameState game = new GameState(1, 100);
Memento savePoint = game.save(); // 备份
game.restore(savePoint); // 恢复
}
}
-
保存游戏状态(level, health)。
-
恢复存档,实现游戏进度回溯。
- 浏览器使用 备忘录模式 记录用户访问的历史页面,并支持“后退”操作。
public class Browser {
private String currentPage;
private Stack<String> history = new Stack<>();
public void visitPage(String url) {
history.push(currentPage);
currentPage = url;
}
public void goBack() {
if (!history.isEmpty()) {
currentPage = history.pop();
}
}
public String getCurrentPage() {
return currentPage;
}
}
4.7 优缺点分析
优点:
- 保持对象封装性,外部无法直接修改对象状态。
- 提供简单灵活的状态管理和恢复机制。
缺点:
-
如果需要保存的状态过多,可能导致内存消耗过大。
-
状态恢复可能需要额外的逻辑支持。
4.8 问题反思与解答
- 如何避免内存过多消耗?
- 对状态数据进行压缩。
- 保存必要的增量状态,而不是完整状态。
- 如何优化状态恢复速度?
- 使用高效的数据结构存储状态。
4.9 适用性分析
- 适用场景:
- 需要保存和恢复复杂对象的状态。
- 对象的状态易变,且需要安全管理。
- 不适用场景:
- 状态变化频繁,且每次保存状态的开销过大。
4.10 常见误区
- 误将 Caretaker 的逻辑混入 Originator:
导致备忘录和业务逻辑耦合。 - 滥用模式:
在状态简单、不需要恢复的场景中使用备忘录模式,会徒增复杂性。
4.11 与其他设计模式的关系
- 命令模式:
结合使用时,命令模式可以将状态变化封装为命令,备忘录模式负责存储命令的执行状态。 - 迭代器模式:
可以结合备忘录模式记录迭代器的当前位置。
4.12 性能考虑
-
减少内存占用:
- 使用轻量级备忘录。
- 只保存关键状态。
-
加速恢复效率:
- 使用缓存或索引机制快速定位备忘录。
-
分布式备忘录:
在分布式系统中,将备忘录状态存储到共享缓存或数据库中。
5. 观察者模式(Observer Pattern)
5.1 背景与动机
在软件开发中,常见的需求是让一个对象的变化自动通知多个其他对象。例如:
- UI框架:当数据模型变化时,界面组件需要自动更新。
- 事件监听:当某个事件发生时,多个监听器需要收到通知。
- 发布-订阅系统:消息的发布者和订阅者之间需要松耦合。
问题:
如果直接在对象间建立依赖关系,会导致系统高度耦合,难以扩展或维护。
解决方案:
观察者模式定义了一种一对多的依赖关系,当被观察对象状态发生改变时,所有依赖它的观察者都会收到通知并自动更新。
5.2 模式定义与结构
观察者模式定义了一种一对多的依赖关系,使得当一个对象状态发生变化时,所有依赖于它的对象都得到通知并被自动更新。它主要用于实现发布-订阅机制。
+----------------+
| Subject |<------------------------+
|----------------| |
| + attach(Observer): void |
| + detach(Observer): void |
| + notify(): void |
+----------------+ |
| |
| |
+----------------+ +----------------+
| ConcreteSubject| | Observer |
|----------------| |----------------|
| - state: State | | + update(): void|
|----------------| +----------------+
| + getState(): State ^
| + setState(State): void |
+----------------+ |
| |
| |
+----------------+ +----------------+
| ConcreteObserver| | ConcreteObserver|
+----------------+ +----------------+
主要角色:
- Subject (主题/被观察者):
维护观察者列表,并在状态改变时通知观察者。 - Observer (观察者):
定义接口,用于接收通知。 - ConcreteSubject (具体主题):
具体实现 Subject,存储状态并负责通知观察者。 - ConcreteObserver (具体观察者):
具体实现 Observer,定义具体的响应逻辑。
5.3 源码示例
新闻发布系统
// 抽象观察者
interface Observer {
void update(String message);
}
// 具体观察者
class Subscriber implements Observer {
private final String name;
public Subscriber(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " received update: " + message);
}
}
// 抽象主题
interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers(String message);
}
// 具体主题
class NewsPublisher implements Subject {
private final List<Observer> observers = new ArrayList<>();
@Override
public void attach(Observer observer) {
observers.add(observer);
}
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
// 客户端
public class ObserverPatternDemo {
public static void main(String[] args) {
NewsPublisher publisher = new NewsPublisher();
Observer subscriber1 = new Subscriber("Alice");
Observer subscriber2 = new Subscriber("Bob");
publisher.attach(subscriber1);
publisher.attach(subscriber2);
publisher.notifyObservers("Breaking News: Observer Pattern Simplified!");
publisher.detach(subscriber1);
publisher.notifyObservers("Update: Observer Pattern Deep Dive.");
}
}
Alice received update: Breaking News: Observer Pattern Simplified!
Bob received update: Breaking News: Observer Pattern Simplified!
Bob received update: Update: Observer Pattern Deep Dive.
5.4 设计原则分析
- 开闭原则:
可以通过增加新的观察者类型扩展系统,而无需修改主题的代码。 - 依赖倒置原则:
主题依赖于抽象的观察者接口,而非具体实现。 - 松耦合:
观察者和主题之间的关系是松耦合的,便于独立开发和测试。
5.5 实际应用场景
- GUI框架:
如 Java Swing 的事件监听器。 - 实时数据推送:
股票价格更新、天气预报推送。 - 分布式系统:
微服务架构中的事件驱动机制。
5.6 开源示例
- Java 内置的
java.util.Observer
早期提供了观察者模式的支持,尽管在 Java 9 之后被标记为 deprecated,但仍可用于理解基本实现。
import java.util.Observable;
import java.util.Observer;
// 被观察者(主题)
class NewsPublisher extends Observable {
private String news;
public void setNews(String news) {
this.news = news;
setChanged(); // 标记状态已更新
notifyObservers(news); // 通知所有观察者
}
}
// 观察者
class NewsReader implements Observer {
private String name;
public NewsReader(String name) {
this.name = name;
}
@Override
public void update(Observable o, Object arg) {
System.out.println(name + " 收到新闻更新: " + arg);
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
NewsPublisher publisher = new NewsPublisher();
NewsReader reader1 = new NewsReader("Alice");
NewsReader reader2 = new NewsReader("Bob");
publisher.addObserver(reader1);
publisher.addObserver(reader2);
publisher.setNews("观察者模式在 Java 中的应用");
}
}
NewsPublisher
作为 被观察者(主题),管理多个NewsReader
观察者。setChanged()
触发变更,notifyObservers()
让所有订阅者收到更新。
2.Spring 框架中广泛使用 观察者模式 来实现 事件驱动架构,例如 Spring ApplicationEvent。
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
// 事件对象
class UserRegisterEvent extends ApplicationEvent {
private String username;
public UserRegisterEvent(Object source, String username) {
super(source);
this.username = username;
}
public String getUsername() {
return username;
}
}
// 事件发布者
@Component
class UserService {
private final ApplicationEventPublisher publisher;
public UserService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void registerUser(String username) {
System.out.println("用户 " + username + " 注册成功!");
publisher.publishEvent(new UserRegisterEvent(this, username)); // 发布事件
}
}
// 事件监听器
@Component
class EmailService {
@EventListener
public void handleUserRegister(UserRegisterEvent event) {
System.out.println("发送欢迎邮件给 " + event.getUsername());
}
}
-
UserService
发布事件UserRegisterEvent
。 -
EmailService
监听事件 并执行发送邮件逻辑。
3.Google Guava 提供了 EventBus,是观察者模式的轻量级实现,适用于 异步事件通知。
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
// 事件
class OrderEvent {
private final String orderId;
public OrderEvent(String orderId) {
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
// 观察者
class OrderEventListener {
@Subscribe
public void onOrderPlaced(OrderEvent event) {
System.out.println("订单 " + event.getOrderId() + " 被处理");
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
EventBus eventBus = new EventBus();
eventBus.register(new OrderEventListener());
eventBus.post(new OrderEvent("12345")); // 发布事件
}
}
-
EventBus
作为 事件分发器,OrderEventListener
作为 观察者 监听OrderEvent
。 -
eventBus.post()
发布事件,所有订阅者收到通知。
- RxJava 采用 观察者模式 实现响应式编程,广泛应用于 Android 开发、流式处理 等场景。
import io.reactivex.Observable;
public class RxJavaExample {
public static void main(String[] args) {
Observable<String> observable = Observable.create(emitter -> {
emitter.onNext("事件 1");
emitter.onNext("事件 2");
emitter.onComplete();
});
observable.subscribe(event -> System.out.println("订阅者收到: " + event));
}
}
Observable
作为 被观察者,subscribe()
作为 观察者 监听数据流。
5.Redis Pub/Sub 机制使用 观察者模式 实现 跨进程消息推送。
Jedis jedis = new Jedis("localhost");
// 订阅者
new Thread(() -> {
jedis.subscribe((channel, message) -> {
System.out.println("收到消息: " + message);
}, "news-channel");
}).start();
// 发布者
jedis.publish("news-channel", "观察者模式在 Redis 中的应用");
subscribe()
监听 频道消息,publish()
发布消息。
6.Kafka 使用 观察者模式 作为核心架构,消息发布者(Producer)和订阅者(Consumer)完全解耦。
// 消费者监听 Kafka 主题
consumer.subscribe(Collections.singletonList("order_topic"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
for (ConsumerRecord<String, String> record : records) {
System.out.println("收到消息: " + record.value());
}
}
5.7 优缺点分析
优点:
- 支持一对多的通知机制。
- 观察者和主题之间的耦合度低。
缺点:
- 如果观察者较多,通知可能影响性能。
- 当多个观察者存在依赖关系时,通知的顺序需要特别注意。
5.8 问题反思与解答
- 如何确保观察者的通知顺序?
- 使用排序机制,根据优先级通知观察者。
- 如何避免重复通知?
- 使用唯一标识符来区分观察者。
5.9 适用性分析
- 适用场景:
- 需要动态更新多个对象时。
- 不希望对象之间高度耦合。
- 不适用场景:
- 系统性能对通知频率敏感。
5.10 常见误区
- 误将观察者的业务逻辑嵌入主题:
违反单一职责原则,降低可维护性。 - 误用同步通知:
在高并发场景中可能导致性能瓶颈。
5.11 性能考虑
- 减少通知开销:
- 批量通知,减少通知次数。
- 使用事件队列异步通知。
- 缓存数据:
避免每次通知时重新计算数据。
6. 状态模式(State Pattern)
6.1 背景与动机
在面向对象开发中,某些对象的行为会随其状态改变而改变。例如:
- 订单处理系统:
一个订单可能有多种状态(如新建、处理中、已完成、已取消),每种状态下允许的操作不同。 - 状态机实现:
游戏角色可能处于不同的状态(如站立、行走、跳跃、攻击),每种状态下表现不同。
问题:
如果使用大量条件语句(如 if-else
或 switch-case
)判断状态,会导致代码臃肿、不易维护。
解决方案:
使用状态模式将与状态相关的行为封装到不同的状态类中,让状态对象决定对象的行为。
6.2 模式定义与结构
状态模式允许对象在其内部状态改变时改变其行为,看起来像是改变了对象的类。
+----------------+
| Context |
|----------------|
| - state: State |
| + setState(State): void |
| + request(): void |
+----------------+
^
|
+-------+--------+
| |
+----------------+ +----------------+
| ConcreteStateA | | ConcreteStateB |
|----------------| |----------------|
| + handle(): void | + handle(): void
+----------------+ +----------------+
主要角色:
- Context (上下文):
持有一个状态对象,定义了状态切换接口,并将客户端请求委托给当前状态对象处理。 - State (抽象状态):
定义所有具体状态的公共接口。 - ConcreteState (具体状态):
实现具体状态的行为。
6.3 源码示例
订单处理系统
// 抽象状态
interface OrderState {
void handle(OrderContext context);
}
// 具体状态:新建
class NewOrderState implements OrderState {
@Override
public void handle(OrderContext context) {
System.out.println("Order is new. Processing it now...");
context.setState(new ProcessingOrderState());
}
}
// 具体状态:处理中
class ProcessingOrderState implements OrderState {
@Override
public void handle(OrderContext context) {
System.out.println("Order is being processed. Completing it now...");
context.setState(new CompletedOrderState());
}
}
// 具体状态:已完成
class CompletedOrderState implements OrderState {
@Override
public void handle(OrderContext context) {
System.out.println("Order is already completed. No further actions needed.");
}
}
// 上下文
class OrderContext {
private OrderState state;
public OrderContext() {
this.state = new NewOrderState(); // 初始状态
}
public void setState(OrderState state) {
this.state = state;
}
public void request() {
state.handle(this);
}
}
// 客户端
public class StatePatternDemo {
public static void main(String[] args) {
OrderContext order = new OrderContext();
// 状态转移
order.request();
order.request();
order.request();
}
}
Order is new. Processing it now...
Order is being processed. Completing it now...
Order is already completed. No further actions needed.
6.4 设计原则分析
- 单一职责原则:
每个状态类只负责实现特定状态下的行为。 - 开闭原则:
增加新状态时无需修改已有状态类和上下文类。 - 策略模式与状态模式的结合:
状态模式可以看作策略模式的扩展,用于描述动态的状态切换。
6.5 实际应用场景
- 订单处理:
电商平台的订单可能有多种状态(新建、待付款、已发货、已完成等)。 - 游戏开发:
游戏角色的行为会因状态改变(如行走、攻击、受伤等)。 - 状态机实现:
流程审批系统中,每个审批节点的操作行为可能不同。
6.6 开源示例
1.Spring 提供了 StateMachine 框架,基于状态模式实现 业务流程控制,比如订单状态流转。
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.stereotype.Component;
import java.util.EnumSet;
// 订单状态
enum OrderState {
NEW, PROCESSING, SHIPPED, DELIVERED, CANCELED
}
// 订单事件
enum OrderEvent {
PROCESS, SHIP, DELIVER, CANCEL
}
// 配置状态机
@EnableStateMachine
@Component
class OrderStateMachine extends StateMachineConfigurerAdapter<OrderState, OrderEvent> {
@Override
public void configure(StateMachineStateConfigurer<OrderState, OrderEvent> states) throws Exception {
states.withStates()
.initial(OrderState.NEW) // 初始状态
.states(EnumSet.allOf(OrderState.class)); // 所有可能的状态
}
@Override
public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions) throws Exception {
transitions
.withExternal().source(OrderState.NEW).target(OrderState.PROCESSING).event(OrderEvent.PROCESS)
.and().withExternal().source(OrderState.PROCESSING).target(OrderState.SHIPPED).event(OrderEvent.SHIP)
.and().withExternal().source(OrderState.SHIPPED).target(OrderState.DELIVERED).event(OrderEvent.DELIVER)
.and().withExternal().source(OrderState.NEW).target(OrderState.CANCELED).event(OrderEvent.CANCEL);
}
}
// 订单服务
@Component
class OrderService {
private final StateMachine<OrderState, OrderEvent> stateMachine;
public OrderService(StateMachine<OrderState, OrderEvent> stateMachine) {
this.stateMachine = stateMachine;
}
public void processOrder() {
stateMachine.sendEvent(OrderEvent.PROCESS);
System.out.println("订单状态: " + stateMachine.getState().getId());
}
}
-
OrderStateMachine
定义状态转换规则,用OrderState
管理订单状态。 -
OrderService
控制状态流转,根据业务触发OrderEvent
状态迁移。
2.Java Thread
的生命周期就是 状态模式 的应用,每个线程都拥有 NEW、RUNNABLE、BLOCKED、WAITING、TERMINATED 等状态,不同状态下执行不同行为。
class ThreadContext {
private ThreadState state;
public ThreadContext() {
this.state = new NewState(); // 初始状态
}
public void setState(ThreadState state) {
this.state = state;
}
public void start() {
state.handle(this);
}
}
// 状态接口
interface ThreadState {
void handle(ThreadContext context);
}
// 具体状态:新建状态
class NewState implements ThreadState {
@Override
public void handle(ThreadContext context) {
System.out.println("线程启动,进入 RUNNING 状态");
context.setState(new RunningState());
}
}
// 具体状态:运行状态
class RunningState implements ThreadState {
@Override
public void handle(ThreadContext context) {
System.out.println("线程执行完毕,进入 TERMINATED 状态");
context.setState(new TerminatedState());
}
}
// 具体状态:终止状态
class TerminatedState implements ThreadState {
@Override
public void handle(ThreadContext context) {
System.out.println("线程已经终止");
}
}
// 使用示例
public class StatePatternExample {
public static void main(String[] args) {
ThreadContext thread = new ThreadContext();
thread.start(); // 启动线程
thread.start(); // 继续执行
}
}
-
ThreadContext
维护当前状态,调用handle()
方法执行状态流转。 -
NewState
、RunningState
、TerminatedState
实现不同状态逻辑。
3.在游戏开发中,角色 AI 行为控制通常使用 状态模式,例如敌人 AI 追踪、攻击、巡逻等状态切换。
// 角色状态接口
interface CharacterState {
void execute(CharacterContext context);
}
// 角色上下文
class CharacterContext {
private CharacterState state;
public CharacterContext() {
this.state = new IdleState(); // 初始状态
}
public void setState(CharacterState state) {
this.state = state;
}
public void act() {
state.execute(this);
}
}
// 空闲状态
class IdleState implements CharacterState {
@Override
public void execute(CharacterContext context) {
System.out.println("角色处于空闲状态...");
context.setState(new AttackState());
}
}
// 攻击状态
class AttackState implements CharacterState {
@Override
public void execute(CharacterContext context) {
System.out.println("角色开始攻击!");
context.setState(new IdleState());
}
}
// 使用示例
public class GameAIExample {
public static void main(String[] args) {
CharacterContext character = new CharacterContext();
character.act(); // 进入攻击状态
character.act(); // 切回空闲状态
}
}
-
CharacterContext
维护当前角色状态,调用execute()
方法进行状态转换。 -
IdleState
、AttackState
定义角色不同状态的行为。
4.JGit 是 Git 的 Java 版本,它使用 状态模式 来管理 Git 分支的不同状
Ref branch = repository.exactRef("refs/heads/main");
if (branch.isSymbolic()) {
System.out.println("分支是符号引用,指向: " + branch.getTarget().getName());
} else {
System.out.println("分支是具体引用,SHA: " + branch.getObjectId().getName());
}
Git 分支有 SymbolicRef(符号引用)和 ObjectIdRef(具体引用) 两种状态,根据状态执行不同逻辑。
6.7 优缺点分析
优点:
- 遵循开闭原则,新增状态容易。
- 避免大量条件判断语句。
- 状态逻辑集中在状态类中,易于维护。
缺点:
- 增加了类的数量,导致系统复杂性提升。
- 状态切换的行为可能增加上下文类的耦合。
6.8 问题反思与解答
-
如何处理大量状态类?
- 使用状态模式的变种,例如状态表驱动的实现。
-
如何避免状态间的强耦合?
- 将状态切换逻辑集中在上下文类中,而不是具体状态类中。
6.9 适用性分析
-
适用场景:
- 对象的行为依赖于状态,并且需要在运行时根据状态改变行为。
- 状态逻辑复杂且容易扩展。
-
不适用场景:
- 状态的种类和数量非常有限,直接使用条件语句可能更简单。
6.10 常见误区
- 误将状态切换逻辑分散到各个状态类中:
应将状态切换的职责尽量集中管理。 - 误用状态模式处理简单状态切换:
对于简单状态切换,直接使用条件语句可能更高效。
6.11 模式变种
- 表驱动状态模式:
使用状态表代替状态类,简化状态管理。 - 有限状态机:
使用专门的状态机框架(如 Spring State Machine)来实现复杂状态切换。
6.12 与其他设计模式的关系
- 策略模式:
状态模式与策略模式类似,但状态模式强调状态之间的切换。 - 命令模式:
状态模式中的状态对象可以实现命令模式的行为封装。
6.13 性能考虑
- 减少类加载开销:
使用单例模式或静态状态对象。 - 优化状态切换逻辑:
将频繁切换的状态实现为轻量级对象。
状态模式通过将状态逻辑封装到独立的类中,有效地提高了代码的可维护性和扩展性。
7. 策略模式(Strategy Pattern)
7.1 背景与动机
在开发中,某些功能可能需要多种不同的实现方式。例如:
- 支付系统:
一个电商系统支持多种支付方式(如支付宝、微信支付、信用卡支付)。 - 排序算法:
根据不同场景可能需要快速排序、归并排序或插入排序。
if (paymentType.equals("Alipay")) {
// 使用支付宝支付
} else if (paymentType.equals("WeChat")) {
// 使用微信支付
} else {
// 其他支付方式
}
痛点:
- 条件判断导致代码臃肿且难以维护。
- 扩展新的策略需要修改已有代码,违反开闭原则。
7.2 模式定义与结构
策略模式定义了一系列算法,将它们封装为独立的类,使它们可以互相替换,且算法的变化不会影响使用算法的客户端。
+------------------+
| Context |
|------------------|
| - strategy: Strategy |
|------------------|
| + setStrategy(Strategy) |
| + executeStrategy() |
+------------------+
^
|
+-------------------+
| Strategy |
|-------------------|
| + execute() |
+-------------------+
^
|
+----------------+ +----------------+
| ConcreteStrategyA | | ConcreteStrategyB |
|-------------------| |-------------------|
| + execute() | | + execute() |
+-------------------+ +-------------------+
主要角色:
- Context (上下文):
持有策略对象,并调用策略对象的具体方法。 - Strategy (抽象策略):
定义所有具体策略的公共接口。 - ConcreteStrategy (具体策略):
实现具体的算法或行为。
7.3 源码示例
// 抽象策略
interface PaymentStrategy {
void pay(int amount);
}
// 具体策略:支付宝支付
class AlipayStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using Alipay.");
}
}
// 具体策略:微信支付
class WeChatPayStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using WeChat Pay.");
}
}
// 上下文
class PaymentContext {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void executePayment(int amount) {
strategy.pay(amount);
}
}
// 客户端
public class StrategyPatternDemo {
public static void main(String[] args) {
PaymentContext context = new PaymentContext();
// 使用支付宝支付
context.setStrategy(new AlipayStrategy());
context.executePayment(100);
// 使用微信支付
context.setStrategy(new WeChatPayStrategy());
context.executePayment(200);
}
}
Paid 100 using Alipay.
Paid 200 using WeChat Pay.
7.4 设计原则分析
- 开闭原则:
新增策略时无需修改已有代码,只需增加新的策略类。 - 单一职责原则:
每个策略类只负责实现一种具体的行为或算法。 - 依赖倒置原则:
上下文依赖于策略接口,而不是具体实现。
7.5 实际应用场景
- 支付系统:
不同支付方式(支付宝、微信、信用卡)的实现。 - 排序算法:
根据数据规模选择不同的排序算法。 - 日志记录:
根据日志级别选择不同的记录方式(如控制台、文件、数据库)。
7.6 开源示例
- Spring Security 中的认证提供了多种策略(如用户名密码、OAuth2、JWT)。
// 抽象策略
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
// 具体策略:用户名密码认证
public class DaoAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) {
// 用户名密码认证逻辑
}
}
// 具体策略:OAuth2 认证
public class OAuth2AuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) {
// OAuth2 认证逻辑
}
}
2.Dubbo 是阿里巴巴开源的分布式服务框架,它使用 策略模式 来实现 负载均衡算法,支持 随机、轮询、一致性哈希等多种策略。
// 1. 负载均衡策略接口
public interface LoadBalance {
Invoker select(List<Invoker> invokers);
}
// 2. 具体策略:随机负载均衡
public class RandomLoadBalance implements LoadBalance {
private final Random random = new Random();
@Override
public Invoker select(List<Invoker> invokers) {
return invokers.get(random.nextInt(invokers.size()));
}
}
// 3. 具体策略:轮询负载均衡
public class RoundRobinLoadBalance implements LoadBalance {
private int index = 0;
@Override
public Invoker select(List<Invoker> invokers) {
index = (index + 1) % invokers.size();
return invokers.get(index);
}
}
// 4. 负载均衡上下文
public class LoadBalanceContext {
private LoadBalance loadBalance;
public LoadBalanceContext(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
public Invoker doSelect(List<Invoker> invokers) {
return loadBalance.select(invokers);
}
}
3.在日志框架中,我们可以 选择不同的日志存储方式(本地、数据库、远程 ELK)。Logback 通过 策略模式 允许我们动态更换日志存储策略。
// 1. 日志策略接口
interface LogStrategy {
void log(String message);
}
// 2. 具体策略:控制台日志
class ConsoleLogStrategy implements LogStrategy {
@Override
public void log(String message) {
System.out.println("ConsoleLog: " + message);
}
}
// 3. 具体策略:文件日志
class FileLogStrategy implements LogStrategy {
@Override
public void log(String message) {
System.out.println("FileLog: " + message);
}
}
// 4. 日志上下文
class LoggerContext {
private LogStrategy logStrategy;
public LoggerContext(LogStrategy logStrategy) {
this.logStrategy = logStrategy;
}
public void log(String message) {
logStrategy.log(message);
}
}
// 5. 测试
public class LogStrategyExample {
public static void main(String[] args) {
LoggerContext context = new LoggerContext(new ConsoleLogStrategy());
context.log("这是控制台日志");
context = new LoggerContext(new FileLogStrategy());
context.log("这是文件日志");
}
}
LogStrategy
定义日志存储策略,ConsoleLogStrategy
和FileLogStrategy
实现不同的日志存储方式。LoggerContext
动态切换日志存储策略,支持不同的日志存储方案。
4.MyBatis 允许支持 多种数据库(MySQL、Oracle、PostgreSQL、SQL Server),它使用 策略模式 来选择 不同数据库的 SQL 语法(方言)。
// 1. 数据库方言接口
interface SqlDialect {
String getLimitQuery(String query, int limit);
}
// 2. MySQL 方言
class MySQLDialect implements SqlDialect {
@Override
public String getLimitQuery(String query, int limit) {
return query + " LIMIT " + limit;
}
}
// 3. Oracle 方言
class OracleDialect implements SqlDialect {
@Override
public String getLimitQuery(String query, int limit) {
return "SELECT * FROM ( " + query + " ) WHERE ROWNUM <= " + limit;
}
}
// 4. 数据库上下文
class DatabaseContext {
private SqlDialect dialect;
public DatabaseContext(SqlDialect dialect) {
this.dialect = dialect;
}
public String getLimitQuery(String query, int limit) {
return dialect.getLimitQuery(query, limit);
}
}
SqlDialect
定义 SQL 语法策略,不同数据库(MySQL、Oracle)有不同的 SQL 语法。DatabaseContext
根据不同数据库动态选择 SQL 生成方式。
7.7 优缺点分析
优点:
- 遵循开闭原则,易于扩展新策略。
- 消除了条件判断语句,代码更清晰。
缺点:
- 增加了类的数量,每个策略需要单独的类。
- 策略类可能会被频繁实例化,增加性能开销。
7.8 问题反思与解答
- 如何管理大量策略类?
可以通过工厂模式结合策略模式,动态管理策略实例化过程。 - 如何避免频繁创建策略对象?
使用单例模式或缓存策略对象,减少创建开销。
7.9 适用性分析
- 适用场景:
- 有多种算法或行为可以互相替换。
- 客户端需要动态选择具体行为。
- 不适用场景:
- 策略数量较少且不会频繁扩展。
7.10 常见误区
- 将策略选择逻辑硬编码到上下文中:
策略模式应动态选择具体策略,而不是在上下文中硬编码逻辑。 - 误用策略模式处理简单情况:
对于简单情况,条件判断可能更高效。
7.11 与其他设计模式的关系
- 状态模式:
策略模式侧重于行为的互换,状态模式强调对象行为随状态改变。 - 装饰模式:
装饰模式动态增强对象功能,而策略模式动态替换行为。
7.12 性能考虑
- 减少策略类实例化的开销:
使用单例或缓存管理策略对象。 - 优化策略选择:
使用配置文件或策略映射表,快速定位所需策略。
策略模式通过将算法或行为封装为独立的策略类,提高了代码的可扩展性和灵活性。
8. 模板方法模式(Template Method Pattern)
8.1 背景与动机
在开发过程中,经常会遇到需要重复使用某些通用流程的场景。例如:
- 文档生成:
不同类型的文档(PDF、Word)生成逻辑可能类似,但细节处理不同。 - 数据导入导出:
不同的数据格式(CSV、JSON)的处理逻辑相似,但读取和写入方式不同。
问题:
传统实现中,可能会通过复制粘贴的方式处理每个类型的逻辑。
这会导致以下问题:
- 代码重复:
通用逻辑在多个地方被重复实现,难以维护。 - 难以扩展:
修改通用逻辑需要逐一调整多个实现。
解决方案:
通过模板方法模式,将通用逻辑放入基类,不同的具体实现由子类负责。
8.2 模式定义与结构
模板方法模式定义一个操作的骨架,将一些步骤延迟到子类中。通过子类可以不改变算法结构的情况下,重新定义算法中的某些步骤。
+--------------------------+
| AbstractClass |
|--------------------------|
| + templateMethod() |
| - primitiveOperation1() |
| - primitiveOperation2() |
+--------------------------+
^
|
+--------------------+ +--------------------+
| ConcreteClassA | | ConcreteClassB |
|--------------------| |--------------------|
| - primitiveOperation1() | - primitiveOperation1() |
| - primitiveOperation2() | - primitiveOperation2() |
+--------------------+ +--------------------+
主要角色:
- AbstractClass (抽象类):
定义算法的骨架,并声明一些抽象方法由子类实现。 - ConcreteClass (具体类):
实现抽象类中的抽象方法,完成具体步骤。
8.3 源码示例
// 抽象类
abstract class DocumentGenerator {
// 模板方法
public final void generateDocument() {
openDocument();
writeContent();
saveDocument();
closeDocument();
}
protected abstract void writeContent(); // 抽象方法
private void openDocument() {
System.out.println("Opening document...");
}
private void saveDocument() {
System.out.println("Saving document...");
}
private void closeDocument() {
System.out.println("Closing document...");
}
}
// 具体实现类:生成 PDF 文档
class PDFGenerator extends DocumentGenerator {
@Override
protected void writeContent() {
System.out.println("Writing content to PDF...");
}
}
// 具体实现类:生成 Word 文档
class WordGenerator extends DocumentGenerator {
@Override
protected void writeContent() {
System.out.println("Writing content to Word...");
}
}
// 客户端
public class TemplateMethodDemo {
public static void main(String[] args) {
DocumentGenerator pdf = new PDFGenerator();
pdf.generateDocument();
DocumentGenerator word = new WordGenerator();
word.generateDocument();
}
}
Opening document...
Writing content to PDF...
Saving document...
Closing document...
Opening document...
Writing content to Word...
Saving document...
Closing document...
8.4 设计原则分析
- 开闭原则:
修改流程不需要改变模板方法,只需扩展新的实现类。 - 依赖倒置原则:
高层模块依赖抽象类,而不是具体实现类。 - 里氏替换原则:
子类可以替换父类,完成模板方法的具体实现。
8.5 实际应用场景
- 文档生成:
不同类型的文档有相似的生成流程,但内容处理方式不同。 - 游戏框架:
游戏的启动、运行、结束流程固定,但具体实现因游戏类型而异。 - 数据导入导出:
统一处理导入导出流程,具体解析由子类实现。
8.6 开源示例
- Spring 框架的
TransactionTemplate
使用了模板方法模式,用于简化事务管理,使得业务代码不需要显式管理事务。
// 1. 事务模板(定义模板方法)
public abstract class TransactionTemplate {
// 模板方法,定义事务流程
public final void execute() {
try {
beginTransaction(); // 开启事务
doTransaction(); // 执行业务逻辑
commitTransaction(); // 提交事务
} catch (Exception e) {
rollbackTransaction(); // 事务回滚
}
}
private void beginTransaction() {
System.out.println("开启事务");
}
private void commitTransaction() {
System.out.println("提交事务");
}
private void rollbackTransaction() {
System.out.println("回滚事务");
}
// 需要子类实现的抽象方法
protected abstract void doTransaction();
}
// 2. 具体实现:订单支付事务
class OrderTransaction extends TransactionTemplate {
@Override
protected void doTransaction() {
System.out.println("处理订单支付逻辑...");
}
}
// 3. 具体实现:库存扣减事务
class StockTransaction extends TransactionTemplate {
@Override
protected void doTransaction() {
System.out.println("处理库存扣减逻辑...");
}
}
// 4. 测试
public class TemplateMethodExample {
public static void main(String[] args) {
TransactionTemplate transaction = new OrderTransaction();
transaction.execute();
transaction = new StockTransaction();
transaction.execute();
}
}
TransactionTemplate
定义事务的标准流程(开启事务 → 执行业务逻辑 → 提交或回滚事务)。OrderTransaction
和StockTransaction
实现 doTransaction() 方法,提供不同的事务处理逻辑。
2.JUnit 使用 模板方法模式 来规范 测试流程,确保每个测试用例的执行顺序:
public abstract class TestCase {
// 模板方法,执行测试流程
public final void runTest() {
setUp(); // 1. 测试前置逻辑
execute(); // 2. 执行测试
tearDown(); // 3. 测试后置逻辑
}
protected abstract void execute(); // 让子类实现具体的测试逻辑
protected void setUp() {
System.out.println("初始化测试环境");
}
protected void tearDown() {
System.out.println("清理测试环境");
}
}
// 具体测试用例
class LoginTest extends TestCase {
@Override
protected void execute() {
System.out.println("执行登录测试");
}
}
// 测试执行
public class JUnitTemplateExample {
public static void main(String[] args) {
TestCase test = new LoginTest();
test.runTest();
}
}
TestCase
定义测试执行流程(setUp → execute → tearDown
)。LoginTest
实现 execute() 方法,提供不同的测试逻辑。
3.Servlet 的 service()
方法是一个 模板方法,它调用 doGet()
或 doPost()
方法,而 doGet()
和 doPost()
由子类实现。
public abstract class HttpServlet {
// 模板方法,定义 HTTP 请求处理流程
public final void service(String requestMethod) {
if ("GET".equals(requestMethod)) {
doGet();
} else if ("POST".equals(requestMethod)) {
doPost();
}
}
protected void doGet() {
throw new UnsupportedOperationException("GET 请求未实现");
}
protected void doPost() {
throw new UnsupportedOperationException("POST 请求未实现");
}
}
// 具体实现
class MyServlet extends HttpServlet {
@Override
protected void doGet() {
System.out.println("处理 GET 请求");
}
@Override
protected void doPost() {
System.out.println("处理 POST 请求");
}
}
// 测试
public class ServletTemplateExample {
public static void main(String[] args) {
HttpServlet servlet = new MyServlet();
servlet.service("GET");
servlet.service("POST");
}
}
HttpServlet
定义 service()
处理流程,根据请求方法调用 doGet()
或 doPost()
。
MyServlet
实现 doGet()
和 doPost()
,提供具体的业务逻辑。
4.RocketMQ 提供 MessageListenerConcurrently
定义消息消费的模板方法,具体业务逻辑由开发者实现。
public interface MessageListenerConcurrently {
// 模板方法
ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages);
}
// 具体消费逻辑
public class OrderMessageListener implements MessageListenerConcurrently {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages) {
for (MessageExt message : messages) {
System.out.println("消费订单消息: " + new String(message.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
MessageListenerConcurrently
定义消息消费的流程。OrderMessageListener
实现consumeMessage()
处理不同类型的消息。
8.7 优缺点分析
优点:
- 明确了算法的固定部分和可变部分。
- 增强了代码复用性。
- 避免了代码重复和流程控制的硬编码。
缺点:
- 子类数量可能较多。
- 父类和子类之间的关系较为紧密。
8.8 问题反思与解答
- 如何避免子类过多?
如果子类过多,可考虑是否可以使用策略模式替代。 - 模板方法的抽象是否过于复杂?
尽量将通用流程抽象到适度的粒度,避免过度设计。
8.9 适用性分析
适用场景:
- 通用流程固定,部分步骤需要根据需求实现。
- 重复代码较多且流程相似时。
替代场景:
- 如果流程的可变部分较多,可能更适合使用策略模式。
8.10 常见误区
- 滥用模板方法:
将不必要的细节强行抽象,导致代码难以理解。 - 忽略单一职责原则:
模板方法过于复杂,既包含流程定义,又包含业务逻辑。
8.11 性能考虑
模板方法模式本身对性能影响较小,但子类的实现可能会引入性能瓶颈。为提高性能:
-
避免子类方法实现过于复杂。
-
对模板方法中的常用流程进行优化或缓存。
9. 迭代器模式(Iterator Pattern)
9.1 背景与动机
在开发中,经常需要遍历容器对象中的元素,而不同容器的实现方式可能不尽相同。例如:
- 数组和列表:
两者结构不同,但都需要提供遍历功能。 - 树或图:
复杂的数据结构也需要支持自定义的遍历方式。
问题:
如果遍历逻辑直接写在容器类中,会带来以下问题:
- 容器和遍历逻辑耦合:
导致容器类变得臃肿,违背单一职责原则。 - 扩展性差:
修改遍历逻辑可能需要修改容器类。 - 多种遍历方式难以支持:
容器可能需要同时支持多种遍历方式(如正序、逆序等)。
解决方案:
引入迭代器模式,将遍历逻辑抽离到独立的迭代器对象中,使得容器和遍历逻辑解耦。
9.2 模式定义与结构
迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
+----------------+ +----------------------+
| Iterator |<----| ConcreteIterator |
|----------------| |----------------------|
| + hasNext() | | + hasNext() |
| + next() | | + next() |
+----------------+ +----------------------+
^
|
+-----------------+ +---------------------+
| Aggregate |<----| ConcreteAggregate |
|-----------------| |---------------------|
| + iterator() | | + iterator() |
+-----------------+ +---------------------+
主要角色:
- Iterator (迭代器接口):
定义访问和遍历元素的方法(如next()
和hasNext()
)。 - ConcreteIterator (具体迭代器):
实现迭代器接口,负责具体的遍历逻辑。 - Aggregate (聚合接口):
定义创建迭代器的方法。 - ConcreteAggregate (具体聚合类):
实现聚合接口,存储数据并创建相应的迭代器对象。
9.3 源码示例
// 迭代器接口
interface Iterator<T> {
boolean hasNext();
T next();
}
// 聚合接口
interface Aggregate<T> {
Iterator<T> iterator();
}
// 具体聚合类
class CustomList<T> implements Aggregate<T> {
private List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
}
@Override
public Iterator<T> iterator() {
return new ListIterator();
}
// 内部类:具体迭代器
private class ListIterator implements Iterator<T> {
private int index = 0;
@Override
public boolean hasNext() {
return index < items.size();
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return items.get(index++);
}
}
}
// 客户端代码
public class IteratorPatternDemo {
public static void main(String[] args) {
CustomList<String> list = new CustomList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
A
B
C
9.4 设计原则分析
- 单一职责原则 (SRP):
将遍历逻辑从容器类中剥离出来,降低了容器类的复杂性。 - 开闭原则 (OCP):
可以新增不同的迭代器实现,而无需修改容器类。 - 依赖倒置原则 (DIP):
客户端依赖于抽象的迭代器接口,而不是具体实现。
9.5 实际应用场景
- 集合框架:
各种编程语言的集合框架(如 Java 的List
、Set
等)都使用了迭代器模式。 - 文件解析:
遍历文件内容或配置文件中的键值对。 - 图形界面组件:
遍历图形组件树中的各个节点。
9.6 开源示例
- Java 集合框架中的
Iterator
接口 直接实现了迭代器模式,用于遍历List
、Set
、Map
等集合。
// 1. 迭代器接口
public interface Iterator<E> {
boolean hasNext(); // 是否还有元素
E next(); // 获取下一个元素
}
// 2. 具体迭代器实现(针对 ArrayList)
class ArrayListIterator<E> implements Iterator<E> {
private List<E> list;
private int index = 0;
public ArrayListIterator(List<E> list) {
this.list = list;
}
@Override
public boolean hasNext() {
return index < list.size();
}
@Override
public E next() {
return list.get(index++);
}
}
// 3. 具体集合类
class MyArrayList<E> {
private List<E> elements = new ArrayList<>();
public void add(E e) {
elements.add(e);
}
public Iterator<E> iterator() {
return new ArrayListIterator<>(elements);
}
}
// 4. 测试迭代器
public class IteratorPatternExample {
public static void main(String[] args) {
MyArrayList<String> list = new MyArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
-
Iterator
定义遍历接口(hasNext()
、next()
)。 -
ArrayListIterator
实现Iterator
,封装遍历逻辑。 -
MyArrayList
提供iterator()
方法,返回迭代器实例。
2.在 MyBatis 中,DefaultResultSetHandler
使用迭代器模式 处理数据库查询的 ResultSet
,逐行映射数据到 Java 对象。
// 1. 迭代器接口
public interface ResultSetIterator {
boolean hasNext();
Object next();
}
// 2. 具体迭代器实现
public class MyBatisResultSetIterator implements ResultSetIterator {
private ResultSet resultSet;
public MyBatisResultSetIterator(ResultSet resultSet) {
this.resultSet = resultSet;
}
@Override
public boolean hasNext() {
try {
return resultSet.next();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Object next() {
try {
return resultSet.getObject(1); // 仅示例,实际会映射到 Java Bean
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
ResultSetIterator
定义数据库结果集的遍历接口。MyBatisResultSetIterator
封装ResultSet
迭代逻辑,避免直接操作ResultSet
。
3.Spring 的 BeanFactory
使用迭代器模式 遍历容器中的 BeanDefinition
,在应用启动时进行 Bean 的加载。
// 1. 迭代器接口
public interface BeanIterator {
boolean hasNext();
BeanDefinition next();
}
// 2. 具体迭代器实现
public class BeanDefinitionIterator implements BeanIterator {
private List<BeanDefinition> beanDefinitions;
private int index = 0;
public BeanDefinitionIterator(List<BeanDefinition> beanDefinitions) {
this.beanDefinitions = beanDefinitions;
}
@Override
public boolean hasNext() {
return index < beanDefinitions.size();
}
@Override
public BeanDefinition next() {
return beanDefinitions.get(index++);
}
}
-
BeanIterator
定义 Bean 遍历的标准方法。 -
BeanDefinitionIterator
实现迭代逻辑,遍历 Bean 定义
4.Hibernate 提供 ScrollableResults
封装数据库结果集的迭代,避免一次性加载大量数据到内存。
ScrollableResults results = query.scroll();
while (results.next()) {
Object entity = results.get(0);
}
-
Guava 提供
PeekingIterator
增强 Java 原生Iterator
,支持 预览下一个元素。PeekingIterator<String> iterator = Iterators.peekingIterator(list.iterator()); while (iterator.hasNext()) { System.out.println("当前元素: " + iterator.next()); if (iterator.hasNext()) { System.out.println("下一个元素: " + iterator.peek()); } }
9.7 优缺点分析
优点:
- 遍历逻辑独立,容器类职责单一。
- 支持多种遍历方式(如正序、逆序、深度优先等)。
- 提供统一接口,客户端代码简单一致。
缺点:
- 对于简单遍历,可能引入不必要的复杂性。
- 增加了类的数量和系统的复杂性。
9.8 问题反思与解答
- 是否总是需要使用迭代器?
对于简单容器,直接提供遍历方法可能更高效。 - 如何管理迭代器状态?
当容器发生变化时,需确保迭代器的状态同步更新。
9.9 适用性分析
适用场景:
- 容器需要支持多种遍历方式。
- 需要为不同容器提供统一的遍历接口。
替代场景:
- 如果容器很简单,直接提供遍历方法可能更适合。
9.10 常见误区
- 将所有遍历逻辑强行抽象到迭代器中:
不同容器的特殊逻辑可能需要单独处理。 - 忽视迭代器的性能问题:
不当的实现可能导致多次重复计算。
9.11 与其他设计模式的关系
- 与组合模式:
组合模式中的元素通常需要支持迭代器来遍历子节点。 - 与工厂模式:
工厂模式可以用于创建迭代器实例。
9.12 性能考虑
- 内存消耗:
避免在迭代器中维护过多状态信息。 - 效率优化:
对于大型容器,使用懒加载方式避免一次性加载所有元素。
10. 访问者模式(Visitor Pattern)
10.1 背景与动机
问题:
当我们需要对一组不同行为的对象进行操作时,传统的方式是将这些操作写在每个对象的类中,这样导致了以下问题:
- 代码耦合:
操作和对象的结构紧密耦合,如果操作频繁变化,修改起来非常困难。 - 扩展困难:
新增操作时需要修改所有的类,违反了开闭原则(对扩展开放,对修改封闭)。 - 维护困难:
如果对象的结构和操作都非常复杂,增加新行为时可能引入大量重复代码,导致维护困难。
解决方案:
访问者模式将操作和对象的结构分离开来,将操作的实现集中在一个访问者对象中,允许我们在不修改对象结构的情况下增加新的操作。
10.2 模式定义与结构
访问者模式使得我们可以在不改变类的前提下,定义作用于类元素的新操作。通过将操作封装到访问者对象中,实现对象和操作的分离。
+----------------+ +-----------------+
| Element |<--------| ConcreteElement |
|----------------| |-----------------|
| + accept() | | + accept() |
+----------------+ +-----------------+
^
|
+-----------------+ +-------------------+
| Visitor |<------| ConcreteVisitor |
|-----------------| |-------------------|
| + visitElementA() | | + visitElementA() |
| + visitElementB() | | + visitElementB() |
+-----------------+ +-------------------+
主要角色:
- Element (元素接口):
该接口定义一个accept()
方法,该方法接受一个访问者对象。 - ConcreteElement (具体元素):
具体元素实现accept()
方法,调用访问者的相关方法,传递自己。 - Visitor (访问者接口):
定义了访问元素的方法,通常会为不同类型的元素提供不同的visit()
方法。 - ConcreteVisitor (具体访问者):
实现访问者接口,定义操作的具体行为。
10.3 源码示例
// 元素接口
interface Element {
void accept(Visitor visitor);
}
// 具体元素A
class ElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visitElementA(this);
}
public void operationA() {
System.out.println("ElementA operation");
}
}
// 具体元素B
class ElementB implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visitElementB(this);
}
public void operationB() {
System.out.println("ElementB operation");
}
}
// 访问者接口
interface Visitor {
void visitElementA(ElementA elementA);
void visitElementB(ElementB elementB);
}
// 具体访问者
class ConcreteVisitor implements Visitor {
@Override
public void visitElementA(ElementA elementA) {
elementA.operationA();
System.out.println("Visited ElementA");
}
@Override
public void visitElementB(ElementB elementB) {
elementB.operationB();
System.out.println("Visited ElementB");
}
}
// 客户端代码
public class VisitorPatternDemo {
public static void main(String[] args) {
Element elementA = new ElementA();
Element elementB = new ElementB();
Visitor visitor = new ConcreteVisitor();
elementA.accept(visitor);
elementB.accept(visitor);
}
}
ElementA operation
Visited ElementA
ElementB operation
Visited ElementB
10.4 设计原则分析
- 开闭原则 (OCP):
访问者模式允许我们在不修改对象结构的情况下,为对象添加新的操作,符合开闭原则。 - 单一职责原则 (SRP):
将操作和对象分开,每个类承担单一的职责,使得代码更容易维护。 - 依赖倒置原则 (DIP):
客户端代码依赖于抽象的访问者接口,而不是具体的实现。
10.5 实际应用场景
- 编译器设计:
编译器通常会为不同的语法树节点定义不同的操作,访问者模式使得在不修改节点结构的情况下增加新操作成为可能。 - GUI 组件:
访问者模式可用于遍历图形组件树(如按钮、文本框等)并执行操作。 - 数据处理:
在复杂的数据结构中,遍历不同类型的元素并执行不同的操作时,访问者模式非常有用。
10.6 开源示例
- 在 Apache Commons Lang 中,
Visitor
模式用于处理复杂的数据结构,比如树形结构的数据,遍历并进行数据处理。
// 定义一个 Visitor 接口
public interface TreeVisitor {
void visitNode(Node node);
}
// 具体访问者
public class NodeVisitor implements TreeVisitor {
@Override
public void visitNode(Node node) {
System.out.println("Visiting node with value: " + node.getValue());
}
}
// 节点类
public class Node {
private String value;
public Node(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void accept(TreeVisitor visitor) {
visitor.visitNode(this);
}
}
2.Java 编译器 javac
使用访问者模式 解析 AST(抽象语法树),所有 Java 代码在编译时都会经过 AST 处理。
// 1. 定义访问者接口
public interface TreeVisitor {
void visitMethod(MethodTree method);
void visitClass(ClassTree clazz);
}
// 2. 具体访问者(打印 AST 节点)
public class PrintTreeVisitor implements TreeVisitor {
@Override
public void visitMethod(MethodTree method) {
System.out.println("访问方法: " + method.getName());
}
@Override
public void visitClass(ClassTree clazz) {
System.out.println("访问类: " + clazz.getName());
}
}
// 3. AST 节点基类
public abstract class TreeNode {
abstract void accept(TreeVisitor visitor);
}
// 4. 具体 AST 节点(方法)
public class MethodTree extends TreeNode {
private String name;
public MethodTree(String name) { this.name = name; }
public String getName() { return name; }
@Override
void accept(TreeVisitor visitor) {
visitor.visitMethod(this);
}
}
// 5. 具体 AST 节点(类)
public class ClassTree extends TreeNode {
private String name;
public ClassTree(String name) { this.name = name; }
public String getName() { return name; }
@Override
void accept(TreeVisitor visitor) {
visitor.visitClass(this);
}
}
// 6. 测试访问者模式
public class VisitorPatternExample {
public static void main(String[] args) {
TreeNode methodNode = new MethodTree("main");
TreeNode classNode = new ClassTree("TestClass");
TreeVisitor visitor = new PrintTreeVisitor();
methodNode.accept(visitor);
classNode.accept(visitor);
}
}
TreeVisitor
定义访问 AST 节点的方法(visitMethod()
、visitClass()
)。PrintTreeVisitor
实现TreeVisitor
,解析 AST 结构。TreeNode
提供accept(TreeVisitor visitor)
方法,允许访问者访问。
2.Spring AOP 使用访问者模式解析 AspectJ 表达式,在 org.aspectj.weaver.patterns
包中,NodeVisitor
访问 AST 节点,进行方法拦截。
// 1. 访问者接口
public interface NodeVisitor {
void visit(PointcutNode node);
void visit(MethodNode node);
}
// 2. 具体访问者(打印拦截的方法)
public class AspectJNodeVisitor implements NodeVisitor {
@Override
public void visit(PointcutNode node) {
System.out.println("访问 Pointcut: " + node.getExpression());
}
@Override
public void visit(MethodNode node) {
System.out.println("访问拦截方法: " + node.getMethodName());
}
}
// 3. AST 节点基类
public abstract class AspectJNode {
abstract void accept(NodeVisitor visitor);
}
// 4. 具体 AST 节点(Pointcut)
public class PointcutNode extends AspectJNode {
private String expression;
public PointcutNode(String expression) { this.expression = expression; }
public String getExpression() { return expression; }
@Override
void accept(NodeVisitor visitor) {
visitor.visit(this);
}
}
// 5. 具体 AST 节点(方法)
public class MethodNode extends AspectJNode {
private String methodName;
public MethodNode(String methodName) { this.methodName = methodName; }
public String getMethodName() { return methodName; }
@Override
void accept(NodeVisitor visitor) {
visitor.visit(this);
}
}
// 6. 测试访问者模式
public class SpringAopVisitorExample {
public static void main(String[] args) {
AspectJNode pointcut = new PointcutNode("execution(* com.example..*(..))");
AspectJNode method = new MethodNode("someMethod");
NodeVisitor visitor = new AspectJNodeVisitor();
pointcut.accept(visitor);
method.accept(visitor);
}
}
NodeVisitor
定义访问 AST 节点的接口(visit(PointcutNode)
)。AspectJNodeVisitor
解析 Pointcut 和拦截的方法。AspectJNode
提供accept()
方法,允许访问者访问。
3.MyBatis 使用访问者模式解析 SQL 映射 XML 文件,其中 org.apache.ibatis.parsing
包中 GenericTokenParser
解析 <select>、<insert>、<update>
等 XML 节点。
// 1. 访问者接口
public interface XMLVisitor {
void visitSelect(SelectNode node);
void visitInsert(InsertNode node);
}
// 2. 具体访问者(解析 SQL)
public class SQLVisitor implements XMLVisitor {
@Override
public void visitSelect(SelectNode node) {
System.out.println("解析 SELECT SQL: " + node.getSql());
}
@Override
public void visitInsert(InsertNode node) {
System.out.println("解析 INSERT SQL: " + node.getSql());
}
}
// 3. XML 节点基类
public abstract class XMLNode {
abstract void accept(XMLVisitor visitor);
}
// 4. 具体 XML 节点(SELECT)
public class SelectNode extends XMLNode {
private String sql;
public SelectNode(String sql) { this.sql = sql; }
public String getSql() { return sql; }
@Override
void accept(XMLVisitor visitor) {
visitor.visitSelect(this);
}
}
// 5. 具体 XML 节点(INSERT)
public class InsertNode extends XMLNode {
private String sql;
public InsertNode(String sql) { this.sql = sql; }
public String getSql() { return sql; }
@Override
void accept(XMLVisitor visitor) {
visitor.visitInsert(this);
}
}
// 6. 测试 MyBatis XML 解析
public class MyBatisXMLVisitorExample {
public static void main(String[] args) {
XMLNode selectNode = new SelectNode("SELECT * FROM users WHERE id = ?");
XMLNode insertNode = new InsertNode("INSERT INTO users (name, age) VALUES (?, ?)");
XMLVisitor visitor = new SQLVisitor();
selectNode.accept(visitor);
insertNode.accept(visitor);
}
}
XMLVisitor
定义访问 SQL 解析节点的接口。SQLVisitor
解析 SQL 语句(visitSelect()
)。XMLNode
提供accept()
方法,允许访问者解析 XML 节点。
10.7优缺点分析
优点:
- 扩展性:
可以很容易地为现有的元素类型添加新操作,而不需要修改元素类。 - 维护性:
操作和对象结构分离,提高了代码的可维护性。 - 行为集中管理:
所有的操作都集中在访问者类中,易于管理和修改。
缺点:
- 增加类的数量:
每个新操作需要创建一个新的访问者类,导致系统中类的数量增加。 - 适应性差:
访问者模式适用于需要扩展操作的场景,但如果操作比较少或不变,使用访问者模式可能导致设计过度。
10.8 问题反思与解答
- 是否总是适用访问者模式?
访问者模式适用于操作频繁变化的场景,但如果对象的操作较少且稳定,可能不需要使用访问者模式。 - 如何避免遍历复杂性?
在复杂的元素结构中,避免在访问者中嵌套过多逻辑,保持每个访问者职责清晰。
10.9 适用性分析
适用场景:
- 需要对对象的不同类型执行不同的操作。
- 系统中的对象结构需要频繁添加新的操作。
- 操作与对象结构解耦,以便于扩展和修改。
替代方案:
- 策略模式:
如果操作较为简单,策略模式可以替代访问者模式,尤其是在对象结构较简单的情况下。
10.10 常见误区
- 访问者不适合所有场景:
对于不频繁变化的操作,访问者模式可能是过度设计,简单的多态和继承可能足够满足需求。 - 访问者实现过于复杂:
在复杂的访问者中,操作的实现可能过于臃肿,应尽量保持访问者的职责单一。
10.11 模式变种
- 复合访问者:
支持访问者嵌套,可以为不同类型的元素组合不同的访问者。 - 双向访问者:
支持两种方向的访问,即不仅元素可以接受访问者,访问者也能主动获取元素的状态。
10.12 与其他设计模式的关系
- 与组合模式:
访问者模式常与组合模式结合使用,用于遍历复杂的树形结构。 - 与策略模式:
两者都涉及行为的封装。策略模式通过将算法封装成对象来改变算法,而访问者模式将操作封装到访问者对象中。
10.13 性能考虑
- 内存消耗:
访问者模式可能会导致多个访问者实例的创建,增加内存消耗。 - 性能优化:
避免在访问者中执行复杂计算,保持每个访问者操作的高效性,特别是当访问的元素结构非常庞大时。