🌈 个人主页:danci_
🔥 系列专栏:《设计模式》
💪🏻 制定明确可量化的目标,并且坚持默默的做事。
探索设计模式的魅力:揭秘中介者模式优雅地管理对象间的沟通
文章目录
一、案例场景🔍
1.1 经典的运用场景
中介者模式在软件开发中的多种应用场景。以下是中介者模式在分布式系统、微服务架构和聊天室应用等场景中的经典应用及其优势:👇
- 分布式系统:
在分布式系统中,各个节点(或服务)之间需要进行通信和协作。中介者模式可以应用于此,作为中央协调器,负责管理和调度各个节点之间的通信。
降低耦合度:通过中介者,各个节点可以只与中介者通信,而无需了解其他节点的具体实现和位置,从而降低了节点之间的耦合度。
提高可扩展性:新节点加入系统时,只需与中介者进行交互,无需修改其他节点的代码,提高了系统的可扩展性。
简化复杂性:中介者可以封装复杂的通信逻辑,使得各个节点只需关注自己的业务逻辑,简化了系统的复杂性。
1.2 一坨坨代码实现😻
不使用中介者模式的情况下实现分布式系统中的节点通信,可以采用直接通信的方式,即每个节点都保存其他节点的引用或地址,并直接与其他节点进行通信。这种方法虽然简单,但随着节点数量的增加,节点之间的连接数将呈指数级增长,导致系统的复杂性和维护成本增加。
这里提供一个简化的示例,其中只有两个节点(NodeA 和 NodeB)进行通信。在实际应用中,您可能需要使用更复杂的机制,如消息队列、RPC 框架或分布式事件总线来处理节点之间的通信。以下是简单的 Java 示例:👇
// 定义节点接口
interface Node {
void sendMessage(String message, Node recipient);
void receiveMessage(String message);
}
// 实现节点A
class NodeA implements Node {
private NodeB nodeB; // 直接持有NodeB的引用
public NodeA(NodeB nodeB) {
this.nodeB = nodeB;
}
@Override
public void sendMessage(String message, Node recipient) {
if (recipient instanceof NodeB) {
nodeB.receiveMessage(message); // 直接发送给NodeB
} else {
System.out.println("Unsupported recipient type");
}
}
@Override
public void receiveMessage(String message) {
System.out.println("NodeA received: " + message);
}
}
// 实现节点B
class NodeB implements Node {
@Override
public void sendMessage(String message, Node recipient) {
// 在这个简化的示例中,NodeB不发送消息给其他节点
System.out.println("NodeB is not configured to send messages");
}
@Override
public void receiveMessage(String message) {
System.out.println("NodeB received: " + message);
}
}
// 主程序入口
public class DistributedSystemExample {
public static void main(String[] args) {
NodeB nodeB = new NodeB(); // 创建NodeB实例
NodeA nodeA = new NodeA(nodeB); // 创建NodeA实例,并将NodeB的引用传递给它
nodeA.sendMessage("Hello from NodeA", nodeB); // NodeA发送消息给NodeB
}
}
在这个示例中,我们定义了一个 Node 接口,其中包含 sendMessage 和 receiveMessage 方法。然后,我们实现了两个节点类 NodeA 和 NodeB,它们分别实现了 Node 接口。NodeA 持有一个对 NodeB 的引用,并可以直接向其发送消息。然而,这个示例非常简单且有限,因为它只支持两个节点之间的单向通信。
在实际应用中,可能需要考虑使用更复杂的通信机制来处理多个节点之间的双向通信、错误处理、异步通信等问题。此外,随着节点数量的增加,可能需要引入中介者模式或其他设计模式来降低系统的复杂性并提高可维护性。
虽然上述实现没有使用设计模式,但也体现出了如下优点:👇
- 简单性:
🚀 代码实现非常直接和简单,容易理解。对于初学者或者小型项目来说,这种简单性可能是一个优点,因为它减少了复杂性并使得代码易于维护。 - 直接通信:
🚀 由于 NodeA 直接持有 NodeB 的引用,因此在两个节点之间传输数据时没有任何中间层,这可以减少延迟和额外的网络开销(在分布式系统的上下文中,这通常不是优点,但在某些特定场景,如紧密集成的组件之间,这可能是可接受的)。 - 无需额外依赖:
🚀 实现没有使用任何外部库或框架,这意味着它可以在没有这些依赖项的环境中运行,减少了部署和管理的复杂性。 - 明确的依赖关系:
🚀 NodeA 对 NodeB 的依赖关系是明确的,这有助于在设计和分析系统时理解组件之间的交互。 - 适用于演示和教学:
🚀 作为一个教学示例,它很好地展示了如何在没有使用设计模式的情况下实现基本的节点间通信。
1.3 痛点
然而,没有复杂的设计下体现上述优点的同时也伴随着一些潜在的缺点,比如代码的耦合性、通用性和可扩展性可能会受到限制。对于更大或更复杂的项目,可能需要考虑使用设计模式和其他高级技术来改善代码的结构和质量。
缺点(问题)👇逐一分析:
- 紧耦合:
😉 NodeA 直接依赖于 NodeB,这导致两者之间的紧密耦合。如果 NodeB 的实现发生变化,或者需要引入新的节点类型,NodeA 的代码可能也需要相应修改。 - 扩展性差:
😉 这个简单的实现不支持多于两个节点的系统。每增加一个节点,都需要在现有的节点类中增加对新节点的引用和处理逻辑,这会导致代码迅速变得复杂且难以维护。 - 单向通信:
😉 在这个实现中,只有 NodeA 能够发送消息给 NodeB,而 NodeB 没有办法回应或者发送消息给其他节点。这在真实的分布式系统中是不够用的。 - 缺乏通用性:
😉 sendMessage 方法中的类型检查限制了只能发送消息给特定类型的节点。这种方法不够灵活,也不支持动态地添加或移除节点。 - 缺乏错误处理:
😉 在发送消息时,没有考虑到可能的错误情况,比如网络故障、接收节点不可用等。 - 同步阻塞调用:
😉 sendMessage 方法是同步的,这意味着发送节点在消息被接收之前会一直等待。在实际的分布式系统中,通常更倾向于使用异步通信来避免阻塞。 - 单点故障:
😉 由于 NodeA 直接依赖于 NodeB,如果 NodeB 出现故障,NodeA 将无法正常工作。
当考虑到分布式系统的复杂性和可扩展性需求时。以下是一些被违反的设计原则(问题)👇逐一分析:
- 开闭原则(Open/Closed Principle):
💪 软件实体(类、模块、函数等)应当对扩展开放,对修改关闭。在上述实现中,如果我们需要增加新的节点类型或者改变节点之间的通信方式,我们可能需要修改现有的 NodeA 和 NodeB 类的代码,这违背了开闭原则。 - 依赖倒置原则(Dependency Inversion Principle):
💪 高层模块不应该依赖于低层模块,它们都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。在上述实现中,NodeA 直接依赖于 NodeB 的具体实现,而不是依赖于一个更抽象的接口或基类,这违背了依赖倒置原则。 - 单一职责原则(Single Responsibility Principle):
💪 一个类应该只有一个引起变化的原因。在上述实现中,NodeA 既负责自己的业务逻辑,又负责与 NodeB 的通信逻辑,这可能使得 NodeA 的职责过于复杂,违背了单一职责原则。 - 里氏替换原则(Liskov Substitution Principle):
💪 在软件系统中,一个可以接受基类对象的地方,必然可以接受一个子类对象,而不会出现任何异常。虽然这个原则在上述实现中没有直接被违背(因为没有涉及到继承),但是如果我们尝试将 NodeA 和 NodeB 抽象为一个基类,并创建子类来扩展它们,可能会遇到问题,因为子类可能需要覆盖或修改基类的方法以实现特定的通信逻辑。 - 接口隔离原则(Interface Segregation Principle):
💪 客户端不应该依赖它不需要的接口;一个类对另一个类的依赖性应当是最小的。在上述实现中,没有明确地定义接口,但是如果我们尝试引入接口来抽象节点的行为,可能会发现接口包含了太多不必要的方法,或者不同的节点类型需要实现不同的接口,这违背了接口隔离原则。 - 迪米特法则(Law of Demeter)或最少知识原则(Least Knowledge Principle):
💪 一个对象应该对其他对象保持最少的了解。在上述实现中,NodeA 直接与 NodeB 交互,并且了解 NodeB 的具体实现细节(例如,它知道消息是发送给 NodeB 的),这违背了迪米特法则。
二、解决方案🚀
为了解决上述代码中的缺点,我们可以使用中介者模式来重构代码。中介者模式引入了一个中介者对象,用于封装节点之间的通信逻辑,从而降低节点之间的耦合度并提高系统的可扩展性。
2.1 定义
简化对象间交互,通过中央协调者管理通信,降低耦合度。 |
2.2 案例分析🧐
2.3 中介者模式结构图及说明
-
Mediator:
抽象中介者定义了一个接口,用于同事对象之间进行通信。它通常声明了一些方法,这些方法由具体中介者实现,用于接收和发送消息。
在下面👇的重构代码中,并没有直接定义一个抽象的Mediator接口,而是直接实现了一个具体的中介者ConcreteMediator。但在更一般的实现中,可能会有一个抽象的Mediator接口,用于定义中介者的基本行为。 -
ConcreteMediator:
具体中介者是抽象中介者的子类,通过协调各个同事对象来实现协作行为。它维持了对各个同事对象的引用,并在需要时将消息从一个同事对象转发给另一个。
在下面👇的重构代码中,ConcreteMediator类扮演了这个角色。它实现了消息的分发逻辑,通过sendMessage方法将消息从发送者传递给其他接收者。 -
Colleague:
抽象同事类定义了各个同事对象共有的方法,并声明了一些抽象方法供子类实现。它通常维持了一个对抽象中介者的引用,以便子类可以通过这个引用来与中介者通信。
在下面👇的重构代码中,并没有明确定义一个抽象的Colleague类,但Node接口扮演了类似的角色。它定义了同事对象的基本行为,如sendMessage和receiveMessage。 -
ConcreteColleague:
具体同事类是抽象同事类的子类(或实现了同事接口的具体类)。每一个同事对象在需要和其他同事对象进行通信时,先与中介者通信,通过中介者间接完成与其他同事的通信。
在下面👇的重构代码中,NodeA和NodeB类就是具体同事类的例子。它们实现了sendMessage方法,但在这个方法中并不是直接与其他节点通信,而是将消息发送给中介者(mediator.sendMessage(message, this)),由中介者负责消息的进一步分发。
结构总结
在中介者模式中,各个组件通过中介者进行间接通信,降低了它们之间的直接依赖。这种模式有助于减少类间的耦合度,提高系统的可扩展性和可维护性。同时,它也需要仔细设计和规划,以确保中介者不会变得过于庞大和复杂。
需要注意的是,下面👇的重构代码示例并没有完全遵循中介者模式的经典结构(如没有明确定义抽象中介者和抽象同事类),但它仍然体现了中介者模式的核心思想:通过一个中介对象来封装和协调一系列对象之间的交互。在实际应用中,可以根据系统的具体需求和设计考虑来选择适当的实现方式。
2.4 使用中介者模式重构示例
重构步骤
为解决传统方式带来的缺点,如高耦合度、低可扩展性、代码难以维护等,我们可以使用中介者模式来重构代码。以下是使用中介者模式重构的核心步骤:👇
-
定义中介者接口:
首先,定义一个中介者的接口,这个接口将声明用于注册同事对象、转发请求等方法。中介者将负责协调各个同事对象之间的交互。 -
实现具体中介者:
接下来,实现中介者的具体类,这个类将实现中介者接口中声明的方法。具体中介者需要维护对各个同事对象的引用,并协调它们之间的交互。 -
定义同事接口(可选):
如果同事对象之间有共同的行为或状态,可以定义一个同事接口。这个接口将声明同事对象需要实现的方法,以便中介者与之交互。 -
实现具体同事类:
实现同事接口的具体类。这些类将代表系统中的各个对象,它们将通过中介者来进行交互。在每个同事类中,将中介者作为成员变量,以便在需要时与中介者通信。 -
在客户端代码中使用中介者和同事对象:
在客户端代码中,创建中介者和同事对象的实例,并将同事对象注册到中介者中。然后,通过调用中介者的方法来触发同事对象之间的交互。 -
测试与调试:
重构完成后,进行充分的测试以确保系统的功能没有受到影响,并且新的结构确实解决了之前存在的问题。
重构示例
以下是使用中介者模式重构后的代码示例:👇
- 定义节点接口
public interface Node {
void sendMessage(String message);
void receiveMessage(String message);
}
- 实现节点
// 实现节点A
public class NodeA implements Node {
private Mediator mediator;
public NodeA(Mediator mediator) {
this.mediator = mediator;
}
@Override
public void sendMessage(String message) {
mediator.sendMessage(message, this);
}
@Override
public void receiveMessage(String message) {
System.out.println("NodeA received: " + message);
}
}
// 实现节点B
public class NodeB implements Node {
private Mediator mediator;
public NodeB(Mediator mediator) {
this.mediator = mediator;
}
@Override
public void sendMessage(String message) {
mediator.sendMessage(message, this);
}
@Override
public void receiveMessage(String message) {
System.out.println("NodeB received: " + message);
}
}
- 定义中介者接口
public interface Mediator {
void sendMessage(String message, Node sender);
void registerNode(Node node);
}
- 实现中介者
public class ConcreteMediator implements Mediator {
private List<Node> nodes = new ArrayList<>();
@Override
public void sendMessage(String message, Node sender) {
for (Node node : nodes) {
if (!node.equals(sender)) { // 不将消息发送给自己
node.receiveMessage(message);
}
}
}
@Override
public void registerNode(Node node) {
nodes.add(node);
}
}
- 主程序入口
public class DistributedSystemExample {
public static void main(String[] args) {
// 创建中介者
Mediator mediator = new ConcreteMediator();
// 创建节点并注册到中介者
NodeA nodeA = new NodeA(mediator);
NodeB nodeB = new NodeB(mediator);
mediator.registerNode(nodeA);
mediator.registerNode(nodeB);
// 节点A发送消息,中介者负责将消息传递给其他节点
nodeA.sendMessage("Hello from NodeA");
}
}
在这个重构后的代码中,我们引入了一个 Mediator 接口和一个具体的实现类 ConcreteMediator。ConcreteMediator 维护了一个节点列表,并提供了注册节点和发送消息的方法。当节点需要发送消息时,它调用中介者的 sendMessage 方法,并传入消息内容和发送者自身。中介者负责将消息传递给其他节点。
这样,节点之间不再直接通信,而是通过中介者进行间接通信。这种设计降低了节点之间的耦合度,提高了系统的可扩展性和可维护性。同时,由于中介者负责消息的分发,我们可以很容易地添加新的节点类型或修改节点之间的通信逻辑,而无需修改现有节点的代码。
2.5 重构后解决的问题👍
优点
上述使用中介者模式重构后的代码解决了以下已知缺点:👇
- 降低了耦合度:
✨ 原始实现中,节点之间直接相互引用并通信,导致高度耦合。重构后,节点不再直接通信,而是通过中介者进行间接通信,降低了节点之间的耦合度。这使得节点可以更加独立地演变和扩展,而不会对其他节点产生不必要的影响。 - 提高了可扩展性:
✨ 在原始实现中,添加新的节点类型或修改节点之间的通信逻辑可能需要修改多个节点的代码。然而,在重构后的代码中,由于引入了中介者,这些变化可以通过修改中介者或添加新的中介者实现来集中处理,从而提高了系统的可扩展性。 - 遵循了开闭原则:
✨ 重构后的代码更加符合开闭原则。我们可以通过扩展中介者或节点的实现来引入新的功能,而不需要修改已有的代码。这有助于保持系统的稳定性和可维护性。 - 简化了节点类的职责:
✨ 原始实现中,节点类既负责自己的业务逻辑,又负责与其他节点的通信逻辑。这违反了单一职责原则。重构后,节点的通信逻辑被封装在中介者中,节点类只需要关注自己的业务逻辑,从而简化了节点类的职责。 - 提高了代码的可读性和可维护性:
✨ 通过引入中介者模式,代码的结构变得更加清晰和易于理解。中介者封装了节点之间的通信逻辑,使得节点之间的交互更加明确和可预测。这有助于提高代码的可读性和可维护性
遵循的设计原则
上述使用中介者模式重构后的代码解决了以下已知缺点:👇
- 迪米特法则(Law of Demeter)或最少知识原则(Least Knowledge Principle):
✈️ 在重构后的代码中,各个节点(如NodeA和NodeB)只与中介者(Mediator)通信,而不直接与其他节点交互。这减少了类之间的直接依赖,使得系统更加模块化,每个部分只关心自己的直接交互对象,即中介者,而不需要了解系统的其他部分。 - 单一职责原则(Single Responsibility Principle):
✈️ 通过引入中介者,节点的职责被分离。节点现在只负责它们自己的特定功能或行为,而通信的逻辑被移交给中介者来处理。这使得代码更加清晰,每个类或模块只做一件事情。 - 开闭原则(Open/Closed Principle):
✈️ 虽然在这个特定的例子中可能不太明显,但中介者模式通常有助于实现开闭原则。这意味着系统应该对扩展开放,对修改关闭。通过修改中介者或添加新的中介者实现,可以引入新的节点类型或修改节点之间的通信方式,而不需要修改已有的节点代码。 - 依赖倒置原则(Dependency Inversion Principle):
✈️ 在重构后的代码中,高层模块(节点)依赖于抽象(中介者接口),而不是具体实现(具体的中介者类)。这有助于减少类之间的耦合,使得系统更加灵活和可扩展。需要注意的是,这个原则在这个例子中的体现可能不是特别明显,因为只有一个具体的中介者实现被使用。但在更大的系统中,可能会有多个不同的中介者实现,节点将依赖于中介者的抽象接口而不是具体实现。
缺点
上述使用中介者模式重构后的实现虽然解决了许多设计上的问题,并带来了诸多好处,但仍然可能存在一些潜在的缺点或局限性:👇
- 中介者可能变得庞大且复杂:
💡 随着系统的发展,中介者可能需要处理越来越多的交互逻辑。这可能导致中介者类变得庞大且难以维护。如果中介者的职责过重,它本身可能成为一个单点故障或瓶颈。 - 减少了直接交互的明确性:
💡 由于节点之间不再直接通信,而是通过中介者间接通信,这可能会减少代码的直接交互明确性。开发者需要查看中介者的实现来理解节点之间的通信方式,这可能会增加理解系统的难度。 - 可能的性能开销:
💡 中介者模式的引入可能会带来一定的性能开销,因为所有的消息传递都需要通过中介者进行。在高性能要求的系统中,这可能成为一个考虑因素。 - 动态行为的限制:
💡 如果系统中的节点需要在运行时动态地改变它们之间的交互方式,中介者模式可能会引入一些限制。因为所有的交互逻辑都被封装在中介者中,所以动态行为的改变可能需要修改中介者的实现。 - 过多的中介者:
💡 在复杂的系统中,可能需要多个中介者来处理不同类型的交互。这可能会导致系统中存在大量的中介者,增加了系统的复杂性。
转载文章只截取部分内容
详请跳转原文地址:https://blog.csdn.net/danci_/article/details/136367657