🌈 个人主页:danci_
🔥 系列专栏:《设计模式》
💪🏻 制定明确可量化的目标,坚持默默的做事。
🚀 转载自:探索设计模式的魅力:探索发布-订阅模式的深度奥秘-实现高效、解耦的系统通信
探索发布-订阅模式的深度奥秘:实现高效、解耦的系统通信
文章目录
- 一、案例场景🔍
- 1.1 经典的运用场景
- 1.2 一坨坨代码实现😻
- 1.3 痛点
- 二、解决方案🚀
- 2.1 定义
- 2.2 案例分析🧐
- 2.3 模式结构图及说明
- 2.4 使用模式重构示例
- 2.5 重构后解决的问题👍
一、案例场景🔍
1.1 经典的运用场景
发布-订阅模式在软件开发中拥有广泛的应用,它适用于多种场景,帮助开发者构建灵活、可扩展和松耦合的系统。以下是一些经典的应用场景及其在实际项目中的应用价值:👇
- 实时消息系统:
在聊天应用、社交媒体平台或实时数据流处理中,发布-订阅模式允许用户发送消息到一个中央通道,而其他用户可以根据兴趣订阅这些通道以接收实时更新。这降低了发送者和接收者之间的直接依赖,支持高并发和实时通信。 - 事件驱动架构:
在微服务或分布式系统中,服务之间通过事件进行通信。一个服务发布事件(如订单创建、用户注册等),而其他服务订阅这些事件并作出响应。这种异步通信方式提高了系统的响应性和可扩展性。 - 状态更新通知:
在用户界面或后台管理中,当某个状态发生变化时(如数据库记录更新),发布-订阅模式可以确保相关组件或用户得到通知。这有助于实现实时反馈和动态更新。 - 日志和监控:
系统日志和监控工具经常采用发布-订阅模式来收集、聚合和分发日志事件。发布者将日志事件发送到中央收集点,而订阅者可以是各种分析工具或警报系统。 - 物联网(IoT):
在物联网应用中,设备产生的大量数据需要被实时处理和分发。发布-订阅模式允许设备将数据发布到消息代理,而各种服务和应用程序可以订阅这些数据流以进行实时分析或响应。 - 股票交易和金融市场:
在金融应用中,实时数据流(如股票价格变动)对交易者至关重要。发布-订阅模式确保交易者能够订阅他们感兴趣的金融工具,并在价格变动时立即收到通知。
下面我们来实现实时消息系统场景 📄✏️。
1.2 一坨坨代码实现😻
要使用Java实现第一个场景(实时消息系统)而不直接使用发布-订阅设计模式,我们可以使用基础的Java并发和集合类来模拟这个系统。下面是一个简单的示例,展示了如何实现一个简易的实时聊天系统。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList; public class SimpleChatSystem { // 存储所有用户的聊天室 private static final Map<String, List<ChatListener>> chatRooms = new HashMap<>(); // 用于在控制台模拟用户发送消息 public static void main(String[] args) { // 创建两个用户 ChatListener alice = new ChatListener("Alice"); ChatListener bob = new ChatListener("Bob"); // 将用户添加到同一个聊天室 String chatRoomName = "General"; addToChatRoom(chatRoomName, alice); addToChatRoom(chatRoomName, bob); // 模拟Alice发送消息 sendMessage(chatRoomName, "Alice: Hello, Bob!"); // 模拟Bob发送消息 sendMessage(chatRoomName, "Bob: Hi, Alice! How are you?"); } // 将用户添加到聊天室 public static void addToChatRoom(String chatRoomName, ChatListener listener) { chatRooms.computeIfAbsent(chatRoomName, k -> new CopyOnWriteArrayList<>()).add(listener); } // 从聊天室移除用户 public static void removeFromChatRoom(String chatRoomName, ChatListener listener) { chatRooms.getOrDefault(chatRoomName, new ArrayList<>()).remove(listener); } // 发送消息到聊天室 public static void sendMessage(String chatRoomName, String message) { List<ChatListener> listeners = chatRooms.get(chatRoomName); if (listeners != null) { for (ChatListener listener : listeners) { listener.receiveMessage(message); } } } // 简单的聊天监听器,用于接收消息并打印到控制台 public static class ChatListener implements Runnable { private final String name; public ChatListener(String name) { this.name = name; } public void receiveMessage(String message) { System.out.println(name + " received: " + message); } @Override public void run() { // 这个run方法没有实际用途,只是为了让ChatListener实现Runnable接口 // 在实际应用中,ChatListener可能会是一个复杂的类,包含网络连接、消息处理等逻辑 } }
}
在这个示例中,我们没有使用任何设计模式,而是直接使用了Java的基础类。我们创建了一个SimpleChatSystem
类,它包含了一个静态的chatRooms
映射,用于存储聊天室和其中的用户(ChatListener
实例)。ChatListener
是一个简单的接口,它有一个receiveMessage
方法,用于接收并处理消息。
在main方法中,我们创建了两个用户(Alice和Bob),并将它们添加到同一个聊天室。然后,我们模拟Alice和Bob发送消息到聊天室,所有在聊天室中的用户都会接收到这些消息,并将它们打印到控制台。
注:上述实现只是个示例。在实际应用中,你可能需要考虑更多的因素,如网络编程、并发控制、错误处理、用户身份验证等。此外,如果你打算构建一个大规模的实时消息系统,使用专业的消息队列中间件(如RabbitMQ
、Kafka
等)可能是一个更好的选择。
问题描述:
用java实现第一个场景,要求不使用设计模式、编码逻辑思路清晰
问题描述:
上述实现是否有优点?
上述实现确实有一些优点,但也存在一些局限性。以下是对其优点的分析:👇
- 简单性:
✨ 这个实现非常直观和简单,易于理解和实现。它使用基础的Java类和并发控制来模拟实时消息系统,不需要掌握复杂的设计模式或框架。 - 演示目的:
✨ 作为一个示例,它很好地演示了如何使用Java的集合和并发特性来构建一个简单的实时通信系统。这对于初学者理解基本概念很有帮助。 - 静态模拟:
✨ 虽然它只在控制台中模拟用户之间的通信,但它可以作为一个起点,用于构建更复杂的系统。例如,可以将ChatListener
类扩展为包含网络连接、GUI界面或与其他服务集成的功能。 - 无额外依赖:
✨ 该实现不依赖于任何外部库或框架,这意味着它可以在任何标准的Java环境中运行,无需担心依赖管理或版本兼容性问题。
这个简单实现也有一些局限性,比如缺乏真正的异步通信、可扩展性限制、没有错误处理等。在实际项目中,通常会使用更复杂、更健壮的解决方案来处理这些问题。
1.3 痛点
然而,没有复杂的设计下上述实现确实存在一些缺点,尤其是在面对更复杂、大规模或生产级别的实时消息系统需求时。以下是对其缺点的分析:👇
-
缺乏真正的异步通信:
✨ 虽然示例中的ChatListener
可以接收消息,但整个系统并不是真正异步的。在真实世界的实时消息系统中,消息通常是通过异步方式发送和接收的,以提高响应性和吞吐量。 -
可扩展性限制:
✨ 由于使用了静态的映射来存储聊天室和用户,这个实现在处理大量用户或聊天室时可能会遇到性能瓶颈。此外,它也没有提供动态添加或删除聊天室的功能。 -
没有错误处理:
✨ 示例代码中没有包含任何错误处理逻辑。在实际应用中,网络中断、用户断开连接、消息格式错误等情况都需要妥善处理。 -
用户界面和交互性有限:
✨ 该实现仅在控制台打印接收到的消息,缺乏一个真正的用户界面或与其他服务的集成。这限制了其在现实世界应用中的实用性。 -
线程安全性问题:
✨ 虽然使用了CopyOnWriteArrayList
来提供一定程度的线程安全,但在更复杂的多线程环境中,这可能不足以保证系统的稳定性和数据的一致性。例如,当同时添加和删除用户时,可能会出现竞态条件。 -
单一的消息传递方式:
✨ 示例中只有一种简单的消息传递方式,即广播到整个聊天室。在实际应用中,可能需要更复杂的消息路由和传递策略,如点对点消息、组播、主题订阅等。 -
缺乏持久化:
✨ 消息在发送后被立即消费,没有持久化机制来存储历史消息或支持离线消息传递。 -
缺乏认证和授权:
✨ 示例中没有实现用户认证和授权机制,这在任何需要安全性的应用中都是必需的。
违反的设计原则(问题)下面逐一分析:👇
-
单一职责原则(SRP):
✨SimpleChatSystem
类负责了多个职责,包括管理聊天室、添加/移除用户以及发送消息。这可能导致类变得难以维护和扩展。更好的做法是将这些职责拆分到不同的类中,例如,可以有一个专门的ChatRoomManager
d 类来管理聊天室和用户,以及一个MessageSender
类来负责发送消息。 -
开闭原则(OCP):
✨ 当前的实现不够灵活,无法在不修改现有代码的情况下添加新的功能或行为。例如,如果想要添加新的聊天室类型或消息传递方式,可能需要修改SimpleChatSystem
类的代码。这违背了开闭原则,即软件实体应该对扩展开放,对修改关闭。 -
里氏替换原则(LSP):
✨ 虽然在这个简单的示例中没有明显的违反里氏替换原则的情况,但如果ChatListener
接口被扩展或修改,可能会导致子类无法正确替换父类或接口的问题。这通常发生在当子类没有遵守父类的约定或行为时。 -
接口隔离原则(ISP):
✨ChatListener
接口可能违反了接口隔离原则,因为它可能包含了不需要的方法(在这个简单示例中只有一个方法,但在更复杂的情况下可能会有更多)。如果接口过于庞大或包含不相关的方法,那么实现这个接口的类可能会被迫实现它们不需要的方法。 -
依赖倒置原则(DIP):
✨ 当前的实现中,高层模块(如SimpleChatSystem
)直接依赖于低层模块(如ChatListener),这可能导致代码的耦合度过高。更好的做法是使用抽象(如接口或抽象类)来定义依赖关系,这样高层模块就可以依赖于抽象而不是具体的实现。 -
迪米特法则(LoD)或最少知识原则:
✨ 在这个实现中,SimpleChatSystem
类直接访问和操作了ChatListener 实例的内部状态(通过调用receiveMessage
方法)。这违反了迪米特法则,即一个对象应该对其他对象保持最少的了解。更好的做法是通过定义清晰的接口和委托关系来减少类之间的直接依赖。
注:设计原则是为了指导软件设计而提出的,旨在提高代码的可维护性、可扩展性和可重用性。在实际项目中,根据具体的需求和上下文,可能需要权衡这些原则的应用。不过,在构建更大规模或更复杂的系统时,遵循这些原则通常是有益的。
二、解决方案🚀
2.1 定义
发布订阅模式:一种消息传递模型,发送者不直接将消息发送给接收者,而是发送到中间层。 |
接收者可以订阅这些消息,并在消息发布时接收到通知。这种模式的核心目的是实现发布者与订阅者之间的解耦,允许它们独立地扩展和修改,同时提供一种灵活的消息通信机制。通过发布订阅模式,系统可以更加灵活、可扩展和可维护。
2.2 案例分析🧐
2.3 模式结构图及说明
- 发布者(Publisher):
它是消息或者说主题的生产者,负责产生并发布消息到发布订阅管理器。发布者不需要知道或关心消息会被哪些订阅者接收,它只需要关注消息的发布。 - 订阅者(Subscriber)
它是订阅消息或主题的消费者,负责从发布订阅管理器接收并处理感兴趣的消息。订阅者需要提前向发布订阅管理器订阅自己感兴趣的主题,当发布者发布相关主题的消息时,发布订阅管理器会将消息推送给所有订阅了该主题的订阅者。 - 发布订阅管理器:
它是整个发布订阅模式的核心,负责接收并存储发布者发布的消息,同时管理订阅者的订阅信息。当发布者发布消息时,发布订阅管理器会根据订阅信息将消息推送给相应的订阅者。发布订阅管理器实现了发布者与订阅者之间的解耦,使得它们可以独立地扩展和修改。
发布订阅模式的功能是实现消息的发布与订阅,提供了一种灵活的消息通信机制,使得消息的发送者和接收者可以解耦,提高了系统的灵活性和可扩展性。同时,发布订阅模式还支持一对多、多对多的消息通信,可以满足复杂场景下的消息通信需求。
2.4 使用模式重构示例
为了解决上述实现中的缺点,并采用发布订阅模式重构实时消息系统,我们需要对原有的代码结构进行调整。以下是一个简化的重构示例:👇
- 首先,定义消息发布的接口和消息订阅的接口:
// 定义消息类型
public class Message { private String sender; private String content; // 构造方法、getters和setters略...
} // 消息发布接口
public interface MessagePublisher { void publish(Message message);
} // 消息订阅接口
public interface MessageSubscriber { void onMessage(Message message);
} // 消息订阅管理器,管理订阅者和消息的分发
public class MessageSubscriptionManager implements MessagePublisher { private List<MessageSubscriber> subscribers = new CopyOnWriteArrayList<>(); @Override public void publish(Message message) { for (MessageSubscriber subscriber : subscribers) { subscriber.onMessage(message); } } public void subscribe(MessageSubscriber subscriber) { subscribers.add(subscriber); } public void unsubscribe(MessageSubscriber subscriber) { subscribers.remove(subscriber); }
}
- 接下来,我们定义ChatRoom类,它将包含
MessageSubscriptionManager
实例来管理订阅和消息发布:
public class ChatRoom { private String name; private MessageSubscriptionManager subscriptionManager = new MessageSubscriptionManager(); public ChatRoom(String name) { this.name = name; } public void sendMessage(String sender, String content) { Message message = new Message(sender, content); subscriptionManager.publish(message); } public void subscribe(MessageSubscriber subscriber) { subscriptionManager.subscribe(subscriber); } public void unsubscribe(MessageSubscriber subscriber) { subscriptionManager.unsubscribe(subscriber); }
}
- 最后,实现一个简单的
MessageSubscriber
示例,它可以接收并处理来自ChatRoom
的消息:
public class ChatUser implements MessageSubscriber { private String username; public ChatUser(String username) { this.username = username; } @Override public void onMessage(Message message) { System.out.println(username + " received message from " + message.getSender() + ": " + message.getContent()); } // getters和setters略...
}
- 现在,创建
ChatRoom
和ChatUser
的实例,并将用户订阅到聊天室中,以接收实时消息:
public class RealTimeMessagingSystem { public static void main(String[] args) { ChatRoom chatRoom = new ChatRoom("General"); ChatUser user1 = new ChatUser("Alice"); ChatUser user2 = new ChatUser("Bob"); chatRoom.subscribe(user1); chatRoom.subscribe(user2); // 发送消息到聊天室 chatRoom.sendMessage("Server", "Hello, everyone!"); // ... 可以添加更多逻辑,比如用户之间的交互,或用户发送消息等。 // 当某个用户需要离开时,取消订阅 chatRoom.unsubscribe(user1); }
}
<这个重构示例遵循了发布订阅模式,允许用户订阅聊天室的消息,并通过聊天室发送消息给所有订阅者。此外,通过 MessageSubscriptionManager
来管理订阅者和消息的分发,使得系统更加解耦、灵活,并支持多个用户之间的实时交互。这种模式使得系统易于扩展,能够适应不同数量和类型的订阅者和消息类型。
2.5 重构后解决的问题👍
优点
上述使用发布订阅模式的实现解决了以下已知缺点:👇
-
解耦:
✨ 在原始的实现中,发送者和接收者之间可能存在直接的依赖关系,这限制了系统的灵活性和可维护性。通过使用发布订阅模式,我们引入了中间层(MessageSubscriptionManager
),使得发送者(发布者)和接收者(订阅者)之间解耦。这意味着发送者不需要知道或关心消息被哪些接收者处理,同样,接收者也不需要知道消息来自哪个发送者。 -
可扩展性:
✨ 发布订阅模式允许系统更容易地扩展。无论是增加新的订阅者还是处理更多的消息类型,都不需要对现有代码进行大量修改。在重构后的代码中,添加新的订阅者只需要调用subscribe
方法,而移除订阅者则调用unsubscribe
方法。 -
实时性与异步处理:
✨ 发布订阅模式天然支持异步消息传递。在重构后的系统中,消息的发送是异步的,发送者不需要等待接收者的响应。这提高了系统的响应性和吞吐量,使得它能够更好地处理实时消息。 -
容错性与可靠性:
✨ 虽然上述示例代码没有直接展示容错性机制,但发布订阅模式为实现容错性和可靠性提供了基础。例如,中间层可以设计为支持消息的持久化存储,以便在订阅者暂时不可用时保留消息。此外,通过监控和重试机制,可以确保消息最终被传递给订阅者。 -
灵活性:
✨ 发布订阅模式使得系统更加灵活,能够适应不同的业务需求。例如,订阅者可以选择性地订阅感兴趣的消息类型,或者根据消息的内容执行不同的操作。这种灵活性使得系统更容易适应变化,降低了维护成本。
遵循的设计原则
上述使用发布-订阅模式重构示例后的代码遵循了以下设计原则:👇
-
单一职责原则(Single Responsibility Principle, SRP):
✈️ 每个类(如Message
,MessagePublisher
,MessageSubscriber
,MessageSubscriptionManager
,ChatRoom
,ChatUser
)都只负责一项功能或职责。例如,Message
类只负责封装消息内容,而MessageSubscriptionManager
只负责管理订阅和消息的分发。 -
开闭原则(Open-Closed Principle, OCP):
✈️ 系统应该对扩展开放,对修改关闭。在上述实现中,如果需要添加新的消息类型或订阅者,不需要修改现有的代码,只需扩展相应的接口或类即可。例如,可以通过实现MessageSubscriber接口来创建新的订阅者。 -
里氏替换原则(Liskov Substitution Principle, LSP):
✈️ 子类必须能够替换其父类出现在任何地方,并且不会引入任何错误或异常。虽然上述示例中没有直接展示继承关系,但如果有子类继承自MessageSubscriber
,那么它们应该能够无缝地替换父类实例,而不需要修改MessageSubscriptionManager
或ChatRoom
的代码。 -
接口隔离原则(Interface Segregation Principle, ISP):
✈️ 客户端不应该依赖于它不使用的接口。在上述实现中,MessagePublisher
和MessageSubscriber
接口都是小而专注的,只定义了与发布和订阅消息相关的操作,没有包含任何不相关的方法。 -
依赖倒置原则(Dependency Inversion Principle, DIP):
✈️ 高层模块不应该依赖于低层模块,它们都应该依赖于抽象。在上述实现中,ChatRoom
类依赖于抽象的MessagePublisher
接口(通过MessageSubscriptionManager
实现),而不是具体的实现类。这意味着如果需要更换消息发布的管理方式,只需要提供一个新的MessagePublisher
实现,而不需要修改ChatRoom的代码。 -
迪米特法则(Law of Demeter, LoD)或最少知识原则(Least Knowledge Principle, LKP)::
✈️ 一个对象应该对其他对象保持最少的了解。在上述实现中,ChatRoom
只与MessageSubscriptionManager
交互,而不直接与订阅者交互。同样,订阅者(如ChatUser
)也只与接收到的消息交互,而不需要知道消息是如何发布或管理的。
缺点
上述实现(假设是指使用发布订阅模式的实现)虽然解决了许多已知的问题,并遵循了良好的设计原则,但仍然可能存在一些潜在的缺点或考虑因素:👇
-
内存消耗:
💡 如果订阅者数量众多或者消息产生频率非常高,系统可能会面临内存压力。因为每个订阅者都可能保留一份消息副本,或者中间层(如MessageSubscriptionManager
)需要维护大量的订阅关系和待处理消息。 -
消息处理顺序:
💡 发布订阅模式通常不保证消息的处理顺序。如果有多个订阅者同时处理同一条消息,或者订阅者在不同线程/进程中运行,那么消息的处理顺序可能会变得不可预测。 -
消息丢失或重复:
💡 在分布式系统或存在网络延迟/故障的环境中,发布订阅模式可能会面临消息丢失或重复的问题。需要额外的机制来确保消息的可靠传输和幂等性处理。 -
复杂性增加:
💡 引入发布订阅模式可能会增加系统的复杂性。需要管理额外的组件(如消息代理、订阅管理器等),并处理它们之间的交互和依赖关系。此外,调试和维护一个复杂的消息传递系统也可能更加困难。 -
性能瓶颈:
💡 如果所有消息都通过一个集中的MessageSubscriptionManager
进行处理和分发,那么它可能会成为系统的性能瓶颈。特别是在高并发或大数据量的场景下,单点压力可能会很大。 -
安全性考虑:
💡 发布订阅模式可能暴露更多的攻击面。例如,未经授权的订阅者可能会接收到敏感信息,或者恶意订阅者可能会发送伪造的消息来干扰系统的正常运行。