23大设计模式(即软件设计中的24种常用设计模式)源自《设计模式:可复用面向对象软件的基础》一书,由四位作者(Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)提出,通常也被称为“GoF设计模式”。这些模式分为三大类:创建型模式、结构型模式和行为型模式。
知识体系系统性梳理
创建型模式(4种)
创建型模式处理对象创建问题,注重如何使系统在创建对象时更加灵活和高效。
单例模式(Singleton Pattern)
单例模式的定义是:确保一个类最多只有一个实例,并提供一个全局访问点。这是为了保证系统中的某些资源(如配置类、数据库连接等)只有一个全局的实例,防止创建多个实例导致不一致的状态或资源浪费。
在单例模式中,可以根据实例的创建时机分为两种实现方式:预加载和懒加载。
预加载(饿汉式,Eager Initialization)
在预加载模式下,单例实例在类加载时就会被创建,无论是否已经使用。这种方式创建的单例对象是线程安全的,因为实例是在类加载过程中完成的,且 JVM 保证类加载过程中的线程安全性。
public class Singleton {
// 1. 静态实例在类加载时创建
private static final Singleton instance = new Singleton();
// 2. 私有化构造函数,防止外部实例化
private Singleton() {}
// 3. 提供全局访问点
public static Singleton getInstance() {
return instance;
}
}
特点:
- 线程安全:因为实例是在类加载的时候创建的,线程安全性由 JVM 保证。
- 高效:没有加锁,也没有延迟创建,每次访问时都能立即返回实例。
- 缺点:即使单例不被使用,它也会一直存在,可能会浪费系统资源,特别是当单例类非常占用内存时。
懒加载(懒汉式,Lazy Initialization)
在懒加载模式下,单例实例只有在第一次调用时才会被创建。这种方式可以避免系统资源的浪费,尤其是在实例的创建代价较大或单例并不常用的情况下。懒加载的单例模式可以是线程安全的,也可以是非线程安全的。
非线程安全的懒加载实现:
public class Singleton {
private static Singleton instance;
// 私有化构造函数,防止外部实例化
private Singleton() {}
// 提供全局访问点(非线程安全)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 第一次调用时创建实例
}
return instance;
}
}
线程安全的懒加载实现:
- 通过 同步 或 双重检查锁 来确保线程安全。
线程安全的同步实现:
public class Singleton {
private static Singleton instance;
private Singleton() {}
// 通过 synchronized 确保线程安全
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检查锁(Double-Checked Locking):
public class Singleton {
// 使用 volatile 防止指令重排序
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
特点:
- 懒加载:实例在第一次使用时才创建,避免了不必要的资源浪费。
- 线程安全的懒加载需要增加一定的性能开销(如加锁、双重检查锁)。
- 延迟加载:适合那些实例化开销较大或不一定会被使用的类。
总结:预加载 vs 懒加载
- 预加载(饿汉式):实例在类加载时就创建,线程安全但可能浪费资源。
- 懒加载(懒汉式):实例在第一次调用时创建,懒加载节约资源,但需要考虑线程安全问题。
原型模式(Prototype Pattern)
原型模式(Prototype Pattern)允许通过复制现有对象来创建新的对象,而不是通过类的实例化。这样可以避免使用 new 关键字直接创建对象,尤其在对象创建成本较高或需要大量类似对象时,原型模式非常有用。
原型模式的关键要素:
- 原型接口:定义了一个用于克隆自身的方法。
- 具体原型类:实现了原型接口,并定义了如何复制对象的具体过程。
- 客户端:通过调用原型对象的克隆方法来创建新的对象。
原型模式的实现步骤:
- 定义一个 Cloneable 接口(或实现 Java 提供的 Cloneable 接口)。
- 实现 clone() 方法,用于复制对象。
- 通过原型模式可以快速创建对象,而不需要直接调用构造函数。
public class Prototype implements Cloneable {
private String name;
private int age;
public Prototype(String name, int age) {
this.name = name;
this.age = age;
}
// 实现 clone 方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 使用 Object 类的 clone() 方法
}
// getter 和 setter 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Prototype{name='" + name + "', age=" + age + "}";
}
}
public class Main {
public static void main(String[] args) {
try {
// 创建原型对象
Prototype original = new Prototype("John", 30);
System.out.println("Original: " + original);
// 通过克隆创建新对象
Prototype cloned = (Prototype) original.clone();
System.out.println("Cloned: " + cloned);
// 修改克隆对象的属性,原型对象不受影响
cloned.setName("Jane");
cloned.setAge(25);
System.out.println("Modified Cloned: " + cloned);
System.out.println("Original after modification: " + original);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
深拷贝与浅拷贝
在原型模式中,深拷贝和浅拷贝是两个重要的概念:
- 浅拷贝(Shallow Copy):复制对象时,仅复制对象的基本类型字段和对引用类型的引用,引用类型本身不会被复制。对于对象的嵌套对象,它们指向的仍然是同一个内存地址。
- 深拷贝(Deep Copy):不仅复制基本类型字段,还会复制对象引用所指向的对象,保证副本对象与原始对象完全独立。
保护性拷贝
原型模式中的保护性拷贝是一种用于只读或防止对象状态被修改的策略。通过返回一个对象的拷贝,而不是返回对象的引用,用户可以对这个拷贝进行操作,但不会影响到原始对象的状态。
这种方式有效防止了原始对象的修改。例如,某个系统中的对象需要在不同模块共享,但不希望某些模块能够修改对象本身,就可以返回对象的一个克隆副本。
public class Configuration implements Cloneable {
private String configName; // 配置名称
private int configValue; // 配置值
public Configuration(String configName, int configValue) {
this.configName = configName;
this.configValue = configValue;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String getConfigName() {
return configName;
}
public int getConfigValue() {
return configValue;
}
}
public class Main {
public static void main(String[] args) {
try {
Configuration config = new Configuration("数据库配置", 100);
// 获取配置对象的克隆副本
Configuration configCopy = (Configuration) config.clone();
// 输出原始对象和克隆对象的配置名称
System.out.println("原始配置名称: " + config.getConfigName());
System.out.println("克隆配置名称: " + configCopy.getConfigName());
// 修改克隆副本,确保原始对象保持不变
configCopy = new Configuration("修改后的配置", 200);
System.out.println("修改后的克隆配置名称: " + configCopy.getConfigName());
System.out.println("原始配置值(克隆修改后): " + config.getConfigValue());
} catch (CloneNotSupportedException e) {
e.printStackTrace(); // 捕获异常并输出错误信息
}
}
}
总结
- 原型模式的核心是克隆,它可以快速复制现有对象,避免重新构建复杂对象的开销。
- 保护性拷贝是原型模式的一个重要用途,能通过返回副本的方式有效防止对象状态被修改,起到只读保护的作用。
工厂方法模式(Factory Method Pattern)
工厂模式主要包括三种不同的实现方式:简单工厂模式、工厂方法模式和抽象工厂模式。
简单工厂模式
定义:简单工厂模式不属于 GoF 设计模式,它通过一个工厂类来创建对象。这个工厂类根据提供的信息决定创建哪个产品类的实例。
适用场合:当只有一个工厂负责生产相同层次的产品(如不同类型的披萨),且产品类型固定,不支持扩展时。
优缺点:
- 优点:简单易用,适合小规模项目。
- 缺点:不利于扩展,一旦需要增加产品类型,必须修改工厂类。
结构
- 工厂类:负责创建对象的逻辑。
- 产品接口:定义产品的共同特征。
- 具体产品类:实现产品接口。
public class SimpleFactory {
public static Product createProduct(String type) {
if ("A".equals(type)) {
return new ConcreteProductA();
} else if ("B".equals(type)) {
return new ConcreteProductB();
}
return null;
}
}
// 客户端使用
Product product = SimpleFactory.createProduct("A");
工厂方法模式
定义:工厂方法模式通过定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法将类的实例化推迟到子类中。
适用场合:当工厂可以有多个实现(如伦敦工厂和纽约工厂),且每个工厂生产固定类型的产品(如三种披萨)时。
优缺点:
- 优点:支持扩展,可以轻松添加新工厂或产品类型。
- 缺点:增加了系统的复杂性,需要为每种产品创建对应的工厂类。
结构
- 产品接口:定义产品的共同特征。
- 具体产品类:实现产品接口。
- 工厂接口:定义创建产品的方法。
- 具体工厂类:实现工厂接口,负责实例化具体产品。
public interface ProductFactory {
Product createProduct();
}
public class ConcreteFactoryA implements ProductFactory {
@Override
public Product createProduct() {
return new ConcreteProductA();
}
}
// 客户端使用
ProductFactory factory = new ConcreteFactoryA();
Product product = factory.createProduct();
抽象工厂模式(Abstract Factory Pattern)
定义:抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。适用于需要创建多个相关产品的场景。
适用场合:当需要创建不同种类的产品(如不同类型的披萨)且这些产品之间可能存在关联时,比如在增加了新的披萨类型(如中式披萨)的情况下。
优缺点:
- 优点:高度灵活,支持扩展和变更,便于管理不同产品族。
- 缺点:系统复杂性较高,增加了类的数量。
结构
- 抽象工厂接口:定义创建不同产品的方法。
- 具体工厂类:实现抽象工厂接口,负责创建一系列相关产品。
- 产品接口:定义产品的共同特征。
- 具体产品类:实现产品接口。
public interface AbstractFactory {
ProductA createProductA();
ProductB createProductB();
}
public class ConcreteFactory1 implements AbstractFactory {
@Override
public ProductA createProductA() {
return new ConcreteProductA1();
}
@Override
public ProductB createProductB() {
return new ConcreteProductB1();
}
}
// 客户端使用
AbstractFactory factory = new ConcreteFactory1();
ProductA productA = factory.createProductA();
ProductB productB = factory.createProductB();
总结对比
- 简单工厂模式适合小型、固定的产品需求。
- 工厂方法模式适合多工厂、同类产品的需求,支持扩展。
- 抽象工厂模式适合多产品族的需求,能够灵活管理不同类型的产品。
建造者模式(Builder Pattern)
定义:建造者模式通过将对象的构建步骤逐一分离开来,使得对象的创建过程更加灵活,并且可以根据需要生成不同类型的对象。
结构
- Builder(建造者接口):定义创建产品各个部分的接口。
- ConcreteBuilder(具体建造者):实现 Builder 接口,负责构建产品的各个部分。
- Product(产品):要创建的复杂对象。
- Director(指挥者):控制建造者的构建过程,按照一定的顺序构建产品。
- Client(客户端):通过 Director 与 Builder 进行交互来获取最终的产品对象。
产品类
// 产品类:一个复杂对象
public class Product {
private String partA;
private String partB;
private String partC;
// Setter 方法
public void setPartA(String partA) {
this.partA = partA;
}
public void setPartB(String partB) {
this.partB = partB;
}
public void setPartC(String partC) {
this.partC = partC;
}
@Override
public String toString() {
return "Product [partA=" + partA + ", partB=" + partB + ", partC=" + partC + "]";
}
}
抽象建造者
// 建造者接口,定义了构建产品的步骤
public interface Builder {
void buildPartA();
void buildPartB();
void buildPartC();
Product getResult();
}
具体建造者
// 具体建造者,实现了构建步骤
public class ConcreteBuilder implements Builder {
private Product product = new Product();
@Override
public void buildPartA() {
product.setPartA("Part A");
}
@Override
public void buildPartB() {
product.setPartB("Part B");
}
@Override
public void buildPartC() {
product.setPartC("Part C");
}
@Override
public Product getResult() {
return product;
}
}
指挥者
// 指挥者,控制建造过程
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
// 指定建造顺序
public void construct() {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
}
}
客户端
public class Main {
public static void main(String[] args) {
// 创建具体建造者
Builder builder = new ConcreteBuilder();
// 创建指挥者并指挥建造过程
Director director = new Director(builder);
director.construct();
// 获取建造的产品
Product product = builder.getResult();
System.out.println(product);
}
}
使用场景
- 复杂对象的创建:创建过程比较复杂,且构建步骤需要灵活的控制。
- 同样的构建过程生成不同产品:可以在构建过程中通过不同的组合方式生成不同的对象。
建造者模式的优缺点
优点:
- 更好的控制复杂对象的创建过程:将复杂对象的构建过程拆分为多个步骤,逐步构建。
- 更好的扩展性:通过不同的建造者,可以生成不同类型的产品,方便扩展。
- 解耦:客户端不需要知道产品的创建细节,构造过程由建造者和指挥者协作完成。
缺点:
- 增加代码复杂度:需要额外创建 Builder 和 Director 类来管理对象的构建。
- 使用场景有限:适用于对象创建过程非常复杂的情况,对于简单的对象可能不适合。
结构型模式(7种)
结构型模式涉及如何将类和对象组合成更大的结构,以确保系统的功能能够高效扩展。
适配器模式(Adapter Pattern)
定义:适配器模式通过定义一个适配器类,将一个接口转化为客户端所期待的另一种接口。适配器模式通常用于两个接口不兼容的场景,允许不兼容的类可以协同工作。
目标接口
// 目标接口
public interface Target {
void request();
}
源类
// 源类,不兼容的接口
public class Adaptee {
public void specificRequest() {
System.out.println("执行源类的特定请求。");
}
}
适配器类
// 适配器类,实现目标接口并适配源类
public class Adapter implements Target {
private Adaptee adaptee;
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void request() {
// 调用源类的方法
adaptee.specificRequest();
}
}
客户端
public class Main {
public static void main(String[] args) {
// 创建源类的实例
Adaptee adaptee = new Adaptee();
// 使用适配器将源类适配为目标接口
Target target = new Adapter(adaptee);
// 调用目标接口的方法
target.request(); // 输出: 执行源类的特定请求。
}
}
使用场景
- 接口不兼容:需要将一个接口转换成另一个接口,使得不同接口的类可以协作。
- 旧代码的重用:想要重用某个旧的类,但该类的接口不符合新系统的需求时。
- 多种接口的适配:在同一个系统中需要适配多个类的接口。
适配器模式的优缺点
优点:
- 提高了类的复用性:可以通过适配器类来复用已有的类,而不必修改原有代码。
- 灵活性和可扩展性:可以在不影响客户端的情况下,方便地添加新适配器或修改现有的适配器。
- 解耦合:客户端与源类之间通过适配器进行解耦,降低了系统的复杂性。
缺点:
- 增加了系统的复杂性:引入了适配器类,增加了系统的复杂度,特别是在适配器数量较多时。
- 性能开销:适配器模式可能会引入额外的性能开销,因为需要通过适配器转发请求。
总结
适配器模式是一种有效的设计模式,用于解决由于接口不兼容而导致的类之间无法协作的问题。通过适配器的引入,可以在不改变现有类的情况下,灵活地实现接口之间的适配和转换。
桥接模式(Bridge Pattern)
定义:桥接模式将抽象与实现解耦,客户端通过抽象接口与实现部分交互,从而允许两者独立变化。它可以被认为是两个独立维度的扩展,一个是抽象层次,另一个是实现层次。
结构
- Abstraction(抽象类):定义抽象部分的接口,包含一个 Implementor 类型的引用。
- RefinedAbstraction(扩展抽象类):扩展抽象类,维护实现部分的引用。
- Implementor(实现类接口):定义实现类的接口,具体实现由具体的实现类来定义。
- ConcreteImplementor(具体实现类):具体实现 Implementor 接口,完成具体的实现工作。
实现类接口
// 实现类接口
public interface Implementor {
void operationImpl();
}
具体实现类
// 具体实现类1
public class ConcreteImplementorA implements Implementor {
@Override
public void operationImpl() {
System.out.println("具体实现 A 的操作。");
}
}
// 具体实现类2
public class ConcreteImplementorB implements Implementor {
@Override
public void operationImpl() {
System.out.println("具体实现 B 的操作。");
}
}
抽象类
// 抽象类
public abstract class Abstraction {
protected Implementor implementor;
public Abstraction(Implementor implementor) {
this.implementor = implementor;
}
public abstract void operation();
}
扩展抽象类
// 扩展抽象类
public class RefinedAbstraction extends Abstraction {
public RefinedAbstraction(Implementor implementor) {
super(implementor);
}
@Override
public void operation() {
System.out.println("扩展抽象类的操作。");
implementor.operationImpl(); // 调用实现部分的操作
}
}
客户端
public class Main {
public static void main(String[] args) {
// 使用具体实现A
Implementor implementorA = new ConcreteImplementorA();
Abstraction abstractionA = new RefinedAbstraction(implementorA);
abstractionA.operation();
// 使用具体实现B
Implementor implementorB = new ConcreteImplementorB();
Abstraction abstractionB = new RefinedAbstraction(implementorB);
abstractionB.operation();
}
}
桥接模式的优缺点
优点:
- 分离抽象和实现:可以独立地扩展抽象类和实现类,避免它们之间的强耦合。
- 提高系统的可扩展性:在不修改原有系统的情况下,可以扩展新的抽象和实现。
- 遵循单一职责原则:抽象类和实现类可以分别进行变化,彼此不受影响。
- 减少子类的数量:通过组合关系代替继承关系,避免创建大量的子类。
缺点:
- 增加系统复杂性:引入了更多的类和对象,可能增加系统的复杂性。
- 调试困难:由于抽象与实现是分离的,可能使调试变得更加复杂。
适用场景
- 需要跨越多个平台:例如图形绘制程序,抽象类可以表示“形状”,而实现类可以表示“绘制API”,不同的绘制API可以在不同的平台上使用。
- 多个维度的变化:系统需要面对多个维度的变化,比如颜色、形状等,不想通过继承来组合这些维度时,可以采用桥接模式。
- 避免继承爆炸:如果使用继承会产生大量的子类,且难以维护时,可以使用桥接模式。
桥接模式与适配器模式的区别
- 桥接模式:用于分离抽象与实现,使它们可以独立变化,强调的是系统内部的设计和扩展性。
- 适配器模式:用于将一个已有接口适配到另一个接口,解决接口不兼容的问题,主要用于系统之间的兼容。
总结
桥接模式通过将抽象与实现解耦,使得系统更灵活,适用于系统中多个维度都可能发生变化的场景。它可以有效地减少继承关系的数量,避免继承导致的类层次复杂度,但也会引入额外的类和对象,增加了系统的设计复杂性。
装饰器模式(Decorator Pattern)
定义:装饰器模式通过创建装饰类来包装原有的类(被装饰的类),并在保持原有类接口一致的情况下,动态地为该类添加新的职责或功能。
结构
- Component(抽象组件):定义一个接口,表示可以动态添加职责的对象。
- ConcreteComponent(具体组件):实现 Component 接口,表示一个具体的对象,它将接受额外的职责。
- Decorator(装饰类):实现 Component 接口,包含一个 Component 类型的成员变量,负责将请求委派给该成员对象。
- ConcreteDecorator(具体装饰类):继承 Decorator,为对象动态添加功能。
抽象组件
// 抽象组件接口
public interface Component {
void operation();
}
具体组件
// 具体组件,实现抽象组件
public class ConcreteComponent implements Component {
@Override
public void operation() {
System.out.println("执行基本操作。");
}
}
装饰类
// 装饰类,实现抽象组件接口,并持有一个组件的引用
public class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public void operation() {
component.operation(); // 委派给具体组件
}
}
具体装饰类
// 具体装饰类A,扩展原有功能
public class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operation() {
super.operation(); // 执行原有操作
addedBehaviorA(); // 添加新的功能
}
private void addedBehaviorA() {
System.out.println("附加功能 A。");
}
}
// 具体装饰类B,扩展原有功能
public class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
@Override
public void operation() {
super.operation(); // 执行原有操作
addedBehaviorB(); // 添加新的功能
}
private void addedBehaviorB() {
System.out.println("附加功能 B。");
}
}
客户端
public class Main {
public static void main(String[] args) {
// 创建具体组件
Component component = new ConcreteComponent();
// 使用装饰器A来装饰具体组件
Component decoratorA = new ConcreteDecoratorA(component);
// 再用装饰器B来装饰A
Component decoratorB = new ConcreteDecoratorB(decoratorA);
// 执行操作
decoratorB.operation();
}
}
装饰器模式的优缺点
优点:
- 职责的动态扩展:可以在运行时动态地为对象增加功能,避免了使用继承带来的类爆炸问题。
- 更灵活的功能组合:可以通过不同的装饰器进行功能的组合,满足不同的功能需求。
- 遵循开闭原则:无需修改现有的类结构,就可以通过添加新的装饰器类来扩展功能。
缺点:
- 生成大量的小对象:每个装饰器都会创建一个新的对象,这可能导致对象数量增多,增加系统的复杂性。
- 调试困难:由于功能是通过多个装饰器类层层叠加的,可能会增加调试和排错的难度。
适用场景
- 功能扩展:希望在不修改原有类的情况下,动态地为对象添加功能。
- 功能灵活组合:不同功能的组合需要灵活变化,例如GUI组件的功能扩展,日志系统的功能扩展。
- 需要在运行时增加功能:而不想通过静态继承关系进行扩展时,可以使用装饰器模式。
总结
装饰器模式通过将对象包裹在装饰器类中,动态地为对象添加新的职责。相比于继承,装饰器模式更加灵活,避免了因为继承导致的类爆炸问题,适用于需要扩展对象功能的场景。然而,它也会增加系统的复杂性,特别是在装饰器类较多时。
组合模式(Composite Pattern)
定义:组合模式通过将对象组合成树形结构,使得客户端可以对单个对象和组合对象进行一致的处理。
结构
- Component(抽象组件):为组合中的对象声明接口,定义接口的默认行为。
- Leaf(叶子组件):实现 Component 接口,表示树的叶子节点,不再包含其他子节点。
- Composite(组合对象):实现 Component 接口,表示一个容器对象,包含子节点,可以增加、删除子节点。
- Client(客户端):通过 Component 接口与组合结构进行交互。
抽象组件
// 抽象组件接口
public abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
public abstract void operation();
// 默认的添加、删除方法
public void add(Component component) {
throw new UnsupportedOperationException("不支持添加操作");
}
public void remove(Component component) {
throw new UnsupportedOperationException("不支持删除操作");
}
public Component getChild(int index) {
throw new UnsupportedOperationException("不支持获取子节点操作");
}
}
叶子组件
// 叶子组件
public class Leaf extends Component {
public Leaf(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("叶子节点 " + name + " 执行操作。");
}
}
组合组件
// 组合组件
import java.util.ArrayList;
import java.util.List;
public class Composite extends Component {
private List<Component> children = new ArrayList<>();
public Composite(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("组合节点 " + name + " 执行操作。");
for (Component child : children) {
child.operation(); // 递归调用子节点的操作
}
}
@Override
public void add(Component component) {
children.add(component);
}
@Override
public void remove(Component component) {
children.remove(component);
}
@Override
public Component getChild(int index) {
return children.get(index);
}
}
客户端
public class Main {
public static void main(String[] args) {
// 创建叶子节点
Component leaf1 = new Leaf("Leaf 1");
Component leaf2 = new Leaf("Leaf 2");
Component leaf3 = new Leaf("Leaf 3");
// 创建组合节点
Composite composite1 = new Composite("Composite 1");
Composite composite2 = new Composite("Composite 2");
// 组合节点添加子节点
composite1.add(leaf1);
composite1.add(leaf2);
composite2.add(leaf3);
composite2.add(composite1); // 组合节点中嵌套另一个组合节点
// 执行操作
composite2.operation();
}
}
组合模式的优缺点
优点:
- 统一处理:客户端可以一致地处理单个对象和组合对象,简化了对复杂树形结构的操作。
- 更容易扩展:通过组合可以灵活地增加新的叶子或组合对象,无需修改现有代码。
- 遵循开闭原则:可以轻松添加新的组件或组合,而无需修改现有代码。
缺点:
- 设计复杂性增加:如果系统中树的层次结构过于复杂,可能会导致类的数量增多,增加设计复杂性。
- 不适合高层次和底层差异较大的场景:组合模式的统一接口要求叶子和组合对象有一致的行为,但如果叶子和组合对象的行为差异较大,则可能会带来设计上的困难。
适用场景
- 树形结构:如文件系统、组织结构、图形控件等,具有明显的“部分-整体”层次关系。
- 需要统一处理单个对象和组合对象:客户不想关心是单个对象还是组合对象时,可以使用组合模式。
- 递归操作:如图形界面的绘制,可以递归遍历所有子节点进行渲染操作。
总结
组合模式是一种用于处理树形结构的设计模式,它允许客户端一致地处理单个对象和组合对象,使得系统可以灵活地进行扩展。在使用组合模式时,要确保叶子节点和组合节点具有一致的接口,这样可以使客户端不需要区分处理对象的类型。尽管组合模式简化了客户端的操作,但它也可能引入一定的设计复杂性,特别是在树形结构较为复杂的情况下。
外观模式(Facade Pattern)
定义:外观模式通过创建一个简单的接口来隐藏系统内部的复杂性,从而简化客户端与子系统的交互过程。
结构
- Facade(外观类):提供一个简化的接口,隐藏子系统的复杂性。
- Subsystems(子系统类):包含实际的业务逻辑或功能,客户端通过外观类与它们交互,但它们自己不需要知道外观类的存在。
- Client(客户端):通过外观类与子系统交互。
子系统类
// 子系统类1
public class SubsystemA {
public void operationA() {
System.out.println("子系统 A 的操作");
}
}
// 子系统类2
public class SubsystemB {
public void operationB() {
System.out.println("子系统 B 的操作");
}
}
// 子系统类3
public class SubsystemC {
public void operationC() {
System.out.println("子系统 C 的操作");
}
}
外观类
// 外观类
public class Facade {
private SubsystemA subsystemA;
private SubsystemB subsystemB;
private SubsystemC subsystemC;
public Facade() {
this.subsystemA = new SubsystemA();
this.subsystemB = new SubsystemB();
this.subsystemC = new SubsystemC();
}
// 提供给客户端的统一接口
public void operation() {
subsystemA.operationA();
subsystemB.operationB();
subsystemC.operationC();
}
}
客户端
public class Main {
public static void main(String[] args) {
// 创建外观类实例
Facade facade = new Facade();
// 客户端通过外观类与子系统交互
facade.operation();
}
}
外观模式的优缺点
优点:
- 简化客户端接口:客户端只需要与外观类交互,减少了与多个子系统之间的耦合,使得代码更加简洁、易懂。
- 松散耦合:外观模式将客户端与复杂子系统解耦,客户端只需关心外观类,减少了系统的依赖。
- 更好的分层:可以将系统分层处理,每一层都可以定义自己的外观类,使得每个层次的实现细节被封装。
缺点:
- 降低子系统的灵活性:外观类可能会隐藏子系统的部分功能,从而使客户端无法直接访问到一些功能。
- 可能增加代码复杂性:如果设计不当,外观类可能会变得臃肿,承担过多的职责,反而增加系统的复杂性。
适用场景
- 简化子系统的使用:当子系统包含多个复杂的接口,外观模式通过统一接口简化它们的使用。
- 系统分层:可以在系统的不同层次上使用外观模式,使得每层的实现细节对外部隐藏,保持层次间的独立性。
- 与遗留系统交互:使用外观模式包装旧有的系统或库,提供一个新接口,使得客户端可以更方便地与旧系统交互。
外观模式与其他模式的对比
- 外观模式 vs. 适配器模式:适配器模式是将一个接口转换为另一个接口,而外观模式是提供一个统一的接口来简化多个接口的调用。
- 外观模式 vs. 代理模式:代理模式用于控制对象的访问,而外观模式用于简化接口的使用。
总结
外观模式是一种用于简化系统与客户端交互的设计模式,它通过提供一个简单的接口,将复杂的子系统封装起来。通过外观模式,客户端不需要了解子系统的内部实现,降低了系统的复杂度和耦合度。在系统设计中,外观模式可以很好地用于简化复杂子系统的操作,尤其是在需要处理多个接口或子系统的情况下。
享元模式(Flyweight Pattern)
定义:享元模式运用共享技术有效地支持大量细粒度对象的复用,它通过将对象的内部状态和外部状态分离,来共享相同的内部状态,从而节省内存空间。
内部状态和外部状态
内部状态:对象可以共享的状态,不会随环境改变。内部状态通常由享元对象本身保存。
外部状态:对象不可以共享的状态,通常随着环境变化,由客户端在使用享元对象时传递。
结构
- Flyweight(享元接口):定义了对象的行为和接口,通常包含操作外部状态的方法。
- ConcreteFlyweight(具体享元类):实现享元接口,存储共享的内部状态。
- UnsharedConcreteFlyweight(非共享享元类):不被共享的类,通常是复合对象,持有多个享元对象。
- FlyweightFactory(享元工厂类):管理享元对象的创建和共享,确保客户端获取的对象是共享的。
- Client(客户端):通过享元工厂获取享元对象,并使用它们。
享元接口
// 享元接口
public interface Flyweight {
void operation(String externalState); // 操作外部状态
}
具体享元类
// 具体享元类
public class ConcreteFlyweight implements Flyweight {
private final String intrinsicState; // 内部状态
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
@Override
public void operation(String externalState) {
System.out.println("内部状态: " + intrinsicState + ", 外部状态: " + externalState);
}
}
享元工厂类
import java.util.HashMap;
import java.util.Map;
// 享元工厂类
public class FlyweightFactory {
private Map<String, Flyweight> flyweights = new HashMap<>();
public Flyweight getFlyweight(String key) {
if (!flyweights.containsKey(key)) {
flyweights.put(key, new ConcreteFlyweight(key));
}
return flyweights.get(key);
}
}
客户端
public class Main {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
// 获取共享的享元对象
Flyweight flyweight1 = factory.getFlyweight("A");
Flyweight flyweight2 = factory.getFlyweight("A"); // 共享对象
Flyweight flyweight3 = factory.getFlyweight("B");
// 操作享元对象
flyweight1.operation("First Call with A");
flyweight2.operation("Second Call with A");
flyweight3.operation("First Call with B");
// 检查是否共享
System.out.println(flyweight1 == flyweight2); // 输出: true, 表示对象是共享的
}
}
享元模式的优缺点
优点:
- 节省内存:通过共享相同的对象实例,减少了大量相似对象的内存消耗。
- 提升性能:享元模式通过减少对象的数量,提高了系统的运行性能。
- 可扩展性:当内部状态是可共享的,而外部状态是由客户端维护时,享元模式可以灵活处理大量对象。
缺点:
- 增加复杂性:享元模式要求将状态分为内部状态和外部状态,导致代码复杂度上升,尤其是在对象状态变化较多的情况下。
- 外部状态管理复杂:外部状态必须由客户端管理,可能增加开发难度。
适用场景
- 大量相似对象:在系统中存在大量相似对象的场景(如字符处理、图形渲染、缓存等),享元模式能有效减少内存消耗。
- 需要共享的对象:某些对象具有可以共享的部分状态,享元模式通过提取这些共享部分,使得系统更加高效。
- 资源受限的场合:如游戏开发中,精灵、粒子系统等图形对象需要大量复用同样的模型。
享元模式的扩展
享元模式通常与对象池结合使用,如线程池、数据库连接池等场景,都是共享资源的典型应用场景。享元模式可以看作是对象池的一种特殊形式,强调共享内部状态。
享元模式与其他模式的关系
- 与单例模式:单例模式保证一个类仅有一个实例,而享元模式则允许多个相同实例共享。
- 与工厂模式:享元模式通常通过工厂模式获取共享对象,工厂负责创建和维护享元对象的缓存。
总结
享元模式是一种通过共享相似对象来减少内存消耗的设计模式,特别适用于需要大量细粒度对象的场景。它通过将对象的状态分为可共享的内部状态和依赖环境的外部状态,减少了对象实例的重复创建。在实际应用中,享元模式广泛应用于需要优化性能和内存使用的系统中,如缓存机制、字符处理系统、图形渲染系统等。
代理模式(Proxy Pattern)
定义:代理模式为其他对象提供一种代理以控制对该对象的访问。代理模式通常用于延迟对象的加载、提供额外的访问控制,或者在实际对象不可用时提供一个替代品。
结构
- Subject(抽象主题类):定义代理类和真实主题类的公共接口,确保代理类和真实类可以互换。
- RealSubject(真实主题类):代理类所代表的实际对象,包含业务逻辑。
- Proxy(代理类):代理类包含对真实主题类的引用,并通过该引用控制对真实对象的访问,可以添加额外的功能或限制。
抽象主题类
// 抽象主题类
public interface Subject {
void request();
}
真实主题类
// 真实主题类
public class RealSubject implements Subject {
@Override
public void request() {
System.out.println("真实对象的请求处理");
}
}
代理类
// 代理类
public class Proxy implements Subject {
private RealSubject realSubject;
@Override
public void request() {
// 在这里可以添加额外的操作,如权限控制、日志记录等
if (realSubject == null) {
realSubject = new RealSubject(); // 延迟加载
}
System.out.println("代理对象处理一些前置任务");
realSubject.request(); // 调用真实对象的方法
System.out.println("代理对象处理一些后置任务");
}
}
客户端
public class Main {
public static void main(String[] args) {
// 使用代理对象
Subject proxy = new Proxy();
proxy.request();
}
}
代理模式的类型
- 静态代理:代理类在编译时确定,代理类需要实现与真实类相同的接口。
- 动态代理:代理类在运行时生成,通常通过反射机制在运行时处理接口的实现。
- 虚拟代理:用于对象的懒加载,只有在需要时才创建对象。
- 保护代理:控制对象的访问权限,确保不同的用户角色有不同的访问权限。
- 远程代理:为位于不同地址空间的对象提供本地代表(用于分布式系统中)。
代理模式的优缺点
优点:
- 控制访问:代理模式可以控制对目标对象的访问,可以通过代理类进行权限控制、日志记录等操作。
- 延迟加载:可以在需要时才创建真实对象,减少不必要的开销,优化资源使用。
- 增强功能:代理模式可以在不修改真实类的情况下增强类的功能,比如添加缓存、日志或安全检查。
缺点:
- 增加复杂性:代理模式会引入额外的对象层次,增加系统的复杂性,特别是在过度使用时可能导致维护困难。
- 性能开销:虽然代理模式可以延迟对象的加载,但每次调用都需要经过代理类,可能引入一定的性能开销。
适用场景
- 远程代理:在分布式系统中,代理对象代表位于不同地址空间的远程对象,如RPC(远程过程调用)。
- 虚拟代理:用于延迟加载对象,只有在需要时才真正创建对象,如懒加载。
- 保护代理:在需要权限控制或访问控制的场景中,代理对象可以限制客户端的访问权限。
- 智能引用:代理对象可以在访问目标对象时加入额外的行为,如引用计数、日志记录等。
扩展
1. 动态代理
动态代理通过反射机制在运行时创建代理对象。Java 提供了java.lang.reflect.Proxy类,允许在运行时动态创建代理类。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 动态代理处理器
public class DynamicProxyHandler implements InvocationHandler {
private Object realObject;
public DynamicProxyHandler(Object realObject) {
this.realObject = realObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("动态代理: 前置处理");
Object result = method.invoke(realObject, args);
System.out.println("动态代理: 后置处理");
return result;
}
}
// 客户端使用动态代理
public class Main {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
Subject proxyInstance = (Subject) Proxy.newProxyInstance(
realSubject.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(),
new DynamicProxyHandler(realSubject)
);
proxyInstance.request();
}
}
2. 静态代理与动态代理的对比
- 静态代理:代理类在编译时生成,必须手动为每个具体类编写代理类。优点是可以定制化每个代理类的行为,但缺点是需要为每个类编写代码,增加了代码冗余。
- 动态代理:代理类在运行时动态生成,减少了编写代理类的代码量,灵活性较强,尤其适合场景变化较多的情况。
总结
代理模式是一种常用的设计模式,它通过引入代理对象控制对目标对象的访问,可以用于延迟加载、权限控制、日志记录等场景。代理模式的主要优势在于可以在不改变目标对象的前提下增强或控制其行为,但也会增加系统的复杂性。代理模式在实际开发中被广泛应用,如远程代理、虚拟代理、保护代理等,尤其在分布式系统、资源受限系统中表现尤为突出。
装饰器模式与代理模式的区别
- 装饰器模式:用于动态为对象增加功能,强调对功能的扩展。
- 代理模式:用于控制对对象的访问,强调对对象的控制和保护,常用于延迟加载、安全检查等场景。
行为型模式(12种)
行为型模式关注对象之间的责任分配,处理对象间的交互和职责划分。
策略模式(Strategy Pattern)
定义:策略模式将算法的定义和使用分开,允许在运行时动态选择或替换算法,使得不同的算法可以独立于使用它的客户端进行变化。
结构
- Context(上下文类):维护一个对 Strategy 对象的引用。客户端通过 Context 使用策略。
- Strategy(策略接口):定义了算法的公共接口,具体策略类会实现这个接口。
- ConcreteStrategy(具体策略类):实现 Strategy 接口,提供具体的算法实现。
策略接口
// 策略接口
public interface Strategy {
int doOperation(int num1, int num2);
}
具体策略类
// 具体策略类:加法策略
public class OperationAdd implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
// 具体策略类:减法策略
public class OperationSubtract implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
// 具体策略类:乘法策略
public class OperationMultiply implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
上下文类
// 上下文类,持有策略的引用
public class Context {
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2) {
return strategy.doOperation(num1, num2);
}
}
客户端
public class Main {
public static void main(String[] args) {
// 使用加法策略
Context context = new Context(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
// 使用减法策略
context = new Context(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
// 使用乘法策略
context = new Context(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
}
策略模式的优缺点
优点:
- 遵循开闭原则:可以在不修改现有代码的情况下添加新的策略,实现算法的动态扩展。
- 消除条件语句:通过将算法封装到独立的策略类中,避免了大量的条件语句。
- 提高代码的可读性和可维护性:每个策略类只负责一种算法,职责单一,代码清晰。
- 可互换的策略:不同的策略类可以动态切换,提升了系统的灵活性。
缺点:
- 增加了对象数量:每个策略都是一个单独的类,可能导致类的数量增多,增加了系统的复杂性。
- 客户端必须了解策略:客户端需要知道每个策略的区别,并根据场景选择合适的策略。
适用场景
- 多种算法:在某些场景下存在多个算法可以解决同一个问题,比如排序算法、支付方式等,策略模式可以帮助动态选择合适的算法。
- 算法易变:如果算法经常发生变化,策略模式可以使变化更加灵活,减少对使用场景的影响。
- 避免条件判断:当代码中有大量的 if-else 或 switch-case 语句选择算法时,策略模式是很好的解决方案。
策略模式的扩展
策略模式可以结合其他模式使用,特别是与工厂模式结合。工厂模式可以用来创建具体的策略对象,从而实现策略的动态选择。
示例:结合工厂模式
// 策略工厂类,用于动态创建策略
public class StrategyFactory {
public static Strategy getStrategy(String type) {
switch (type) {
case "add":
return new OperationAdd();
case "subtract":
return new OperationSubtract();
case "multiply":
return new OperationMultiply();
default:
throw new IllegalArgumentException("Unknown strategy type");
}
}
}
客户端
public class Main {
public static void main(String[] args) {
// 通过工厂模式动态获取策略
Context context = new Context(StrategyFactory.getStrategy("add"));
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context = new Context(StrategyFactory.getStrategy("subtract"));
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
context = new Context(StrategyFactory.getStrategy("multiply"));
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
}
策略模式与其他模式的关系
- 策略模式 vs 状态模式:策略模式的主要目的是让算法可以互换,而状态模式的目的是根据对象的状态来改变行为。两者的实现结构类似,但意图不同。
- 策略模式 vs 模板方法模式:模板方法模式通过继承来决定算法的具体实现,而策略模式通过组合来实现算法的动态选择,策略模式比模板方法模式更灵活。
- 策略模式 vs 装饰器模式:装饰器模式用于动态增强对象的功能,而策略模式用于动态选择算法。
总结
策略模式通过将算法封装到不同的策略类中,允许算法在运行时动态切换,消除了条件语句的使用,符合开闭原则。在实际应用中,策略模式被广泛用于解决具有多种算法选择或动态变化需求的场景,如支付系统、折扣策略、排序算法等。策略模式的灵活性使得系统能够轻松应对算法的变化和扩展。
模板方法模式(Template Method Pattern)
定义:模板方法模式是一种通过定义一个抽象方法或钩子方法,让子类去实现这些具体细节,确保算法的框架不变。这样做的好处是提高代码复用性,子类可以通过复写某些方法实现不同的行为。
结构
- AbstractClass(抽象类):定义了算法的骨架,包含模板方法以及具体步骤或抽象步骤的定义。
- ConcreteClass(具体类):实现了抽象类中的抽象步骤,或者覆写钩子方法。
抽象类定义算法骨架
// 抽象类,定义了算法的骨架
public abstract class Game {
// 模板方法,定义算法的步骤,final 以防止子类改变整体流程
public final void play() {
initialize();
startPlay();
endPlay();
}
// 抽象方法,子类实现这些具体步骤
protected abstract void initialize();
protected abstract void startPlay();
protected abstract void endPlay();
}
具体类实现算法的某些步骤
// 具体类1:实现具体步骤
public class Football extends Game {
@Override
protected void initialize() {
System.out.println("足球游戏初始化");
}
@Override
protected void startPlay() {
System.out.println("足球游戏开始");
}
@Override
protected void endPlay() {
System.out.println("足球游戏结束");
}
}
// 具体类2:实现不同的步骤
public class Basketball extends Game {
@Override
protected void initialize() {
System.out.println("篮球游戏初始化");
}
@Override
protected void startPlay() {
System.out.println("篮球游戏开始");
}
@Override
protected void endPlay() {
System.out.println("篮球游戏结束");
}
}
客户端
public class Main {
public static void main(String[] args) {
Game footballGame = new Football();
footballGame.play(); // 执行足球游戏的模板方法
System.out.println();
Game basketballGame = new Basketball();
basketballGame.play(); // 执行篮球游戏的模板方法
}
}
模板方法模式的关键点
- 模板方法:模板方法是算法的骨架,它是一个具体方法,并且通常声明为 final,以防止子类修改算法的顺序。
- 具体步骤方法:一些步骤在模板方法中已经实现,它们在父类中定义,可以由所有子类共享。
- 抽象步骤方法:一些步骤被声明为抽象方法,强制子类去实现具体的逻辑。
- 钩子方法:钩子方法是子类可以选择性地覆盖的方法,它在父类中提供默认实现,子类可以根据需要进行扩展。
优缺点
优点:
- 代码复用:将不变的部分实现放在父类中,而将可变的部分交给子类,实现了代码的复用。
- 灵活性强:子类可以通过扩展具体步骤或者钩子方法来实现不同的行为,增强了灵活性。
- 符合开闭原则:可以通过添加新的子类来扩展新的行为,而不需要修改已有的代码。
缺点:
- 继承带来的局限:子类必须继承抽象类,使用继承机制会限制类的灵活性,过多的层次继承可能导致复杂性增加。
- 子类实现增加:每个子类都需要实现父类中定义的抽象方法,当子类较多时,可能导致代码臃肿。
适用场景
- 框架搭建:当构建一个框架时,可以使用模板方法模式将通用的流程定义在父类中,允许框架的使用者根据需要扩展具体的行为。
- 多种操作流程的变体:比如游戏中的初始化、开始、结束操作相似,但细节有所不同时,可以使用模板方法模式。
- 复用公共逻辑:如果多个类的行为大致相同,只有少量步骤不同,模板方法可以有效地将相同逻辑复用。
钩子方法
钩子方法是模板方法模式中的一个重要概念。它允许子类有机会在算法的关键步骤中加入或修改行为,而不必强制子类实现这些方法。钩子方法在父类中提供一个默认的空实现,子类可以根据需要选择性地重写钩子方法。
public abstract class Game {
public final void play() {
initialize();
startPlay();
if (isHalfTime()) { // 钩子方法
halfTime();
}
endPlay();
}
protected abstract void initialize();
protected abstract void startPlay();
protected abstract void endPlay();
// 钩子方法,子类可覆盖
protected boolean isHalfTime() {
return false;
}
// 默认空实现,子类可选择性实现
protected void halfTime() {
System.out.println("中场休息");
}
}
客户端
public class Football extends Game {
@Override
protected void initialize() {
System.out.println("足球游戏初始化");
}
@Override
protected void startPlay() {
System.out.println("足球游戏开始");
}
@Override
protected void endPlay() {
System.out.println("足球游戏结束");
}
@Override
protected boolean isHalfTime() {
return true; // 足球比赛有中场休息
}
@Override
protected void halfTime() {
System.out.println("足球比赛中场休息");
}
}
public class Main {
public static void main(String[] args) {
Game footballGame = new Football();
footballGame.play(); // 调用带有钩子方法的模板方法
}
}
抽象类定义算法骨架模板方法模式的扩展
模板方法模式可以和其他模式结合使用,如:
- 工厂方法模式:可以在模板方法中调用工厂方法,以动态创建某些步骤中的对象。
- 策略模式:在模板方法模式中,可以通过策略模式动态选择某些步骤的实现方式。
总结
模板方法模式是一种有效的代码复用和算法控制手段,通过在父类中定义算法的骨架,子类可以灵活地实现或修改算法的某些步骤。它可以减少代码的重复,实现逻辑的复用,同时允许不同的子类对算法的部分步骤进行定制或扩展。模板方法模式非常适合处理具有固定流程但细节可能不同的场景,比如游戏流程、数据处理流程等。
观察者模式(Observer Pattern)
定义:观察者模式允许对象之间建立一种松耦合的关系。被观察的对象称为主题(Subject),它维护一个观察者(Observer)列表。当主题的状态发生变化时,主题会通知所有注册的观察者进行相应的更新。
角色
- Subject(主题):被观察者,维护观察者列表,并提供注册、取消注册和通知的功能。
- Observer(观察者):订阅主题的对象,一旦主题的状态发生改变,观察者会接收到通知并执行更新操作。
- ConcreteSubject(具体主题):主题的具体实现,维护观察者并在状态改变时调用通知方法。
- ConcreteObserver(具体观察者):观察者的具体实现,定义如何在收到通知时做出响应。
定义观察者接口
// 观察者接口,定义更新操作
public interface Observer {
void update(String message);
}
定义主题接口
// 主题接口,定义注册、移除观察者以及通知操作
public interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers();
}
具体主题类
import java.util.ArrayList;
import java.util.List;
// 具体主题类
public class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
private String message; // 被观察的状态
@Override
public void attach(Observer observer) {
observers.add(observer);
}
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(message);
}
}
// 更新状态并通知所有观察者
public void setMessage(String message) {
this.message = message;
notifyObservers();
}
}
具体观察者类
// 具体观察者类,实现更新逻辑
public class ConcreteObserver implements Observer {
private String name;
public ConcreteObserver(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + " 收到消息: " + message);
}
}
客户端
public class Main {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
// 创建两个观察者
Observer observer1 = new ConcreteObserver("Observer 1");
Observer observer2 = new ConcreteObserver("Observer 2");
// 注册观察者
subject.attach(observer1);
subject.attach(observer2);
// 更新状态并通知观察者
subject.setMessage("Hello Observers!");
// 移除一个观察者
subject.detach(observer1);
// 更新状态并通知剩下的观察者
subject.setMessage("Another update!");
}
}
观察者模式的关键点
- 注册与取消注册:观察者可以动态地注册或取消注册,控制是否接收主题的通知。
- 松耦合:观察者与主题之间是松耦合的,主题并不关心观察者的具体实现,只通过抽象接口通知。
- 通知机制:一旦主题的状态发生改变,它会自动通知所有已注册的观察者,使得观察者能够响应变化。
优缺点
优点:
- 松耦合:观察者和主题之间是松耦合的,它们彼此不知道对方的具体实现,符合开闭原则。
- 动态联动:观察者可以随时注册或取消注册,允许在运行时动态修改依赖关系。
- 可扩展性强:可以轻松地添加新的观察者而不需要修改现有的代码。
缺点:
- 过度通知:如果观察者数量过多,频繁的状态更新可能导致系统开销增加。
- 通知顺序:由于观察者的通知顺序不确定,可能会导致某些业务场景中的执行顺序问题。
- 内存泄漏:如果观察者没有正确移除注册,可能会导致内存泄漏。
使用场景
- 事件驱动系统:在事件驱动架构中,观察者模式广泛用于通知事件监听器处理事件,比如GUI编程中的按钮点击、网络编程中的消息传递。
- 模型-视图联动:在MVC架构中,观察者模式常用于视图与模型的同步更新,模型发生变化时,视图能够自动更新。
- 消息广播系统:可以将观察者模式用于消息发布订阅系统,多个订阅者同时接收一个发布者的消息。
与发布-订阅模式的区别
观察者模式和发布-订阅模式的核心思想类似,但在实现上有所不同:
- 直接通知:观察者模式是主题直接通知观察者,观察者主动接收通知;而发布-订阅模式通常通过一个中间件(如消息队列)来解耦发布者和订阅者。
- 灵活性:发布-订阅模式更灵活,因为它允许多个系统通过中间代理进行通信,而观察者模式更适合单个应用程序内部的对象间联动。
扩展
观察者模式可以与其他模式结合使用:
- 装饰器模式:可以使用装饰器来增强某些观察者的功能。
- 策略模式:可以为不同的观察者提供不同的策略,以动态改变观察者的行为。
- 单例模式:如果某个主题是系统全局唯一的,可以结合单例模式实现。
总结
观察者模式提供了一种非常灵活的机制来实现对象之间的联动关系,尤其适合在状态变化需要通知其他对象时使用。通过定义主题和观察者的抽象接口,观察者模式使得系统更具扩展性,同时保持了松耦合的特性。
迭代器模式(Iterator Pattern)
定义:迭代器模式定义了一个迭代器接口,这个接口包含了遍历集合元素所需的方法。具体集合对象提供实现了该接口的迭代器,这样就可以通过迭代器遍历集合中的元素,而无需知道集合的底层实现方式。
角色
- Iterator(迭代器接口):定义访问和遍历元素的方法,例如hasNext()和next()。
- ConcreteIterator(具体迭代器):实现迭代器接口,负责遍历集合中的元素。
- Aggregate(聚合接口):定义创建迭代器的方法,例如createIterator()。
- ConcreteAggregate(具体聚合类):实现聚合接口,创建具体的迭代器对象。
定义迭代器接口
// 迭代器接口,定义遍历方法
public interface Iterator {
boolean hasNext();
Object next();
}
定义聚合接口
// 聚合接口,定义创建迭代器的方法
public interface Aggregate {
Iterator createIterator();
}
具体迭代器类
// 具体迭代器类,遍历聚合对象
public class ConcreteIterator implements Iterator {
private String[] items;
private int position = 0;
public ConcreteIterator(String[] items) {
this.items = items;
}
@Override
public boolean hasNext() {
return position < items.length;
}
@Override
public Object next() {
if (hasNext()) {
return items[position++];
}
return null;
}
}
具体聚合类
// 具体聚合类,实现创建迭代器的方法
public class ConcreteAggregate implements Aggregate {
private String[] items;
public ConcreteAggregate(String[] items) {
this.items = items;
}
@Override
public Iterator createIterator() {
return new ConcreteIterator(items);
}
}
客户端
public class Main {
public static void main(String[] args) {
String[] items = {"Item 1", "Item 2", "Item 3", "Item 4"};
Aggregate aggregate = new ConcreteAggregate(items);
Iterator iterator = aggregate.createIterator();
// 使用迭代器遍历元素
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
迭代器模式的关键点
- 封装迭代逻辑:迭代器封装了遍历的逻辑,客户端通过迭代器接口与集合对象交互,而不需要关心集合的内部结构。
- 统一接口:无论集合类型如何(数组、列表等),通过迭代器接口,客户端可以以相同的方式遍历集合对象中的元素。
- 多样化迭代:不同的集合可以有不同的迭代器实现,支持不同的遍历方式(顺序遍历、反向遍历等)。
优缺点
优点:
- 简化客户端代码:客户端不需要直接与集合的内部结构打交道,只需要通过迭代器接口访问集合中的元素,简化了代码。
- 统一遍历方式:通过统一的接口遍历不同的集合对象,方便扩展和维护。
- 支持不同遍历方式:可以根据需要自定义不同的迭代器,实现不同的遍历方式。
缺点:
- 开销增加:对于较大的集合,创建迭代器可能会有一定的开销,尤其是在需要频繁创建迭代器的情况下。
- 对集合对象的修改不敏感:如果在迭代的过程中,集合对象发生了修改,可能会导致迭代器失效或产生不一致的结果。
使用场景
- 遍历集合:当需要遍历复杂的数据结构(如树、图、链表等)时,迭代器模式可以提供统一的访问接口。
- 对集合对象解耦:当集合的内部结构复杂、但需要对外提供简洁的遍历方式时,迭代器模式能很好地将内部结构与客户端解耦。
- 需要多种遍历方式:当同一个集合可能需要不同的遍历方式(例如正序遍历、倒序遍历、跳跃遍历等),可以通过不同的迭代器实现这些遍历。
扩展
Java 内置迭代器:Java 集合框架已经实现了迭代器模式,所有的集合类都提供了Iterator接口的实现。例如,ArrayList、HashSet、HashMap等类都可以通过iterator()方法返回一个迭代器。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
多种迭代方式:可以通过自定义多个具体迭代器来实现不同的遍历策略。例如,正序遍历、倒序遍历等。
总结
迭代器模式为集合对象的遍历提供了一种统一的接口,极大地简化了遍历逻辑,并且将遍历操作与集合的具体实现分离。通过迭代器,集合的内部结构对客户端透明,既增强了系统的灵活性,又使得代码更易于维护。
责任链模式(Chain of Responsibility Pattern)
定义:责任链模式的核心思想是将请求的处理者串联起来,形成一条链式的结构,依次传递请求。每个处理者都包含对下一个处理者的引用,当一个处理者无法处理请求时,传递给下一个处理者,直到找到能够处理请求的对象为止。
角色
- Handler(处理者):定义处理请求的接口,并包含一个对下一个处理者的引用。
- ConcreteHandler(具体处理者):继承处理者接口,负责处理它所擅长的请求。如果不能处理,则将请求传递给下一个处理者。
- Client(客户端):创建处理链,并将请求发送给第一个处理者。
定义处理者接口
// 处理者接口,定义处理请求的抽象方法
public abstract class Handler {
protected Handler nextHandler;
// 设置下一个处理者
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
// 处理请求的抽象方法
public abstract void handleRequest(String request);
}
具体处理者
// 具体处理者1:处理特定类型的请求
public class ConcreteHandler1 extends Handler {
@Override
public void handleRequest(String request) {
if (request.equals("Request1")) {
System.out.println("ConcreteHandler1 处理了 " + request);
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
// 具体处理者2:处理其他类型的请求
public class ConcreteHandler2 extends Handler {
@Override
public void handleRequest(String request) {
if (request.equals("Request2")) {
System.out.println("ConcreteHandler2 处理了 " + request);
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
}
}
}
客户端
public class Main {
public static void main(String[] args) {
// 创建处理者
Handler handler1 = new ConcreteHandler1();
Handler handler2 = new ConcreteHandler2();
// 设置责任链
handler1.setNextHandler(handler2);
// 发送请求
handler1.handleRequest("Request1");
handler1.handleRequest("Request2");
handler1.handleRequest("UnknownRequest");
}
}
责任链模式的关键点
- 请求传递:请求沿着链条依次传递,直到某个处理者处理该请求,或者到达链条的末端。如果没有处理者能处理请求,通常可以设计默认的处理机制。
- 解耦请求的发送者和处理者:责任链模式将请求的发送者和实际处理者解耦,发送者无需知道是哪一个处理者处理了请求,增加了系统的灵活性。
- 灵活的扩展:可以动态地增加或修改处理者的链条结构,符合“开闭原则”(OCP:对扩展开放,对修改封闭)。
优缺点
优点:
- 降低耦合:发送者不需要明确指定接收者,多个对象可以协作处理请求。
- 动态组合处理者:处理者链可以根据需要动态地组织和重组,灵活性较高。
- 符合职责分离原则:每个处理者只关心自己能处理的请求,其余的交给链条上的下一个处理者。
缺点:
- 请求处理不保证:如果链条末端没有处理请求的能力,可能会导致请求被忽略或处理不完整。
- 调试困难:由于请求的传递是链式的,可能会增加调试和排错的难度,尤其是链条很长时。
适用场景
- 多种请求处理:当系统中有多种请求需要处理,而请求的处理者可能不唯一时,适合使用责任链模式。
- 动态组织请求处理者:当处理者的顺序或种类需要灵活变化时,可以使用责任链模式。
- 权限校验、日志记录等:常见的场景包括权限校验、日志记录等功能,这些操作通常需要串联处理。
实际应用
- Java 过滤器链:Servlet 的过滤器(Filter)机制就使用了责任链模式。多个过滤器按照链式结构依次处理请求。
- 异常处理机制:在一些编程语言中,异常处理也可以使用责任链模式。多个异常处理器可以链式处理异常,直到找到匹配的异常处理器。
扩展
- 双向链表:在某些场景下,可以扩展为双向责任链,使得请求可以向前或向后传递。
- 组合模式与责任链模式结合:有时责任链中的某些节点本身也可以是一个组合结构,例如某个处理者可以包含一组子处理者,它们内部形成一个小责任链。
总结
责任链模式通过将请求处理者串联起来,降低了发送者与接收者之间的耦合度,并且可以动态地扩展和修改处理链。它特别适合用于需要多个对象处理请求的场景,增强了系统的灵活性和可扩展性。然而,由于链式结构,调试和维护可能变得稍微复杂。
命令模式(Command Pattern)
定义:命令模式的核心是将一个动作或操作封装为一个对象。这个对象包含了执行该操作所需的信息,包括操作的接收者和执行的参数。通过这种方式,命令对象可以被传递、存储和执行。
角色
- Command(命令接口):定义一个执行操作的接口。
- ConcreteCommand(具体命令):实现命令接口,定义与接收者之间的绑定,调用接收者相应的操作。
- Receiver(接收者):具体的业务逻辑类,负责实际执行命令。
- Invoker(调用者):持有命令对象并调用其执行方法。
- Client(客户端):创建具体命令对象并设置其接收者。
定义命令接口
// 命令接口,定义执行操作的方法
public interface Command {
void execute();
}
具体命令类
// 具体命令类,封装请求
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
}
接收者
// 接收者类,执行具体操作
public class Light {
public void turnOn() {
System.out.println("Light is ON");
}
public void turnOff() {
System.out.println("Light is OFF");
}
}
调用者
// 调用者类,持有命令对象并调用执行方法
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
客户端
public class Main {
public static void main(String[] args) {
Light light = new Light();
Command lightOn = new LightOnCommand(light);
Command lightOff = new LightOffCommand(light);
RemoteControl remote = new RemoteControl();
// 开灯
remote.setCommand(lightOn);
remote.pressButton();
// 关灯
remote.setCommand(lightOff);
remote.pressButton();
}
}
命令模式的关键点
- 请求封装:通过将请求封装为命令对象,可以灵活地处理请求,包括队列、记录和撤销等操作。
- 解耦:请求的发送者与接收者之间解耦,发送者只需要知道命令对象,而无需知道具体的接收者。
- 扩展性:可以通过添加新的命令类来扩展功能,无需修改现有代码,符合开闭原则。
优缺点
优点:
- 解耦请求者和接收者:请求者无需了解请求的具体处理过程,只需调用命令对象。
- 支持撤销和重做:可以通过保存命令对象的状态,实现命令的撤销和重做。
- 可扩展性强:添加新命令时,无需更改现有代码,符合开闭原则。
缺点:
- 命令对象增多:随着命令的增加,可能导致命令对象数量激增,增加系统的复杂性。
- 对命令的管理:需要对命令对象进行有效管理,否则可能导致混乱。
适用场景
- 需要支持撤销/重做操作:例如在图形编辑器中,用户可以撤销或重做上一步操作。
- 需要记录操作日志:可以将命令对象存储到日志中,以便后续查看。
- 构建简单的宏命令:将多个命令组合成一个命令,通过调用单个命令执行一系列操作。
实际应用
- 图形用户界面(GUI):例如按钮点击事件、菜单项选择等都可以使用命令模式实现。
- 事务管理:可以通过命令模式实现对事务的管理,确保数据的一致性。
- 远程控制:在智能家居系统中,通过命令模式实现远程控制设备的开关。
总结
命令模式通过将请求封装为对象,解耦请求的发送者与接收者,增强了系统的灵活性和可扩展性。适用于需要记录、撤销或重做操作的场景,能够提高代码的可维护性与可读性。尽管可能导致命令类数量增加,但其带来的灵活性和扩展性是非常值得的。
备忘录模式(Memento Pattern)
定义:备忘录模式的核心在于提供一个可以保存对象状态的备忘录对象,以及一个可以恢复对象状态的发起人。发起人可以在不违反封装原则的情况下,将其状态保存到备忘录中。
角色
- Originator(发起人):创建一个备忘录,记录当前状态,并可以根据备忘录恢复状态。
- Memento(备忘录):保存发起人的内部状态,可以被发起人和外部对象访问。
- Caretaker(管理者):管理备忘录对象,负责保存和恢复备忘录,但不允许访问备忘录的内容。
定义备忘录类
// 备忘录类,保存发起人的状态
public class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
定义发起人类
// 发起人类,负责创建和恢复备忘录
public class Originator {
private String state;
public void setState(String state) {
this.state = state;
}
public String getState() {
return state;
}
// 创建备忘录
public Memento createMemento() {
return new Memento(state);
}
// 从备忘录恢复状态
public void restore(Memento memento) {
this.state = memento.getState();
}
}
定义管理者类
// 管理者类,负责备忘录的保存和恢复
import java.util.ArrayList;
import java.util.List;
public class Caretaker {
private List<Memento> mementoList = new ArrayList<>();
public void save(Originator originator) {
mementoList.add(originator.createMemento());
}
public void restore(Originator originator, int index) {
if (index < mementoList.size()) {
originator.restore(mementoList.get(index));
}
}
}
客户端
public class Main {
public static void main(String[] args) {
Originator originator = new Originator();
Caretaker caretaker = new Caretaker();
originator.setState("State 1");
caretaker.save(originator);
originator.setState("State 2");
caretaker.save(originator);
originator.setState("State 3");
System.out.println("Current State: " + originator.getState());
// 恢复到之前的状态
caretaker.restore(originator, 0);
System.out.println("Restored State: " + originator.getState());
caretaker.restore(originator, 1);
System.out.println("Restored State: " + originator.getState());
}
}
备忘录模式的关键点
- 封装性:备忘录模式提供了对内部状态的封装,外部对象不能直接访问发起人的内部状态。
- 状态恢复:可以在需要的时候方便地恢复到某个之前的状态,支持历史版本的管理。
- 管理者的角色:管理者负责管理多个备忘录,能够灵活地保存和恢复状态。
优缺点
优点:
- 保护内部状态:不暴露发起人的内部状态,维护了对象的封装性。
- 简化恢复操作:提供简单的接口来恢复对象的状态,方便使用。
- 灵活性:可以灵活地保存和恢复多个状态,适合需要历史记录的场景。
缺点:
- 内存消耗:如果对象状态较复杂且变化频繁,可能会消耗大量内存,存储多个备忘录。
- 备忘录管理:需要有效管理备忘录的生命周期,以防止内存泄漏。
适用场景
- 需要实现撤销操作的应用:如文本编辑器、图形编辑器等,需要保存历史状态以便用户撤销操作。
- 版本控制:管理对象的多个版本,支持状态恢复和比较。
- 复杂状态的保存:在游戏、模拟器等复杂状态管理中,备忘录模式能够方便地保存和恢复状态。
实际应用
- 图形编辑器:在图形编辑软件中,用户的操作可以被记录,用户可以随时撤销或恢复到某个状态。
- 文本编辑器:文本编辑器可以在用户编辑文本时,记录每个版本,以便用户随时回退。
- 游戏状态:游戏中的玩家状态可以被保存,以便在游戏中断后恢复。
总结
备忘录模式通过提供一个保存和恢复对象状态的机制,增强了系统的灵活性和可维护性。它适用于需要历史记录和撤销操作的场景,能够有效地管理对象状态,同时保持对象的封装性。尽管可能带来内存消耗的问题,但其带来的便利性通常是值得的。
状态模式(State Pattern)
定义:状态模式的核心思想是将对象的状态封装成独立的类,并通过状态对象来控制对象的行为。每个状态类实现了相同的接口,代表对象在该状态下的行为。
角色
- Context(上下文):维护一个对具体状态对象的引用,并在需要时改变状态。
- State(状态接口):定义一个接口,表示状态所应具备的行为。
- ConcreteState(具体状态类):实现状态接口,定义在该状态下的具体行为。
定义状态接口
// 状态接口,定义状态的行为
public interface State {
void handle();
}
具体状态类
// 具体状态类A
public class ConcreteStateA implements State {
@Override
public void handle() {
System.out.println("Handling request in State A");
}
}
// 具体状态类B
public class ConcreteStateB implements State {
@Override
public void handle() {
System.out.println("Handling request in State B");
}
}
上下文类
// 上下文类,维护当前状态
public class Context {
private State state;
public void setState(State state) {
this.state = state;
}
public void request() {
state.handle();
}
}
客户端
public class Main {
public static void main(String[] args) {
Context context = new Context();
// 设置状态为A并执行请求
context.setState(new ConcreteStateA());
context.request();
// 改变状态为B并执行请求
context.setState(new ConcreteStateB());
context.request();
}
}
状态模式的关键点
- 状态封装:每个状态的行为被封装在独立的状态类中,减少了条件语句的使用,使代码更清晰。
- 状态切换:上下文可以根据需要动态改变状态,状态的改变不影响上下文的其他功能。
- 行为动态变化:对象的行为可以随其状态的变化而变化,符合“开放/封闭原则”。
优缺点
优点:
- 提高可维护性:通过将状态行为封装在独立类中,减少了条件判断,提高了代码的可读性和可维护性。
- 易于扩展:添加新状态时,只需新增状态类,而无需修改现有代码,符合开闭原则。
- 明确的状态管理:状态与行为的对应关系明确,使得状态管理更加清晰。
缺点:
- 类的数量增加:每个状态都需要创建一个类,可能导致类的数量激增,增加系统的复杂性。
- 状态切换复杂:如果状态之间有复杂的切换逻辑,可能会使得状态管理变得复杂。
适用场景
- 对象的行为依赖于状态:当一个对象的行为受到其内部状态的影响,并且可能会在运行时发生变化时,适合使用状态模式。
- 状态转换较多的系统:如游戏角色、工作流程、文档编辑等场景,需要在多种状态之间切换。
- 复杂状态管理:在需要管理多个状态和状态之间的转换逻辑时,状态模式能够简化代码结构。
实际应用
- 游戏开发:在游戏中,角色可能有不同的状态(如“行走”、“跳跃”、“攻击”等),状态模式可以用来管理这些状态及其对应行为。
- 工作流管理:在工作流系统中,不同的状态(如“待审核”、“审核通过”、“已完成”)对应不同的处理逻辑。
- UI组件:在用户界面中,按钮、输入框等组件的状态(如“正常”、“悬停”、“点击”)可以通过状态模式来管理。
总结
状态模式通过将对象的状态和行为封装成独立的类,增强了系统的灵活性和可扩展性。适用于需要管理复杂状态的场景,能够有效地减少条件判断,使代码更加清晰、可维护。尽管可能导致类的数量增加,但其带来的管理便利性通常是值得的。
访问者模式(Visitor Pattern)
定义:访问者模式的核心是将操作与对象结构分离。访问者通过访问对象的方法来实现特定的功能。这样,如果要添加新功能,只需添加新的访问者类,而无需修改已有的对象结构。
角色
- Visitor(访问者接口):定义了访问每种元素的方法。
- ConcreteVisitor(具体访问者):实现访问者接口,定义对每个元素的具体操作。
- Element(元素接口):定义接受访问者的方法。
- ConcreteElement(具体元素类):实现元素接口,定义可以被访问的对象。
- ObjectStructure(对象结构):管理元素的集合,提供接受访问者的方法。
定义访问者接口
// 访问者接口
public interface Visitor {
void visit(ConcreteElementA element);
void visit(ConcreteElementB element);
}
具体访问者类
// 具体访问者
public class ConcreteVisitor implements Visitor {
@Override
public void visit(ConcreteElementA element) {
System.out.println("Visiting Element A");
}
@Override
public void visit(ConcreteElementB element) {
System.out.println("Visiting Element B");
}
}
定义元素接口
// 元素接口
public interface Element {
void accept(Visitor visitor);
}
具体元素类
// 具体元素类A
public class ConcreteElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
// 具体元素类B
public class ConcreteElementB implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
对象结构类
import java.util.ArrayList;
import java.util.List;
// 对象结构类
public class ObjectStructure {
private List<Element> elements = new ArrayList<>();
public void attach(Element element) {
elements.add(element);
}
public void accept(Visitor visitor) {
for (Element element : elements) {
element.accept(visitor);
}
}
}
客户端
public class Main {
public static void main(String[] args) {
ObjectStructure structure = new ObjectStructure();
structure.attach(new ConcreteElementA());
structure.attach(new ConcreteElementB());
Visitor visitor = new ConcreteVisitor();
structure.accept(visitor);
}
}
访问者模式的关键点
- 分离操作和数据结构:通过访问者模式,将操作和数据结构分开,使得可以灵活地添加新操作。
- 扩展性:添加新操作时,只需增加一个新的访问者类,无需更改现有元素类。
- 增强代码可读性:将与特定操作相关的代码集中在访问者中,使得代码更加清晰。
优缺点
优点:
- 增加新的操作容易:可以在不修改现有元素类的情况下,轻松增加新的操作。
- 集中操作:相关操作可以集中在访问者中,增强代码的可读性和可维护性。
- 支持复合结构:可以对复杂对象结构中的元素进行统一操作。
缺点:
- 违反封装:访问者需要了解元素的内部结构,可能会破坏封装性。
- 增加新元素难度:添加新元素时,需要在每个访问者中增加相应的方法,增加了维护成本。
适用场景
- 对象结构稳定但操作频繁变动:当对象结构相对稳定,但需要频繁添加新的操作时,适合使用访问者模式。
- 复杂对象结构的操作:在需要对复杂的对象结构进行多种操作时,可以使用访问者模式来简化管理。
- 需要在不同对象上执行操作:如编译器中对抽象语法树的遍历和处理等。
实际应用
- 编译器:编译器中的语法树遍历,访问不同节点进行不同的操作(如语法分析、语义分析等)。
- 图形程序:在图形界面应用中,可以实现不同图形元素的操作,如绘制、打印等。
- 数据结构操作:在一些数据结构中,需要对元素进行多种操作时,使用访问者模式可以简化操作流程。
总结
访问者模式通过将操作与对象结构分离,增强了系统的灵活性和可扩展性。适用于对象结构相对稳定而操作频繁变动的场景,有助于提高代码的可读性和可维护性。尽管可能违反封装性和增加新元素的难度,但其带来的便利性通常是值得的。
中介者模式(Mediator Pattern)
定义:中介者模式的核心思想是将对象间的复杂关系封装在中介者中,让对象不直接通信,而通过中介者进行交互。这使得对象之间的依赖关系降低,便于扩展和维护。
角色
- Mediator(中介者接口):定义与各个同事对象交互的方法。
- ConcreteMediator(具体中介者):实现中介者接口,协调各个同事对象的交互。
- Colleague(同事接口):定义同事对象的接口,包含与中介者的引用。
- ConcreteColleague(具体同事类):实现同事接口,利用中介者进行交互。
定义中介者接口
// 中介者接口
public interface Mediator {
void notify(Colleague colleague, String message);
}
具体中介者类
// 具体中介者
public class ConcreteMediator implements Mediator {
private ConcreteColleagueA colleagueA;
private ConcreteColleagueB colleagueB;
public void setColleagueA(ConcreteColleagueA colleagueA) {
this.colleagueA = colleagueA;
}
public void setColleagueB(ConcreteColleagueB colleagueB) {
this.colleagueB = colleagueB;
}
@Override
public void notify(Colleague colleague, String message) {
if (colleague == colleagueA) {
System.out.println("Colleague A sends message: " + message);
colleagueB.receive(message);
} else if (colleague == colleagueB) {
System.out.println("Colleague B sends message: " + message);
colleagueA.receive(message);
}
}
}
定义同事接口
// 同事接口
public abstract class Colleague {
protected Mediator mediator;
public Colleague(Mediator mediator) {
this.mediator = mediator;
}
public abstract void send(String message);
public abstract void receive(String message);
}
具体同事类
// 具体同事类A
public class ConcreteColleagueA extends Colleague {
public ConcreteColleagueA(Mediator mediator) {
super(mediator);
}
@Override
public void send(String message) {
mediator.notify(this, message);
}
@Override
public void receive(String message) {
System.out.println("ConcreteColleagueA received: " + message);
}
}
// 具体同事类B
public class ConcreteColleagueB extends Colleague {
public ConcreteColleagueB(Mediator mediator) {
super(mediator);
}
@Override
public void send(String message) {
mediator.notify(this, message);
}
@Override
public void receive(String message) {
System.out.println("ConcreteColleagueB received: " + message);
}
}
客户端
public class Main {
public static void main(String[] args) {
ConcreteMediator mediator = new ConcreteMediator();
ConcreteColleagueA colleagueA = new ConcreteColleagueA(mediator);
ConcreteColleagueB colleagueB = new ConcreteColleagueB(mediator);
mediator.setColleagueA(colleagueA);
mediator.setColleagueB(colleagueB);
colleagueA.send("Hello from A");
colleagueB.send("Hello from B");
}
}
中介者模式的关键点
- 降低耦合:通过中介者管理对象间的通信,减少了对象之间的直接依赖,提高了系统的灵活性。
- 集中管理:中介者集中管理了所有的交互逻辑,使得对象间的交互变得简单而清晰。
- 易于扩展:如果需要新增同事类,只需实现同事接口并在中介者中添加相应的逻辑,不需要修改现有的代码。
优缺点
优点:
- 降低复杂性:通过中介者,减少了对象之间的直接连接,降低了系统的复杂性。
- 便于扩展:添加新的同事类或功能时,只需修改中介者,而不需要修改其他对象。
- 集中化控制:所有交互逻辑集中在中介者中,便于管理和维护。
缺点:
- 中介者过于复杂:如果中介者需要处理的对象和逻辑较多,可能导致中介者变得复杂和难以维护。
- 增加了中介者的依赖:所有对象都依赖于中介者,可能成为系统中的单点故障。
适用场景
- 对象之间的复杂交互:当多个对象之间有复杂的交互关系时,使用中介者模式可以简化这些关系。
- 需要松耦合设计:在系统中希望降低对象之间的耦合度时,中介者模式是一个不错的选择。
- 动态交互:需要在运行时动态改变对象之间的交互关系时,适合使用中介者模式。
实际应用
- GUI框架:在图形用户界面中,不同组件(如按钮、文本框)之间的交互可以通过中介者模式来管理。
- 聊天系统:在聊天室中,用户通过中介者(如聊天服务器)发送和接收消息,减少了用户之间的直接连接。
- 工作流管理:在工作流系统中,不同任务之间的依赖和交互可以通过中介者模式进行有效管理。
总结
中介者模式通过引入中介者来管理对象间的交互,降低了对象间的耦合度,增强了系统的灵活性和可维护性。适用于对象之间存在复杂交互的场景,但需注意中介者可能变得复杂,需谨慎设计。
解释器模式(Interpreter Pattern)
定义:解释器模式的核心思想是为一种特定的语言定义语法规则,并提供一种解释执行这些语法的方式。通过创建一系列的解释器类来实现不同的语法规则,从而能够解析和执行该语言的句子。
角色
- Context(上下文):包含全局信息,供解释器使用,通常用于存储变量、状态等。
- AbstractExpression(抽象表达式):定义解释的方法,并声明一个接受上下文的方法。
- TerminalExpression(终结符表达式):实现文法中的终结符规则,直接解释基本元素。
- NonterminalExpression(非终结符表达式):实现文法中的非终结符规则,通常由多个表达式组合而成。
定义上下文
// 上下文类
import java.util.HashMap;
import java.util.Map;
public class Context {
private Map<String, Integer> variables = new HashMap<>();
public int getVariable(String key) {
return variables.getOrDefault(key, 0);
}
public void setVariable(String key, int value) {
variables.put(key, value);
}
}
定义抽象表达式
// 抽象表达式
public abstract class AbstractExpression {
public abstract int interpret(Context context);
}
终结符表达式
// 终结符表达式
public class TerminalExpression extends AbstractExpression {
private String key;
public TerminalExpression(String key) {
this.key = key;
}
@Override
public int interpret(Context context) {
return context.getVariable(key);
}
}
非终结符表达式
// 非终结符表达式
public class NonterminalExpression extends AbstractExpression {
private AbstractExpression leftExpression;
private AbstractExpression rightExpression;
public NonterminalExpression(AbstractExpression left, AbstractExpression right) {
this.leftExpression = left;
this.rightExpression = right;
}
@Override
public int interpret(Context context) {
return leftExpression.interpret(context) + rightExpression.interpret(context); // 例如简单加法
}
}
客户端
public class Main {
public static void main(String[] args) {
Context context = new Context();
context.setVariable("x", 10);
context.setVariable("y", 5);
// 表达式: x + y
AbstractExpression x = new TerminalExpression("x");
AbstractExpression y = new TerminalExpression("y");
AbstractExpression expression = new NonterminalExpression(x, y);
int result = expression.interpret(context);
System.out.println("Result: " + result); // 输出: Result: 15
}
}
解释器模式的关键点
- 定义语言的语法:通过抽象类和具体类的组合,定义了语言的语法规则。
- 上下文管理:上下文对象负责存储和管理解析过程中所需的状态信息。
- 可扩展性:可以通过添加新的表达式类来扩展语言的语法和功能。
优缺点
优点:
- 易于扩展:新增语法规则只需增加新的解释器类,不影响现有代码。
- 清晰的语法结构:通过类的结构清晰地定义了语法和逻辑。
- 灵活性:可以使用组合表达式来构造复杂的语法结构。
缺点:
- 类的数量增加:如果语法规则复杂,可能导致需要创建大量的类,增加系统的复杂性。
- 性能问题:在某些情况下,解释执行可能导致性能问题,尤其是在处理大量数据时。
适用场景
- 简单语言解释器:需要实现简单的表达式计算或简单语言解析的场景。
- 需要灵活语法的系统:例如规则引擎、查询解析等需要动态解析和执行的系统。
- 特定领域语言:在某些特定领域(如编程语言、配置文件等)中,定义和解释领域特定的语言。
实际应用
- SQL解析器:在数据库系统中,解释SQL语句的执行过程。
- 编译器:编译器中的语法分析部分,解析和执行代码。
- 规则引擎:通过定义规则和条件,动态解析和执行逻辑。
总结
解释器模式通过定义语法和创建解释器,提供了一种灵活的方式来解析和执行特定的语言或表达式。尽管可能增加类的数量和引入性能问题,但在需要灵活性和扩展性的场合,它提供了一种有效的解决方案。
空对象模式(Null Object Pattern)
定义:空对象模式的核心思想是创建一个类的实例,该实例在逻辑上表示“无”或“空”,但仍然遵循该类的接口。这样,客户端代码可以直接使用这些对象而不必担心它们是否为 null,从而简化代码并提高可读性。
角色
- Abstract Class(抽象类):定义行为的接口或抽象类。
- Real Object(真实对象):实现具体行为的类。
- Null Object(空对象):实现抽象类,并提供一种无操作的默认实现。
定义抽象类
// 抽象类
public abstract class AbstractOperation {
public abstract void doSomething();
}
实现真实对象
// 真实对象
public class RealOperation extends AbstractOperation {
@Override
public void doSomething() {
System.out.println("Performing a real operation.");
}
}
实现空对象
// 空对象
public class NullOperation extends AbstractOperation {
@Override
public void doSomething() {
// 不执行任何操作
System.out.println("Doing nothing.");
}
}
客户端
public class Main {
public static void main(String[] args) {
AbstractOperation realOperation = new RealOperation();
AbstractOperation nullOperation = new NullOperation();
performOperation(realOperation); // 输出: Performing a real operation.
performOperation(nullOperation); // 输出: Doing nothing.
}
public static void performOperation(AbstractOperation operation) {
operation.doSomething();
}
}
空对象模式的关键点
- 避免 null 检查:通过使用空对象,消除了代码中对 null 值的频繁检查,简化了逻辑。
- 符合开放/关闭原则:可以通过添加新的具体对象或空对象,灵活地扩展系统,而不影响现有代码。
- 统一接口:所有对象都遵循相同的接口,客户端代码可以透明地使用它们。
优缺点
优点:
- 简化代码:减少了 null 检查,使得代码更简洁、可读。
- 提高健壮性:避免了因 null 引发的空指针异常,提高了系统的健壮性。
- 灵活性:便于扩展新的行为,只需添加新的具体对象或空对象。
缺点:
- 可能增加复杂性:如果存在多个空对象,可能导致系统中有多个类的实现,增加了维护复杂性。
- 不适合所有情况:在某些场合,空对象可能并不代表“无操作”,使用 null 可能更合适。
适用场景
- 需要处理可选行为:在某些情况下,某些对象可能没有具体实现,而使用空对象可以避免 null 检查。
- 减少空指针异常:在复杂系统中,避免因空指针引起的异常,提高代码的安全性。
- 策略模式和命令模式:在这些模式中,可以使用空对象来实现无操作的行为。
实际应用
- 日志系统:可以使用空日志对象来替代 null 日志对象,避免日志记录时的空检查。
- 配置系统:在配置管理中,使用空配置对象来表示缺失的配置。
- 用户接口:在用户管理系统中,使用空用户对象来处理未找到用户的情况。
总结
空对象模式通过引入空对象来代替 null 值,简化了代码并提高了系统的健壮性。适用于需要减少 null 检查和空指针异常的场景,但也需注意其可能增加的复杂性。在设计中应谨慎使用,以确保代码的清晰和易于维护。