前言
设计模式从面向对象的角度出发,通过对封装、继承、多态、组合等技术的反复使用提炼出可反复使用的面向对象设计技巧,不涉及具体的代码实现,而是提供解决问题的思路和结构,他的作用是让人们写出可复用和可维护性高的程序,但不可避免可能会增加程序的复杂度,鼓励将行为划分到各个对象内部,把对象划分为更小的粒度,有助于增强对象的可复用性,以及减少耦合性。
本文的主要目的是收集一些常见的设计模式,方便查询与学习,欢迎大家补充和纠正。
设计模式的分类
设计模式大体上分为三大类:
- 创建型模式:关注对象的创建过程,帮助我们创建对象而不会暴露创建逻辑,并且通过某种方式提供更好的对象创建管理。
- 常见的创建型模式有:
- 工厂模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
- 单例模式(Singleton)
- 建造者模式(Builder)
- 原型模式(Prototype)
- 常见的创建型模式有:
- 结构型模式:处理类或对象的组合,目的是为了获得更大的灵活性和复用性。它们帮助我们以某种方式组合对象,以便更好地组织代码结构。
- 常见的结构型模式有:
- 适配器模式(Adapter)
- 桥接模式(Bridge)
- 装饰者模式(Decorator)
- 组合模式(Composite)
- 外观模式(Facade)
- 享元模式(Flyweight)
- 代理模式(Proxy)
- 常见的结构型模式有:
- 行为型模式:关注对象之间的通信,帮助定义对象之间的职责以及如何进行交互。它们旨在解决如何合理地分配责任和管理对象间复杂的通信。
- 常见的行为型模式有:
- 策略模式(Strategy)
- 观察者模式(Observer)
- 责任链模式(Chain of Responsibility)
- 命令模式(Command)
- 迭代器模式(Iterator)
- 中介者模式(Mediator)
- 备忘录模式(Memento)
- 状态模式(State)
- 模板方法模式(Template Method)
- 访问者模式(Visitor)
- 解释器模式(Interpreter)
- 常见的行为型模式有:
1 单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,旨在确保一个类只有一个实例,并提供全局访问该实例的方式。它常用于需要控制资源的唯一性,避免重复实例化的场景,如日志记录器、数据库连接池、配置管理器等。
1.1 单例模式的核心思想
- 唯一实例:单例模式确保一个类只能有一个实例,并且该实例是全局可访问的。
- 全局访问点:通过类本身提供的静态方法,外部可以获取唯一的实例。
1.2 单例模式的实现步骤
- 构造函数私有化:防止类在外部通过构造函数创建实例。
- 提供静态方法获取实例:通过一个静态方法来返回该类的唯一实例,确保外部只能通过此方法获取对象。
- 保存唯一实例:使用静态成员变量保存类的唯一实例。
1.3 单例模式的实现示例(懒汉式)
懒汉式单例模式是在第一次调用 getInstance() 方法时才创建实例,延迟初始化。这是最常见的单例模式实现。
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
}
}
1.4 单例模式的优缺点
1.4.1 优点:
- 控制实例数量:保证全局只有一个实例,节省系统资源。
- 全局访问点:提供一个全局访问对象的方法,易于访问。
- 延迟加载(懒汉式):可以在第一次使用时再创建实例,减少内存开销。
1.4.2 缺点:
- 扩展性差:单例类不易扩展,因为它的实例是唯一的,不能轻易继承或修改其行为。
- 并发问题:在多线程环境中,懒汉式的单例可能需要加锁来保证线程安全,这会带来一定的性能开销。
- 隐藏依赖性:使用全局单例可能导致代码中的隐藏依赖,使得代码可测试性和可维护性下降。
1.5 适用场景
- 需要控制资源的唯一性:如数据库连接池、日志管理器、文件系统等。
- 需要共享状态的地方:某些全局配置类或者状态控制器。
2 策略模式
策略模式(Strategy Pattern)是一种行为型设计模式,它定义了一系列算法,并将每个算法封装起来,使得它们可以互相替换。这种模式的关键在于将算法的实现从使用算法的代码中分离出来,以便不同的算法可以灵活替换。
2.1 策略模式的基本组成:
- 策略接口(Strategy Interface):定义所有具体策略必须实现的算法接口。
- 具体策略类(Concrete Strategy Classes):实现策略接口,包含具体算法的实现。
- 上下文类(Context Class):使用策略接口,包含对策略对象的引用,允许上下文在运行时决定使用哪个策略。
2.2 具体实现
2.2.1 策略接口
// 支付策略接口
public interface PaymentStrategy {
void pay(int amount);
}
2.2.2 具体策略类
// 使用信用卡支付的策略
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card: " + cardNumber);
}
}
// 使用PayPal支付的策略
public class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal: " + email);
}
}
// 使用比特币支付的策略
public class BitcoinPayment implements PaymentStrategy {
private String walletAddress;
public BitcoinPayment(String walletAddress) {
this.walletAddress = walletAddress;
}
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using Bitcoin Wallet: " + walletAddress);
}
}
2.2.3 上下文类
// 上下文类,用于选择并执行支付策略
public class PaymentContext {
private PaymentStrategy paymentStrategy;
// 构造方法中传入支付策略
public PaymentContext(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
// 执行支付操作
public void executePayment(int amount) {
paymentStrategy.pay(amount);
}
}
2.2.4 客户端代码
public class Main {
public static void main(String[] args) {
// 使用信用卡支付
PaymentContext context = new PaymentContext(new CreditCardPayment("1234-5678-9012-3456"));
context.executePayment(100);
// 使用PayPal支付
context = new PaymentContext(new PayPalPayment("user@example.com"));
context.executePayment(200);
// 使用比特币支付
context = new PaymentContext(new BitcoinPayment("1BitcoinWalletAddress"));
context.executePayment(300);
}
}
2.3 总结:
- 扩展性强:如果需要新增一种支付方式,只需添加一个实现
PaymentStrategy接口的新类,而无需修改现有的支付逻辑。 - 符合开闭原则:对修改关闭,对扩展开放。
- 灵活性:不同的策略可以在运行时动态选择和替换。
2.4 使用场景
策略模式非常适合那些需要动态替换算法或行为的场景,比如支付系统、折扣策略、排序算法等。
3 代理模式
代理模式(Proxy Pattern)是一种结构型设计模式,它为其他对象提供一种代理,以控制对这个对象的访问。代理模式通常用来延迟对象的创建、控制访问权限或在访问对象时进行一些额外的处理。
3.1 代理模式的基本概念
- 代理(Proxy):代理类持有对实际对象的引用,并提供与实际对象相同的接口。代理类可以在调用实际对象的方法之前或之后执行一些额外的操作。
- 实际对象(Real Subject):代理所代表的实际业务类。
- 客户端(Client):通过代理类间接与实际对象进行交互,客户端并不知道它是在和代理交互,而不是直接与实际对象交互。
3.2 代理模式的类型
根据代理的目的不同,代理模式可以分为以下几种类型:
- 远程代理(Remote Proxy):为一个位于不同地址空间的对象提供代理,通常在分布式系统中使用。
- 虚拟代理(Virtual Proxy):延迟初始化资源密集型对象,直到真正需要时才创建,节省系统资源。
- 保护代理(Protection Proxy):控制对对象的访问,常用于权限控制。
- 缓存代理(Cache Proxy):为耗时的操作提供临时的缓存,避免重复执行相同操作。
- 智能引用代理(Smart Reference Proxy):在访问对象时进行额外的操作,如计数、日志记录等。
3.3 代理模式的结构
代理模式通常包含以下角色:
- 抽象主题(Subject):声明真实对象与代理对象的共同接口,以便在任何使用真实对象的地方都可以使用代理对象。
- 具体主题(RealSubject):实现抽象主题的具体类,表示实际的业务逻辑。
- 代理(Proxy):实现抽象主题的代理类,包含对真实对象的引用,可以在调用真实对象的方法前后做一些附加操作。
3.4 代理模式的实现示例
以访问图片的例子为例,假设图片的加载过程非常耗时,我们可以使用虚拟代理来延迟加载图片,直到真正需要显示时才加载。
3.4.1 抽象主题接口
// 抽象主题,定义图片的加载方法
public interface Image {
void display();
}
3.4.2 具体主题类(实际对象)
// 真实的图片类,加载和显示图片
public class RealImage implements Image {
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk();
}
// 加载图片
private void loadFromDisk() {
System.out.println("Loading " + fileName);
}
@Override
public void display() {
System.out.println("Displaying " + fileName);
}
}
3.4.3 代理类
// 代理图片类,控制图片的加载
public class ProxyImage implements Image {
private RealImage realImage;
private String fileName;
public ProxyImage(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
// 延迟加载图片,只有在需要时才创建RealImage对象
if (realImage == null) {
realImage = new RealImage(fileName);
}
realImage.display();
}
}
3.4.4 客户端代码
public class Client {
public static void main(String[] args) {
Image image = new ProxyImage("test_image.jpg");
// 图片第一次加载时,从磁盘读取
image.display();
System.out.println("");
// 图片第二次显示时,直接使用已经加载的图片
image.display();
}
}
3.4.5 运行结果:
Loading test_image.jpg
Displaying test_image.jpg
Displaying test_image.jpg
3.5 代理模式的优点
- 控制访问:通过代理可以控制对真实对象的访问,比如延迟加载、权限控制等。
- 性能优化:虚拟代理可以在必要时才创建对象,减少资源浪费,提高系统性能。
- 增强功能:代理可以在真实对象的前后执行额外的操作,比如日志记录、计数器、安全控制等。
3.6 代理模式的缺点
- 增加复杂性:引入了额外的代理对象,可能会增加系统的复杂性。
- 延迟处理:虚拟代理可能会带来响应时间的延迟,特别是在首次访问时。
3.7 代理模式的应用场景
- 远程调用:通过远程代理处理不同地址空间中的对象通信。
- 权限控制:通过保护代理限制访问对象的权限,比如在大型系统中对敏感资源的访问控制。
- 延迟加载:虚拟代理延迟资源密集型对象的初始化,如大文件、图片、数据库连接等。
- 日志记录与监控:智能引用代理在访问对象时增加日志记录、计数等操作。
3.8 总结
代理模式的核心思想是控制对对象的访问,通过代理类来增强、延迟、或保护对实际对象的操作。它可以有效地提高系统的灵活性、性能和安全性,在很多场景中得到广泛应用。
例如代理图片预加载,和给源对象img设置src可以分别隔离在两个对象里面,如果某一天要去掉预加载不必更改源对象,开放封闭原则,附加功能用代理对象实现,不去更改源对象
面向对象设计鼓励将行为分布到更细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。
4 迭代器模式
迭代器模式(Iterator Pattern)是一种行为型设计模式,它提供了一种方法顺序访问一个集合对象中的各个元素,而不需要暴露集合对象的内部表示。通过迭代器模式,客户端可以使用相同的方式遍历不同集合结构(如数组、列表、树等),从而达到访问集合元素的统一性。
4.1 迭代器模式的组成部分
- 迭代器接口(Iterator Interface):定义访问和遍历元素的方法。
- 具体迭代器(Concrete Iterator):实现迭代器接口,负责具体集合元素的遍历。
- 聚合接口(Aggregate Interface):定义创建迭代器的方法。
- 具体聚合(Concrete Aggregate):实现聚合接口,存储集合中的元素并返回迭代器。
4.2 迭代器模式的工作原理
- 聚合类(如集合、列表等)实现一个创建迭代器的接口,允许客户端获取一个迭代器对象。
- 迭代器类负责遍历集合中的元素,并提供统一的遍历方法,如
next()、hasNext()。 - 客户端通过迭代器访问集合中的元素,而不需要关心集合的内部实现。
4.3 迭代器模式的实现示例
假设我们有一个包含多个元素的数字集合,我们使用迭代器模式来遍历这个集合。
4.3.1 迭代器接口
// 迭代器接口,定义遍历集合的方法
public interface Iterator {
boolean hasNext();
Object next();
}
4.3.2 具体迭代器
// 数字集合的具体迭代器,实现遍历功能
public class NumberIterator implements Iterator {
private int[] numbers;
private int position = 0;
public NumberIterator(int[] numbers) {
this.numbers = numbers;
}
@Override
public boolean hasNext() {
return position < numbers.length;
}
@Override
public Object next() {
if (hasNext()) {
return numbers[position++];
}
return null;
}
}
4.3.3 聚合接口
// 聚合接口,定义创建迭代器的方法
public interface Aggregate {
Iterator createIterator();
}
4.3.4 具体聚合
// 数字集合类,存储元素并创建迭代器
public class NumberCollection implements Aggregate {
private int[] numbers;
public NumberCollection(int[] numbers) {
this.numbers = numbers;
}
@Override
public Iterator createIterator() {
return new NumberIterator(numbers);
}
}
4.3.5 客户端代码
public class Client {
public static void main(String[] args) {
// 创建一个数字集合
NumberCollection numberCollection = new NumberCollection(new int[] {1, 2, 3, 4, 5});
// 创建迭代器
Iterator iterator = numberCollection.createIterator();
// 遍历集合中的元素
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
4.3.6 运行结果
1
2
3
4
5
4.4 迭代器模式的优点
- 分离遍历和集合的实现:迭代器模式将集合的遍历操作封装在迭代器中,不暴露集合的内部结构,使集合和遍历逻辑解耦。
- 统一遍历接口:迭代器模式提供了一套统一的接口,无论集合的内部结构如何(如数组、列表、链表等),客户端都可以通过相同的方式访问集合中的元素。
- 扩展性好:可以为不同类型的集合提供不同的迭代器实现,添加新的集合类型时,不需要修改客户端代码。
4.5 迭代器模式的缺点
- 开销增加:对于较小的集合,使用迭代器可能会增加一些额外的开销,如存储迭代器对象。
- 单一职责可能被打破:如果迭代器还要处理复杂的遍历逻辑,可能会承担过多的职责。
4.6 迭代器模式的应用场景
- 遍历复杂的数据结构:当需要遍历不同类型的集合时(如列表、树、图等),可以使用迭代器模式。
- 隐藏集合的内部结构:当不希望客户端了解集合的内部表示时,可以通过迭代器提供访问接口。
- 提供不同的遍历方式:可以实现多个不同的迭代器来提供不同的遍历方式,例如正序遍历、倒序遍历等。
4.7 总结
迭代器模式通过将遍历逻辑封装在迭代器中,提供了对集合元素的统一访问方式,解耦了集合的内部结构和遍历操作,增强了代码的灵活性和可扩展性。
5 发布订阅模式
发布-订阅模式(Publish-Subscribe Pattern),也称为观察者模式(Observer Pattern),是一种行为型设计模式,它定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个发布者对象。当发布者状态发生改变时,所有订阅者都会收到通知并自动更新。
5.1 发布-订阅模式的基本概念
- 发布者(Publisher):发布者维护一组订阅者,当发布者状态发生变化时,会通知所有订阅者。
- 订阅者(Subscriber):订阅者注册到发布者上,以便在发布者状态改变时接收到通知。
- 通知机制:发布者通过某种方式(如回调函数)通知所有订阅者。
5.2 发布-订阅模式的组成部分
- 发布者(Publisher 或 Subject):负责管理订阅者列表,并在内部状态发生变化时通知订阅者。
- 订阅者(Subscriber 或 Observer):订阅发布者的通知,并在接收到通知时执行相应的更新操作。
- 通知机制:发布者通过通知机制告知订阅者有状态变化。
5.3 发布-订阅模式的工作流程
- 订阅:订阅者注册到发布者,表示对发布者的状态变化感兴趣。
- 发布:发布者的状态发生变化时,通知所有注册的订阅者。
- 通知:订阅者接收到通知后执行相应的操作(如更新状态、刷新视图等)。
5.4 发布-订阅模式的实现示例
以一个简化的新闻发布系统为例,新闻发布者(Publisher)可以发布新闻,当有新消息发布时,所有订阅者(Subscribers)都会收到通知。
5.4.1 发布者接口
import java.util.ArrayList;
import java.util.List;
// 发布者接口
public interface Publisher {
void subscribe(Subscriber subscriber); // 添加订阅者
void unsubscribe(Subscriber subscriber); // 移除订阅者
void notifySubscribers(); // 通知所有订阅者
}
5.4.2 具体发布者类
// 具体发布者,实现发布者接口
public class NewsPublisher implements Publisher {
private List<Subscriber> subscribers = new ArrayList<>();
private String news; // 存储最新的新闻
// 添加订阅者
@Override
public void subscribe(Subscriber subscriber) {
subscribers.add(subscriber);
}
// 移除订阅者
@Override
public void unsubscribe(Subscriber subscriber) {
subscribers.remove(subscriber);
}
// 通知所有订阅者
@Override
public void notifySubscribers() {
for (Subscriber subscriber : subscribers) {
subscriber.update(news);
}
}
// 发布新闻
public void publishNews(String news) {
this.news = news;
notifySubscribers(); // 通知所有订阅者
}
}
5.4.3 订阅者接口
// 订阅者接口
public interface Subscriber {
void update(String news); // 更新方法,用于接收发布者的通知
}
5.4.4 具体订阅者类
// 具体订阅者,实现订阅者接口
public class NewsSubscriber implements Subscriber {
private String name;
public NewsSubscriber(String name) {
this.name = name;
}
// 当发布者有新消息时,订阅者接收更新
@Override
public void update(String news) {
System.out.println(name + " received news update: " + news);
}
}
5.4.5 客户端代码
public class Client {
public static void main(String[] args) {
// 创建发布者
NewsPublisher newsPublisher = new NewsPublisher();
// 创建订阅者
Subscriber subscriber1 = new NewsSubscriber("Alice");
Subscriber subscriber2 = new NewsSubscriber("Bob");
// 订阅新闻
newsPublisher.subscribe(subscriber1);
newsPublisher.subscribe(subscriber2);
// 发布新闻
newsPublisher.publishNews("Breaking News: New Java release!");
// 取消订阅
newsPublisher.unsubscribe(subscriber1);
// 发布新的新闻
newsPublisher.publishNews("Update: New features in Java!");
}
}
5.4.6 运行结果:
Alice received news update: Breaking News: New Java release!
Bob received news update: Breaking News: New Java release!
Bob received news update: Update: New features in Java!
5.5 发布-订阅模式的优点
- 松耦合:发布者和订阅者之间通过接口进行交互,彼此之间并不需要知道对方的具体实现,有效降低了耦合性。
- 动态添加订阅者:可以在运行时添加或移除订阅者,不影响其他订阅者的工作。
- 扩展性好:可以轻松地扩展发布者或订阅者,支持多个发布者和多个订阅者。
5.6 发布-订阅模式的缺点
- 通知延迟:在有大量订阅者时,通知的速度可能会降低,尤其是在同步通知的情况下。
- 复杂性增加:系统中涉及多个订阅者和发布者时,管理关系可能会变得复杂。
- 过度通知:如果没有进行良好的过滤或条件通知,所有订阅者可能会收到不必要的通知,造成效率低下。
5.7 发布-订阅模式的应用场景
- 事件驱动系统:如GUI事件处理机制中,按钮点击会通知监听器更新。
- 日志系统:不同的日志系统可以作为订阅者接收不同种类的日志信息。
- 消息队列系统:通过消息队列发布者和订阅者进行消息的异步通信。
- 数据绑定:如前端开发中的数据绑定框架,当数据模型发生变化时,视图会自动更新。
5.8 发布-订阅模式在实际中的应用
- Java 中的
java.util.Observer和Observable类是对观察者模式的标准实现,虽然它们已被标记为过时,但仍然能够很好地展示发布-订阅模式的核心思想。 - 消息队列(Message Queue) 系统,如 RabbitMQ、Kafka 等,基于发布-订阅模式提供异步消息处理。
5.9 总结
发布-订阅模式通过解耦发布者和订阅者,使得它们可以独立发展。它不仅适用于简单的通知机制,还可以用在复杂的消息系统中,实现灵活的扩展和动态的消息分发。
6 命令模式
命令模式(Command Pattern)是一种行为型设计模式,它将请求封装成对象,从而允许系统使用不同的请求、队列或日志来参数化方法。使用命令模式,可以将请求的发送者与接收者解耦,使得请求的发出和执行分离开来,从而更加灵活地控制请求的处理过程。
1. 命令模式的组成
命令模式通常由以下几部分组成:
- 命令接口(Command Interface):声明执行命令的操作。
- 具体命令类(Concrete Command):实现命令接口,封装具体的命令行为。
- 接收者(Receiver):实际执行命令逻辑的对象。
- 调用者(Invoker):负责调用命令,通常保存对命令对象的引用。
- 客户端(Client):创建具体的命令对象,并将其传递给调用者。
2. 命令模式的工作流程
- 客户端创建一个具体命令对象,具体命令对象会绑定执行者(接收者)。
- 客户端将具体命令对象传递给调用者,调用者调用该命令的
execute()方法。 - 具体命令对象将调用传递给接收者,接收者执行相应的操作。
3. 命令模式的示例
以遥控器控制家电为例,遥控器可以执行各种命令,比如打开电视、关闭电灯等。我们可以通过命令模式将这些操作封装起来,使得遥控器和具体的家电控制逻辑解耦。
3.3.1 命令接口
java
复制代码
// 命令接口,定义执行操作
public interface Command {
void execute();
}
3.3.2 接收者类
java
复制代码
// 电视类(接收者)
public class TV {
public void on() {
System.out.println("TV is On");
}
public void off() {
System.out.println("TV is Off");
}
}
// 电灯类(接收者)
public class Light {
public void on() {
System.out.println("Light is On");
}
public void off() {
System.out.println("Light is Off");
}
}
3.3.3 具体命令类
java
复制代码
// 打开电视的命令
public class TVOnCommand implements Command {
private TV tv;
public TVOnCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.on();
}
}
// 关闭电视的命令
public class TVOffCommand implements Command {
private TV tv;
public TVOffCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.off();
}
}
// 打开电灯的命令
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
// 关闭电灯的命令
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
}
3.3.4 调用者类
java
复制代码
// 遥控器类(调用者),可以绑定不同的命令
public class RemoteControl {
private Command command;
// 设置命令
public void setCommand(Command command) {
this.command = command;
}
// 执行命令
public void pressButton() {
command.execute();
}
}
3.3.5 客户端代码
java
复制代码
public class Client {
public static void main(String[] args) {
// 创建接收者
TV tv = new TV();
Light light = new Light();
// 创建具体命令
Command tvOn = new TVOnCommand(tv);
Command tvOff = new TVOffCommand(tv);
Command lightOn = new LightOnCommand(light);
Command lightOff = new LightOffCommand(light);
// 创建遥控器(调用者)
RemoteControl remote = new RemoteControl();
// 控制电视
remote.setCommand(tvOn);
remote.pressButton(); // TV is On
remote.setCommand(tvOff);
remote.pressButton(); // TV is Off
// 控制电灯
remote.setCommand(lightOn);
remote.pressButton(); // Light is On
remote.setCommand(lightOff);
remote.pressButton(); // Light is Off
}
}
3.4 命令模式的优点
- 解耦发送者和接收者:发送者只负责调用命令,接收者执行实际逻辑,双方之间没有直接的依赖关系。
- 扩展性好:新增命令时,只需添加新的具体命令类,无需修改已有的代码。
- 支持撤销和重做操作:可以通过存储命令历史,实现命令的撤销和重做功能。
3.5 命令模式的应用场景
- 请求排队:比如任务调度系统,将操作封装为命令对象并排队处理。
- 日志记录:可以将命令记录下来,支持重做或回放功能。
- 撤销操作:可以存储执行过的命令对象,从而实现撤销功能。
命令模式适用于需要解耦请求的发起者和执行者的场景,尤其是那些可能需要支持撤销、重做或者请求排队的系统。
7 组合模式
组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构来表示“部分-整体”的层次结构。组合模式使得客户端可以统一地对待单个对象和对象组合,也就是说,客户端无需关心处理的是一个单个对象,还是一个组合对象,从而让对象使用的方式具有一致性。
7.1 组合模式的基本概念
- 组合模式的核心思想是将单个对象和组合对象统一处理。组合模式将对象组合成树形结构,其中树的每个节点可以是叶子节点(表示单个对象),也可以是复合节点(表示对象的组合)。
- 组件(Component):定义了组合对象和叶子对象的共同接口,这样客户端可以一致地处理组合对象和叶子对象。
- 叶子(Leaf):表示树的基本单元对象,它不能再有子对象,具体实现组件接口中的操作。
- 组合对象(Composite):包含子对象(可能是叶子或其他组合对象),并实现组件接口中的操作。
7.2 组合模式的组成部分
- 组件(Component):声明组合对象和叶子对象的共同接口,如添加、删除子对象,或执行某种操作等。
- 叶子节点(Leaf):实现组件接口,表示树的最底层节点,它没有子节点,通常是具体的操作对象。
- 组合节点(Composite):实现组件接口,表示有子对象的组合对象,它可以包含叶子对象和其他组合对象,并递归调用子对象的操作。
7.3 组合模式的结构图
Component(组件)
|
|----Leaf(叶子节点)
|
|----Composite(组合节点)
|
|---- Component
|---- Component
7.4 组合模式的实现示例
假设我们要实现一个文件系统,其中有两种对象:文件和文件夹。文件夹中可以包含文件或其他文件夹,通过组合模式,文件和文件夹可以统一处理,文件夹可以递归包含文件和其他文件夹。
7.4.1 组件接口
// 组件接口,定义了文件和文件夹的通用操作
public interface FileComponent {
void show(); // 展示组件信息
}
7.4.2 叶子节点(文件类)
// 文件类,表示叶子节点
public class FileLeaf implements FileComponent {
private String name;
public FileLeaf(String name) {
this.name = name;
}
@Override
public void show() {
System.out.println("File: " + name);
}
}
7.4.3 组合节点(文件夹类)
import java.util.ArrayList;
import java.util.List;
// 文件夹类,表示组合节点,可以包含文件和其他文件夹
public class FolderComposite implements FileComponent {
private String name;
private List<FileComponent> components = new ArrayList<>();
public FolderComposite(String name) {
this.name = name;
}
// 添加子组件
public void addComponent(FileComponent component) {
components.add(component);
}
// 删除子组件
public void removeComponent(FileComponent component) {
components.remove(component);
}
// 展示文件夹内容
@Override
public void show() {
System.out.println("Folder: " + name);
for (FileComponent component : components) {
component.show();
}
}
}
7.4.4 客户端代码
public class Client {
public static void main(String[] args) {
// 创建文件
FileComponent file1 = new FileLeaf("file1.txt");
FileComponent file2 = new FileLeaf("file2.txt");
FileComponent file3 = new FileLeaf("file3.txt");
// 创建文件夹并添加文件
FolderComposite folder1 = new FolderComposite("folder1");
folder1.addComponent(file1);
FolderComposite folder2 = new FolderComposite("folder2");
folder2.addComponent(file2);
folder2.addComponent(file3);
// 创建根文件夹并添加子文件夹
FolderComposite rootFolder = new FolderComposite("root");
rootFolder.addComponent(folder1);
rootFolder.addComponent(folder2);
// 展示文件夹结构
rootFolder.show();
}
}
7.4.5 运行结果:
Folder: root
Folder: folder1
File: file1.txt
Folder: folder2
File: file2.txt
File: file3.txt
7.5 组合模式的优点
- 统一的处理方式:组合模式让客户端可以统一地处理单个对象和组合对象,不需要区分处理叶子对象和组合对象的逻辑,简化了代码。
- 便于扩展:通过添加新的叶子对象或组合对象,系统可以很容易扩展,且不会影响现有代码。
- 简化客户端代码:客户端可以直接通过组件接口操作对象,而不需要关心是单个对象还是组合对象,代码更为简洁。
7.6 组合模式的缺点
- 设计复杂:如果组合对象的结构过于复杂,可能会导致系统设计变得复杂,维护也更为困难。
- 不容易限制组合结构:组合模式允许任意组合对象和叶子对象,有时可能会导致过度灵活,使系统难以管理。
7.7 组合模式的应用场景
- 文件系统:如文件夹和文件的层次结构,文件夹可以包含文件或子文件夹。
- 图形界面:窗口、按钮、文本框等界面组件可以组成复杂的界面结构,其中窗口可以包含其他窗口或组件。
- 菜单系统:菜单和子菜单的结构可以通过组合模式表示,子菜单可以包含菜单项或其他子菜单。
- 组织结构:企业的组织结构图可以通过组合模式来表示,部门可以包含员工,也可以包含子部门。
7.8 总结
组合模式通过将对象组合成树形结构,使得客户端可以统一地对待单个对象和组合对象,适用于需要处理树形结构的场景。它不仅提高了代码的灵活性和可扩展性,也简化了客户端代码的操作逻辑。
8 模板方法模式
模板方法模式(Template Method Pattern)是一种行为型设计模式,它定义了一个操作的算法骨架,并允许子类在不改变算法结构的前提下,重新定义算法中的某些步骤。通过模板方法模式,开发者可以把算法中的不变部分放在父类中,将可变部分延迟到子类中实现,从而实现代码复用和灵活扩展。
8.1 组成部分
- 抽象类(Abstract Class):定义算法的骨架,在其中包含一个或多个模板方法(通常是具体方法),以及若干个可以由子类实现的抽象方法或钩子方法。
- 模板方法(Template Method):在抽象类中定义,封装了一个算法的基本结构,规定了执行的步骤及调用顺序。模板方法一般是
final类型,防止子类改变其执行顺序。 - 具体类(Concrete Class):实现抽象类中定义的抽象方法或钩子方法,具体定义算法的某些步骤。
8.2 模板方法模式的工作原理
- 在父类中定义一个算法的通用流程(算法骨架),但某些具体步骤由子类提供。
- 子类可以重写父类的某些方法,但不能改变算法的整体结构。
- 通过这种方式,确保算法的核心流程不被破坏,同时让子类可以根据需求灵活定制具体实现。
8.3 模板方法模式的类图
AbstractClass(抽象类)
|
|---- templateMethod(模板方法) → 定义算法骨架
|---- primitiveOperation1(抽象方法) → 由子类实现
|---- primitiveOperation2(钩子方法) → 子类可选择实现
|
ConcreteClass(具体类)
|---- primitiveOperation1(具体实现)
|---- primitiveOperation2(具体实现)
8.4 模板方法模式的示例
以一个咖啡和茶制作流程为例,这两个饮品的制作步骤大致相同,但某些步骤略有不同,我们可以通过模板方法模式将相同的部分提取出来,将不同的部分交给子类实现。
8.4.1 抽象类
public abstract class Beverage {
// 模板方法,定义制作饮料的步骤
public final void prepareRecipe() {
// 煮水
boilWater();
// 沏茶或冲咖啡
brew();
// 倒入杯中
pourInCup();
// 添加调味品
addCondiments();
}
// 通用的步骤
private void boilWater() {
System.out.println("Boiling water");
}
private void pourInCup() {
System.out.println("Pouring into cup");
}
// 抽象方法,由子类实现
protected abstract void brew(); // 冲泡
protected abstract void addCondiments(); // 添加调料
}
8.4.2 具体类(茶的制作)
public class Tea extends Beverage {
@Override
protected void brew() {
System.out.println("Steeping the tea");
}
@Override
protected void addCondiments() {
System.out.println("Adding lemon");
}
}
8.5.3 具体类(咖啡的制作)
public class Coffee extends Beverage {
@Override
protected void brew() {
System.out.println("Dripping coffee through filter");
}
@Override
protected void addCondiments() {
System.out.println("Adding sugar and milk");
}
}
8.5.4 客户端代码
public class Client {
public static void main(String[] args) {
Beverage tea = new Tea();
tea.prepareRecipe();
System.out.println();
Beverage coffee = new Coffee();
coffee.prepareRecipe();
}
}
8.5.5 运行结果:
Boiling water
Steeping the tea
Pouring into cup
Adding lemon
Boiling water
Dripping coffee through filter
Pouring into cup
Adding sugar and milk
8.6 模板方法模式的优点
- 代码复用:将算法的通用部分放在父类中,避免了重复代码,子类只需关注定制化的部分,减少了代码冗余。
- 灵活扩展:通过子类实现不同的具体步骤,可以灵活定制算法的实现,而不影响算法的整体结构。
- 控制流程:父类控制着算法的整体流程,而子类实现具体步骤,保证了算法的一致性。
8.7 模板方法模式的缺点
- 子类增多:如果算法的步骤变化较多,每种变化都需要创建新的子类,导致类的数量增加,维护复杂。
- 父类对子类的侵入性:父类中的模板方法固定了算法的执行流程,子类在某些情况下可能无法灵活地调整流程。
8.8 模板方法模式的应用场景
- 流程稳定、细节可变:当算法的大部分步骤是固定的,只有某些部分需要灵活变化时,可以使用模板方法模式。例如,报表生成系统中,不同类型的报表有共同的生成步骤,但某些细节如数据格式、展示方式等可能不同。
- 代码复用:在有多个子类共享相同算法结构时,使用模板方法模式可以避免重复代码。
- 框架设计:框架中可以定义模板方法,让使用者通过继承和重写特定方法来定制化行为。
8.9 钩子方法
在模板方法模式中,通常还会有钩子方法(Hook Method)。钩子方法是一种可选的步骤,父类提供默认的空实现,子类可以根据需要重写该方法来插入额外的行为,而不破坏模板方法的整体结构。例如:
protected void hook() {
// 子类可以选择重写该方法
}
8.10 总结
模板方法模式通过将算法的通用部分放在父类中,把具体的步骤交给子类实现,实现了代码复用与扩展性。在软件设计中,它适用于需要固定流程但某些步骤可变的场景,有助于简化代码、提高灵活性,同时保持算法的一致性。
9 享元模式
享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享大量细粒度对象,减少系统中的内存消耗。享元模式通过将对象的共享部分提取出来,并将不可共享的部分进行外部化,从而减少重复对象的创建,节省内存资源。
9.1 核心思想
享元模式的核心思想是将系统中重复的对象进行共享,通过把对象的内在状态和外在状态分离来实现内存的优化。
- 内在状态:可以共享的部分,不会随环境改变而改变的属性,由享元对象存储并进行复用。
- 外在状态:不能共享的部分,依赖于具体环境的属性,由客户端负责维护。
9.2 享元模式的组成部分
- 享元接口(Flyweight Interface):定义享元对象的行为,通过该接口可以访问对象的内在状态。
- 具体享元(Concrete Flyweight):实现享元接口,包含可以共享的内在状态。
- 不可共享的享元对象(Unshared Flyweight):在某些情况下,不可以共享的对象即是外在状态,通常由外部传入。
- 享元工厂(Flyweight Factory):负责创建和管理享元对象,确保享元对象可以共享使用,避免重复创建。
9.3 享元模式的类图
Flyweight(享元接口)
|
ConcreteFlyweight(具体享元类) ← FlyweightFactory(享元工厂)
|
UnsharedFlyweight(非共享对象)
9.4 享元模式的实现示例
我们通过一个象棋游戏的例子来展示享元模式的实现。在象棋中,棋子的颜色和类型可以共享,而位置是每个棋子特有的。因此,我们可以使用享元模式共享棋子的颜色和类型,位置则作为外在状态由客户端来管理。
9.4.1 享元接口
// 享元接口,定义了棋子的行为
public interface ChessPiece {
void place(int x, int y); // 设置棋子的位置信息
}
9.4.2 具体享元类(共享棋子)
// 具体享元类,表示可以共享的棋子(颜色和类型)
public class ConcreteChessPiece implements ChessPiece {
private String color; // 棋子的颜色(内在状态)
public ConcreteChessPiece(String color) {
this.color = color;
}
@Override
public void place(int x, int y) {
System.out.println("Placing a " + color + " chess piece at (" + x + ", " + y + ")");
}
}
9.4.3 享元工厂类
import java.util.HashMap;
import java.util.Map;
// 享元工厂,负责管理享元对象的创建和共享
public class ChessPieceFactory {
private static final Map<String, ChessPiece> pieces = new HashMap<>();
// 获取享元对象,如果不存在则创建
public static ChessPiece getChessPiece(String color) {
if (!pieces.containsKey(color)) {
pieces.put(color, new ConcreteChessPiece(color));
}
return pieces.get(color);
}
}
9.4.4 客户端代码
public class Client {
public static void main(String[] args) {
// 获取享元对象(棋子),颜色相同的棋子会被共享
ChessPiece blackPiece1 = ChessPieceFactory.getChessPiece("Black");
ChessPiece blackPiece2 = ChessPieceFactory.getChessPiece("Black");
ChessPiece whitePiece1 = ChessPieceFactory.getChessPiece("White");
// 棋子的具体位置(外在状态)由客户端提供
blackPiece1.place(1, 1);
blackPiece2.place(2, 2);
whitePiece1.place(3, 3);
// 输出对象比较,验证是否共享
System.out.println(blackPiece1 == blackPiece2); // true
System.out.println(blackPiece1 == whitePiece1); // false
}
}
9.4.5 运行结果:
Placing a Black chess piece at (1, 1)
Placing a Black chess piece at (2, 2)
Placing a White chess piece at (3, 3)
true
false
9.5 享元模式的优点
- 节省内存:通过共享重复的对象,享元模式大大减少了内存占用,适用于大量重复对象的场景。
- 提高性能:减少了对象的创建次数,降低了内存使用和垃圾回收压力。
- 实现对象的共享:通过分离内在状态和外在状态,多个对象可以共享相同的内在状态。
9.6 享元模式的缺点
- 引入了复杂性:系统需要分离内在状态和外在状态,增加了设计的复杂性,尤其是在对象的状态需要频繁变化时。
- 适用场景有限:享元模式主要适用于内在状态较少、可以共享的对象,过多的外在状态可能导致系统设计复杂化。
9.7 享元模式的应用场景
- 系统中有大量相似对象:当系统中有许多相同或相似对象时,可以使用享元模式减少内存开销。例如:图形编辑软件中的形状、文本编辑器中的字符对象。
- 对象的状态可以分为内在和外在:对象的内在状态是可以共享的,而外在状态依赖于上下文环境。例如:棋盘游戏中的棋子、网页中的字体渲染。
- 需要节省内存的场景:在需要高效使用内存的场景下,通过共享对象减少内存占用。
9.8 享元模式在实际中的应用
- 数据库连接池:数据库连接池通过共享数据库连接对象,避免了每次请求都创建新的连接,从而减少了系统资源消耗。
- 字符串常量池:Java 的字符串常量池机制也是享元模式的一种应用,当创建一个新的字符串时,JVM 会先检查常量池中是否存在相同的字符串对象,如果存在,则直接返回。
9.9 总结
享元模式通过共享对象的内在状态,有效减少了系统中重复对象的创建,节省了内存资源。在一些需要大量相似对象的场景中,享元模式可以显著提高性能和内存利用率。但同时,享元模式也会增加设计复杂性,需要合理划分内在状态和外在状态才能有效应用。
10 职责链模式
职责链模式(Chain of Responsibility Pattern)是一种行为型设计模式,允许多个对象都有机会处理请求,避免请求的发送者和接收者之间的耦合。将这些接收对象连成一条链,并沿着这条链传递请求,直到有对象处理它为止。
职责链模式的关键在于:每个对象都持有对下一个处理对象的引用。当一个请求到来时,对象首先尝试自己处理,如果无法处理,就将请求传递给链中的下一个对象,直到找到能够处理请求的对象。
10.1 职责链模式的组成部分
- 抽象处理者(Handler):定义一个处理请求的接口或抽象类,包含处理请求的抽象方法和设置下一个处理者的引用。
- 具体处理者(Concrete Handler):继承或实现抽象处理者,具体处理请求。如果无法处理请求,则将请求传递给下一个处理者。
- 客户端(Client):创建职责链并发出请求,由职责链中的处理者决定谁来处理请求。
10.2 职责链模式的类图
Handler(抽象处理者)
|
|---- ConcreteHandler1(具体处理者1)
|---- ConcreteHandler2(具体处理者2)
|---- ...
10.3 职责链模式的工作原理
- 每个处理者对象包含对下一个处理者的引用。
- 客户端将请求发送到第一个处理者,处理者根据具体逻辑判断是否能处理请求,如果不能,则将请求传递给链中的下一个处理者。
- 这样,客户端不需要知道具体是哪个处理者处理了请求,消除了发送者与接收者的耦合。
10.4 职责链模式的实现示例
以一个报销审批的例子为例。不同层级的审批者(如组长、经理、总监)有不同的审批权限,报销金额在不同的区间时,应该由不同的审批者进行处理。我们使用职责链模式来实现这个报销审批流程。
10.4.1 抽象处理者
public abstract class Approver {
protected Approver nextApprover; // 下一个处理者
// 设置下一个处理者
public void setNextApprover(Approver nextApprover) {
this.nextApprover = nextApprover;
}
// 处理请求的抽象方法
public abstract void processRequest(int amount);
}
10.4.2 具体处理者(组长、经理、总监)
// 组长,处理1000元以内的报销请求
public class TeamLeader extends Approver {
@Override
public void processRequest(int amount) {
if (amount <= 1000) {
System.out.println("TeamLeader approves " + amount + " RMB.");
} else if (nextApprover != null) {
nextApprover.processRequest(amount);
}
}
}
// 经理,处理5000元以内的报销请求
public class Manager extends Approver {
@Override
public void processRequest(int amount) {
if (amount <= 5000) {
System.out.println("Manager approves " + amount + " RMB.");
} else if (nextApprover != null) {
nextApprover.processRequest(amount);
}
}
}
// 总监,处理10000元以内的报销请求
public class Director extends Approver {
@Override
public void processRequest(int amount) {
if (amount <= 10000) {
System.out.println("Director approves " + amount + " RMB.");
} else if (nextApprover != null) {
nextApprover.processRequest(amount);
}
}
}
10.4.3 客户端代码
public class Client {
public static void main(String[] args) {
// 创建审批者
Approver teamLeader = new TeamLeader();
Approver manager = new Manager();
Approver director = new Director();
// 设置职责链
teamLeader.setNextApprover(manager);
manager.setNextApprover(director);
// 发出报销请求
teamLeader.processRequest(500); // TeamLeader approves 500 RMB.
teamLeader.processRequest(3000); // Manager approves 3000 RMB.
teamLeader.processRequest(8000); // Director approves 8000 RMB.
teamLeader.processRequest(12000); // 12000 exceeds all limits, no approval.
}
}
10.4.4 运行结果:
TeamLeader approves 500 RMB.
Manager approves 3000 RMB.
Director approves 8000 RMB.
10.5 职责链模式的优点
- 降低耦合:请求的发送者和处理者解耦,发送者无需知道具体是哪个处理者处理了请求,处理者只需关注自己的职责。
- 灵活性:职责链可以动态组合,修改链中的处理者顺序或添加新的处理者非常容易。
- 增强扩展性:通过增加新的具体处理者,系统可以很方便地扩展链条的处理逻辑。
10.6 职责链模式的缺点
- 可能影响性能:如果职责链过长,请求需要经过多个处理者,可能会影响系统性能。
- 不易调试:由于请求是在多个处理者之间传递,链条较长时,调试问题会比较困难,尤其是某些请求没有明确的终结处理者。
10.7 职责链模式的应用场景
- 多级请求处理:在系统中,当请求需要经过多个对象逐级处理时,可以使用职责链模式,例如:审批流程、权限校验等。
- 可灵活处理的场景:请求的处理者不明确,可能由多个对象中的某一个或多个对象处理的场景,如:事件处理机制。
- 日志系统:日志处理系统可以根据日志的级别,选择不同的处理者(如将错误日志写入文件,普通日志输出到控制台)。
- 表单验证:验证电话或者邮箱
10.8 职责链模式的实际应用
- 异常处理机制:Java 中的异常处理使用了类似职责链的机制,多个
catch块可以依次尝试处理异常。 - Servlet 过滤器:Java Servlet 中的过滤器链(Filter Chain)是职责链模式的一个典型应用,请求可以在多个过滤器之间传递,直到被最终处理。
10.9
职责链模式通过将请求沿着处理者链传递,使得多个对象都有机会处理请求。它有效降低了发送者和接收者之间的耦合,使得系统更加灵活扩展,适用于请求处理责任不明确、需要动态配置责任链的场景。但在实际应用中,职责链的长度需要适度控制,避免引入性能问题。
11 中介者模式
中介者模式(Mediator Pattern)是一种行为型设计模式,用于减少对象之间的直接交互,通过一个中介者对象来封装多个对象之间的交互。中介者模式通过引入一个中介者对象,使各个对象不再直接引用彼此,而是通过中介者来进行消息传递和协调。这样可以有效降低对象之间的耦合度,使系统更易于维护和扩展。
11.1 核心思想
中介者模式的核心思想是引入一个中介者对象,来管理对象之间的通信,使对象间的交互从“多对多”变成“多对一”。每个对象不再需要知道其他对象的存在,而只需要知道中介者即可。
11.2 中介者模式的组成部分
- 中介者接口(Mediator):定义了与各同事对象交互的方法,通常包括一个事件通知的方法。
- 具体中介者(Concrete Mediator):实现中介者接口,负责协调各同事对象的交互和行为。
- 同事类(Colleague):各个具体的交互对象,持有对中介者的引用,通过中介者与其他同事对象进行交互。
11.3 中介者模式的类图
Mediator(中介者接口)
|
|---- ConcreteMediator(具体中介者)
|
|---- Colleague1(具体同事类1)
|---- Colleague2(具体同事类2)
|---- ...
11.4 中介者模式的工作原理
- 同事类(Colleague)通过调用中介者的方法来与其他同事类进行交互。
- 中介者负责管理同事类之间的通信和协调。
- 这样,同事类之间的直接引用被消除了,所有交互都通过中介者进行。
11.5 中介者模式的示例
以一个聊天室系统为例,用户通过聊天室发送消息给其他用户,而用户之间并不知道彼此的存在,他们通过聊天室(中介者)来进行通信。
11.5.1 中介者接口
// 中介者接口,定义发送消息的行为
public interface ChatMediator {
void sendMessage(String message, User user);
void addUser(User user);
}
11.5.2 具体中介者类
import java.util.ArrayList;
import java.util.List;
// 具体中介者类,实现了聊天室的行为
public class ChatRoom implements ChatMediator {
private List<User> users;
public ChatRoom() {
this.users = new ArrayList<>();
}
@Override
public void addUser(User user) {
users.add(user);
}
@Override
public void sendMessage(String message, User user) {
for (User u : users) {
// 消息不发给自己
if (u != user) {
u.receive(message);
}
}
}
}
11.5.3 同事类
// 同事类,表示聊天室中的用户
public abstract class User {
protected ChatMediator mediator;
protected String name;
public User(ChatMediator mediator, String name) {
this.mediator = mediator;
this.name = name;
}
public abstract void send(String message);
public abstract void receive(String message);
}
11.5.4 具体同事类
// 具体同事类,用户的具体实现
public class ConcreteUser extends User {
public ConcreteUser(ChatMediator mediator, String name) {
super(mediator, name);
}
@Override
public void send(String message) {
System.out.println(this.name + " sends: " + message);
mediator.sendMessage(message, this);
}
@Override
public void receive(String message) {
System.out.println(this.name + " receives: " + message);
}
}
11.5.5 客户端代码
public class Client {
public static void main(String[] args) {
ChatMediator chatRoom = new ChatRoom();
User user1 = new ConcreteUser(chatRoom, "Alice");
User user2 = new ConcreteUser(chatRoom, "Bob");
User user3 = new ConcreteUser(chatRoom, "Charlie");
chatRoom.addUser(user1);
chatRoom.addUser(user2);
chatRoom.addUser(user3);
user1.send("Hi everyone!");
user2.send("Hello Alice!");
}
}
11.5.6 运行结果:
Alice sends: Hi everyone!
Bob receives: Hi everyone!
Charlie receives: Hi everyone!
Bob sends: Hello Alice!
Alice receives: Hello Alice!
Charlie receives: Hello Alice!
11.6 中介者模式的优点
- 减少类之间的耦合:对象不需要直接相互引用,所有通信都通过中介者进行,降低了对象间的耦合性。
- 更易扩展:增加新的同事类或修改交互行为时,只需修改中介者而不必修改每个同事类,系统更易于维护和扩展。
- 简化对象之间的交互:通过中介者,复杂的多对多关系被简化为一对多的关系,系统结构更加清晰。
11.7 中介者模式的缺点
- 中介者过于复杂:随着系统中交互逻辑的增加,中介者会承担越来越多的职责,可能会变得过于复杂,成为“上帝类”。
- 难以维护:如果中介者的逻辑复杂,维护起来也会变得困难,特别是在交互行为较多时。
11.8 中介者模式的应用场景
- 对象之间存在复杂的引用关系,导致对象之间的依赖性过强:例如,图形界面设计中,多个组件之间有相互通信的需求,使用中介者可以简化这些组件之间的复杂交互。
- 一个对象修改其他多个对象的行为:如果一个对象的操作会影响多个对象,且这些对象之间的行为存在依赖性,中介者模式可以帮助控制对象之间的相互作用。
- 事件驱动系统:在事件驱动系统中,事件的发送者和处理者之间的关系可以通过中介者来解耦。
11.9 中介者模式的实际应用
- GUI 框架中的事件处理:例如,MVC 架构中的控制器(Controller)常常作为视图(View)和模型(Model)之间的中介者,协调它们之间的交互。
- 飞机塔台调度系统:飞机之间不能直接通信,它们通过塔台(中介者)进行通信,塔台负责调度和管理飞机的飞行和降落。
- 聊天室应用:聊天室系统中的用户通过聊天室(中介者)发送和接收消息,用户之间无需直接通信。
11.10 总结
中介者模式通过引入一个中介者对象,解耦了对象之间的直接交互,使得系统中的对象能够更好地独立工作。它适用于对象之间存在复杂交互、耦合度较高的场景,可以简化系统的结构并提升可维护性。但中介者模式的中介者对象容易变得复杂,需要在设计时控制好中介者的职责,以避免将所有逻辑都集中到中介者中。
12 工厂模式
工厂模式是一种创建型设计模式,用于通过定义一个用于创建对象的接口来实例化对象,而不需要知道具体的类。工厂模式的主要目的是将对象的创建与使用分离,使得代码更具可维护性和扩展性。
工厂模式有三种常见的实现方式:
- 简单工厂模式(Simple Factory)
- 工厂方法模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
12.1 简单工厂模式
简单工厂模式是最基本的一种工厂模式,提供一个创建对象的静态方法,根据传入的参数决定要实例化的对象。
12.1.1 示例
假设我们要创建不同类型的汽车,可以使用一个简单工厂来生产这些汽车。
// 抽象产品类
abstract class Car {
public abstract void drive();
}
// 具体产品类
class Sedan extends Car {
@Override
public void drive() {
System.out.println("Driving a sedan.");
}
}
class SUV extends Car {
@Override
public void drive() {
System.out.println("Driving an SUV.");
}
}
// 简单工厂类
class CarFactory {
public static Car createCar(String type) {
if (type.equals("sedan")) {
return new Sedan();
} else if (type.equals("suv")) {
return new SUV();
}
return null;
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Car sedan = CarFactory.createCar("sedan");
sedan.drive();
Car suv = CarFactory.createCar("suv");
suv.drive();
}
}
12.1.2 运行结果:
Driving a sedan.
Driving an SUV.
12.1.3 简单工厂模式的优点:
- 简单易用:只需要一个工厂类,根据不同的参数就能创建不同的对象。
12.4.4 缺点:
- 不符合开闭原则:如果需要添加新类型的产品,需要修改工厂类的代码,违反了“对扩展开放,对修改关闭”的原则。
12.2 工厂方法模式
工厂方法模式定义了一个创建对象的接口,但由子类决定实例化哪一个具体类。工厂方法将对象的创建延迟到子类。
12.2.1 示例
使用工厂方法模式,每个类型的汽车有各自的工厂来生产。
// 抽象产品类
abstract class Car {
public abstract void drive();
}
// 具体产品类
class Sedan extends Car {
@Override
public void drive() {
System.out.println("Driving a sedan.");
}
}
class SUV extends Car {
@Override
public void drive() {
System.out.println("Driving an SUV.");
}
}
// 抽象工厂类
interface CarFactory {
Car createCar();
}
// 具体工厂类
class SedanFactory implements CarFactory {
@Override
public Car createCar() {
return new Sedan();
}
}
class SUVFactory implements CarFactory {
@Override
public Car createCar() {
return new SUV();
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
CarFactory sedanFactory = new SedanFactory();
Car sedan = sedanFactory.createCar();
sedan.drive();
CarFactory suvFactory = new SUVFactory();
Car suv = suvFactory.createCar();
suv.drive();
}
}
12.2.2 运行结果:
Driving a sedan.
Driving an SUV.
12.2.3 工厂方法模式的优点:
- 符合开闭原则:每当需要增加新的产品,只需要添加新的工厂类,而不必修改已有的代码。
- 高内聚、低耦合:具体产品的创建由子类实现,父类只关心创建的接口,符合单一职责原则。
12.2.4 缺点:
- 类的数量增加:每种产品都需要一个具体的工厂,导致类的数量增多,增加了系统的复杂性。
12.3 抽象工厂模式
抽象工厂模式提供一个接口,用于创建相关或依赖对象的家族,无需指定具体类。它是一种创建产品家族的工厂模式,可以生产一组相关的产品。
12.3.1 示例
假设我们有不同类型的汽车,还分为不同的配件(例如轮胎、发动机等),抽象工厂模式可以创建相互依赖的产品。
// 抽象产品类
interface Car {
void drive();
}
interface Engine {
void start();
}
// 具体产品类
class Sedan implements Car {
@Override
public void drive() {
System.out.println("Driving a sedan.");
}
}
class SUV implements Car {
@Override
public void drive() {
System.out.println("Driving an SUV.");
}
}
class SedanEngine implements Engine {
@Override
public void start() {
System.out.println("Starting a sedan engine.");
}
}
class SUVEngine implements Engine {
@Override
public void start() {
System.out.println("Starting an SUV engine.");
}
}
// 抽象工厂类
interface CarFactory {
Car createCar();
Engine createEngine();
}
// 具体工厂类
class SedanFactory implements CarFactory {
@Override
public Car createCar() {
return new Sedan();
}
@Override
public Engine createEngine() {
return new SedanEngine();
}
}
class SUVFactory implements CarFactory {
@Override
public Car createCar() {
return new SUV();
}
@Override
public Engine createEngine() {
return new SUVEngine();
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
CarFactory sedanFactory = new SedanFactory();
Car sedan = sedanFactory.createCar();
Engine sedanEngine = sedanFactory.createEngine();
sedan.drive();
sedanEngine.start();
CarFactory suvFactory = new SUVFactory();
Car suv = suvFactory.createCar();
Engine suvEngine = suvFactory.createEngine();
suv.drive();
suvEngine.start();
}
}
12.3.2 运行结果:
Driving a sedan.
Starting a sedan engine.
Driving an SUV.
Starting an SUV engine.
12.3.3抽象工厂模式的优点:
- 产品族的一致性:保证同一个工厂生产出的产品是相互兼容的,产品族之间不相互影响。
- 符合开闭原则:可以通过增加新的工厂来扩展新的产品族。
12.3.4 缺点:
- 复杂度高:随着产品族的增加,系统的复杂度也随之增加。
- 不易扩展新产品:如果需要增加一个新产品,而不是产品族中的某个产品,所有的工厂类都需要进行修改。
12.4 总结
| 模式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 简单工厂模式 | 简单易用,代码集中 | 违反开闭原则,工厂类复杂 | 产品较少、需求变化不频繁的场景 |
| 工厂方法模式 | 符合开闭原则,便于扩展 | 类数量增多,系统复杂性增加 | 需要频繁扩展产品的场景 |
| 抽象工厂模式 | 便于创建产品家族,保证产品族的兼容性 | 难以增加新产品,复杂度较高 | 产品家族多,产品之间有依赖关系的场景 |
工厂模式通过将对象创建过程封装起来,有助于提高系统的灵活性和扩展性。
13 适配器模式
适配器模式(Adapter Pattern)是一种结构型设计模式,它的作用是将一个类的接口转换成客户端希望的另一个接口。适配器模式使得原本接口不兼容的类可以一起工作。它通过提供一个中间层(适配器)来连接不同接口的类,从而实现接口的适配。
适配器模式常用于将现有的类适配到现有的框架或库中,或者当我们想要重用现有的类,但接口与系统不兼容时,可以使用适配器模式来实现兼容性。
13.1 适配器模式的组成部分
- 目标接口(Target):客户端需要的接口,这个接口定义了客户希望使用的方法。
- 现有类(Adaptee):现有的接口,客户端不能直接使用,需要进行适配。
- 适配器(Adapter):适配器类通过实现目标接口,调用现有类的方法,从而让现有类能够适配客户端。
13.2 适配器模式的类型
- 类适配器模式:适配器通过继承现有类,并实现目标接口。
- 对象适配器模式:适配器通过组合现有类,在内部持有现有类的对象,从而进行适配。
13.3 适配器模式的类图
13.3.1 类适配器模式类图:
+--------------+ +------------+
| Target |<-----------| Adapter |
+--------------+ +------------+
^
|
+--------------+
| Adaptee |
+--------------+
13.3.2 对象适配器模式类图:
+--------------+ +------------+
| Target |<-----------| Adapter |
+--------------+ +------------+
^
|
+--------------+
| Adaptee |
+--------------+
13.4 适配器模式的示例
假设我们有一个老式的圆形插头(Adaptee),但现在我们需要适配到现代的三孔插座(Target)。我们可以使用适配器模式来进行适配。
13.4.1 目标接口(Target)
// 定义客户端需要的接口
public interface ThreeHoleSocket {
void plugIn();
}
13.4.2 现有类(Adaptee)
// 老式的两孔插头
public class TwoHolePlug {
public void connect() {
System.out.println("Connected using a two-hole plug.");
}
}
13.4.3 类适配器(Adapter - 类适配器模式)
// 类适配器通过继承实现接口的适配
public class PlugAdapter extends TwoHolePlug implements ThreeHoleSocket {
@Override
public void plugIn() {
// 调用老式插头的方法,实现接口适配
connect();
}
}
13.4.4 对象适配器(Adapter - 对象适配器模式)
// 对象适配器通过组合实现接口的适配
public class PlugAdapter implements ThreeHoleSocket {
private TwoHolePlug twoHolePlug;
public PlugAdapter(TwoHolePlug twoHolePlug) {
this.twoHolePlug = twoHolePlug;
}
@Override
public void plugIn() {
// 调用老式插头的方法,实现接口适配
twoHolePlug.connect();
}
}
13.4.5 客户端代码
public class Client {
public static void main(String[] args) {
// 类适配器方式
ThreeHoleSocket socket = new PlugAdapter();
socket.plugIn(); // 输出: Connected using a two-hole plug.
// 对象适配器方式
TwoHolePlug oldPlug = new TwoHolePlug();
ThreeHoleSocket socket2 = new PlugAdapter(oldPlug);
socket2.plugIn(); // 输出: Connected using a two-hole plug.
}
}
13.4.6 运行结果:
Connected using a two-hole plug.
Connected using a two-hole plug.
13.5 适配器模式的优缺点
13.5.1 优点:
- 提高了类的复用性:可以将现有类复用到不同接口的系统中,而不需要修改原有代码。
- 解耦性强:客户端与现有类之间的依赖通过适配器实现,客户端不直接依赖现有类,增强了系统的可扩展性。
- 灵活性强:可以在不修改现有类的情况下进行扩展,尤其是在第三方类库不易修改的情况下。
13.5.2 缺点:
- 复杂性增加:在增加适配器后,代码的复杂度可能会增加,尤其是类适配器模式需要继承多个类时(由于 Java 等语言不支持多继承,这点尤其困难)。
- 性能问题:在一些性能敏感的应用中,适配器可能引入额外的函数调用,影响性能。
13.6 适用场景
- 接口不兼容时的适配:当系统需要使用一个已经存在的类,但是它的接口与当前系统不兼容时,可以使用适配器模式来进行适配。
- 使用第三方库:如果我们要将一个第三方库整合到现有系统中,但其接口与系统不一致,可以使用适配器模式进行集成。
- 遗留系统的升级:将旧系统中的模块或类迁移到新系统中,而不希望大规模修改旧代码时。
13.7 适配器模式 vs. 装饰器模式
- 适配器模式:关注的是接口的兼容性,解决现有接口与目标接口不匹配的问题。
- 装饰器模式:关注的是功能的扩展,通过动态地为对象添加行为来增强功能。
13.8 总结
适配器模式是一种重要的设计模式,通过它可以让不兼容的类一起工作,减少代码重复、增强代码的可维护性。在现代开发中,特别是在整合第三方系统、老系统改造等场景中非常有用。
14 装饰者模式
装饰者模式(Decorator Pattern)是一种结构型设计模式,允许通过动态地为对象添加额外的职责或行为,而不会影响其他对象。与继承相比,装饰者模式更灵活,因为它通过对象组合来实现功能扩展,而不是通过子类化。
装饰者模式的核心思想是:将核心功能与装饰功能分离,装饰功能通过装饰类动态地叠加在核心功能上。
14.1 装饰者模式的组成部分
- 组件接口(Component):定义一个对象的基本接口,可以是抽象类或接口。
- 具体组件(ConcreteComponent):实现组件接口,定义基本的对象行为。
- 装饰者类(Decorator):持有一个组件对象的引用,实现组件接口,并为组件添加额外功能。它通常是抽象类。
- 具体装饰者类(ConcreteDecorator):继承自装饰者类,向组件添加具体的功能。
14.2 装饰者模式的类图
+------------------+ +-----------------+
| Component |<-----| Decorator |
+------------------+ +-----------------+
^ ^
| |
+------------------+ +------------------+
| ConcreteComponent| | ConcreteDecorator |
+------------------+ +------------------+
- Component 是接口或抽象类,定义了基本功能。
- ConcreteComponent 是实际的对象,实现了基础功能。
- Decorator 是装饰者抽象类,持有 Component 的引用,用于动态地为 ConcreteComponent 添加新功能。
- ConcreteDecorator 是具体的装饰者类,负责为对象添加新的职责。
14.3 装饰者模式的示例
假设我们有一个基本的饮料(Beverage),可以通过添加不同的配料(如牛奶、糖等)来装饰它,并增加其功能或价格。
14.3.1 组件接口(Component)
// 饮料接口,定义基础功能
public abstract class Beverage {
public abstract String getDescription();
public abstract double cost();
}
14.3.2 具体组件(ConcreteComponent)
// 具体的饮料类,比如 Espresso
public class Espresso extends Beverage {
@Override
public String getDescription() {
return "Espresso";
}
@Override
public double cost() {
return 1.99;
}
}
14.3.3 装饰者类(Decorator)
// 装饰者类,扩展 Beverage 类
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}
14.3.4 具体装饰者类(ConcreteDecorator)
// 加牛奶的装饰者
public class Milk extends CondimentDecorator {
Beverage beverage;
public Milk(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
return beverage.getDescription() + ", Milk";
}
@Override
public double cost() {
return 0.20 + beverage.cost();
}
}
// 加糖的装饰者
public class Sugar extends CondimentDecorator {
Beverage beverage;
public Sugar(Beverage beverage) {
this.beverage = beverage;
}
@Override
public String getDescription() {
return beverage.getDescription() + ", Sugar";
}
@Override
public double cost() {
return 0.10 + beverage.cost();
}
}
14.3.5 客户端代码
public class CoffeeShop {
public static void main(String[] args) {
// 订一杯 Espresso
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost());
// 加牛奶和糖
beverage = new Milk(beverage);
beverage = new Sugar(beverage);
System.out.println(beverage.getDescription() + " $" + beverage.cost());
}
}
14.3.6 运行结果:
Espresso $1.99
Espresso, Milk, Sugar $2.29
14.4. 装饰者模式的优缺点
14.4.1 优点:
- 动态扩展对象功能:无需通过创建子类,就可以动态地为对象增加额外的职责,保持了类的灵活性。
- 遵循单一职责原则:每个装饰者类只负责增加一种特定的功能,使得代码更具模块化和可维护性。
- 遵循开闭原则:无需修改已有的类,就能扩展其功能。
14.4.2 缺点:
- 大量小对象的创建:使用装饰者模式会增加对象数量,过多的装饰者类可能增加系统的复杂性。
- 调试困难:由于功能是通过多个装饰者层层叠加的,调试时很难跟踪对象的行为。
14.5 适用场景
- 希望动态地给对象添加功能,且这些功能可以随意组合时。
- 不希望通过继承扩展类的功能,而是通过组合来实现功能扩展。
- 需要为不同的对象组合不同的行为,使得系统更具弹性和可扩展性。
14.6 装饰者模式 vs. 代理模式 vs. 适配器模式
- 装饰者模式:用于扩展对象的功能,不改变其接口,主要通过动态组合来添加职责。
- 代理模式:用于控制对象的访问,通常用于懒加载、权限控制等。
- 适配器模式:用于将现有类的接口转换成另一种接口,以便不同接口的类能够协同工作。
14.7 总结
装饰者模式通过组合的方式为对象添加功能,避免了类的继承层次过深的问题。它非常适合在运行时根据需要动态扩展对象功能的场景,同时还能保持原有对象的结构与行为,确保灵活性与可扩展性。
15 状态模式
状态模式(State Pattern)是一种行为型设计模式,允许对象在其内部状态改变时,改变它的行为。状态模式将对象的行为与其状态进行解耦,通过状态对象来管理状态转移,从而使得状态的变化能够导致不同的行为表现。
在状态模式中,状态是对象的一个重要组成部分,对象在运行过程中可以动态地改变其状态,并且每个状态可以有不同的行为。这种模式有助于避免使用大量的条件语句来管理状态的变化。
15.1 状态模式的组成部分
- 上下文(Context):维护一个当前状态,并负责根据状态执行行为。
- 状态接口(State):定义状态的公共接口,每个具体状态类需要实现这个接口。
- 具体状态类(Concrete State):实现状态接口,定义在特定状态下对象的行为。
15.2 状态模式的类图
+------------------+ +-------------------+
| Context |<---->| State |
+------------------+ +-------------------+
^ ^
| |
+-------------------+ +------------------+
| ConcreteStateA | | ConcreteStateB |
+-------------------+ +------------------+
- Context:上下文对象,它持有状态的引用,并且可以切换状态。
- State:状态接口,定义了在某个状态下,如何响应用户请求。
- ConcreteStateA、ConcreteStateB:具体状态类,表示对象在某种状态下的具体行为。
15.3 状态模式的示例
假设我们要模拟一个电梯的运行过程,电梯有三种状态:待机状态(Idle)、运行状态(Running)和暂停状态(Paused)。不同状态下,电梯的行为是不同的。
15.3.1 状态接口(State)
// 状态接口,定义电梯的行为
public interface ElevatorState {
void pressButton(ElevatorContext context);
}
15.3.2 具体状态类(Concrete State)
// 待机状态
public class IdleState implements ElevatorState {
@Override
public void pressButton(ElevatorContext context) {
System.out.println("Elevator is now running.");
context.setState(new RunningState()); // 切换到运行状态
}
}
// 运行状态
public class RunningState implements ElevatorState {
@Override
public void pressButton(ElevatorContext context) {
System.out.println("Elevator is now paused.");
context.setState(new PausedState()); // 切换到暂停状态
}
}
// 暂停状态
public class PausedState implements ElevatorState {
@Override
public void pressButton(ElevatorContext context) {
System.out.println("Elevator is now idle.");
context.setState(new IdleState()); // 切换到待机状态
}
}
15.3.3 上下文类(Context)
// 上下文类,维护当前状态
public class ElevatorContext {
private ElevatorState currentState;
public ElevatorContext() {
currentState = new IdleState(); // 初始状态为待机状态
}
public void setState(ElevatorState state) {
currentState = state;
}
public void pressButton() {
currentState.pressButton(this);
}
}
15.3.4 客户端代码
public class ElevatorSimulation {
public static void main(String[] args) {
ElevatorContext elevator = new ElevatorContext();
// 模拟电梯状态变化
elevator.pressButton(); // 电梯从待机状态到运行状态
elevator.pressButton(); // 电梯从运行状态到暂停状态
elevator.pressButton(); // 电梯从暂停状态回到待机状态
}
}
15.3.5 运行结果:
Elevator is now running.
Elevator is now paused.
Elevator is now idle.
15.4 状态模式的优缺点
15.4.1 优点:
- 遵循开闭原则:状态和行为是通过状态类实现的,增加新状态时无需修改已有代码,只需添加新的状态类。
- 避免条件语句:将状态与行为封装在不同的类中,避免了大量的
if-else或switch语句。 - 易于扩展和维护:通过独立的状态类实现不同状态下的行为,代码逻辑更清晰,易于维护。
- 封装状态转换:状态的转换逻辑被封装在具体的状态类中,上下文类无需关心状态的变化逻辑。
15.4.2 缺点:
- 类的数量增多:每个状态都需要定义一个类,可能导致类的数量增加,增加系统的复杂度。
- 状态切换的复杂性:如果状态之间的关系复杂,状态转换的管理可能变得难以维护。
15.5 适用场景
- 对象的行为依赖于状态变化:当对象在不同状态下有不同的行为时,可以使用状态模式。
- 状态切换比较复杂:当状态转换多且复杂时,状态模式可以将状态管理的逻辑封装在具体的状态类中。
- 需要避免条件语句:当对象的行为随着状态变化而变化,如果使用
if-else或switch会导致代码冗长时,状态模式是一个很好的解决方案。
15.6 状态模式 vs 策略模式
- 状态模式:状态模式的状态转换是自动发生的,它内部会管理状态的切换,行为依赖于状态。比如电梯从运行到暂停状态的切换。
- 策略模式:策略模式的行为选择是手动的,由客户端来决定使用哪个策略,通常用于不同算法的选择。
15.7 总结
状态模式通过将对象的状态与行为分离开来,使得对象能够在不同的状态下表现出不同的行为,同时避免了大量条件语句的使用。它使得代码更加灵活且容易扩展,适用于那些状态变化复杂且行为与状态强相关的场景。