中介模式与享元模式
中介模式
定义一个中介对象来封装一系列对象之间的交互关系。中介者使各个对象之间不需要显式地相互引用,从而使耦合性降低,而且可以独立地改变它们之间的交互行为。它是一种对象行为型模式。
优点:减少类间依赖,把原有的一对多的依赖变成了一对一的依赖。降低了类间耦合
缺点:中介者会膨胀很大,而且逻辑比较复杂、
组成部分:
1. 抽象中介类(Mediator): 抽象中介者是中介者的抽象类,它提供了同事对象注册与转发同事对象信息的抽象方法,用于各个同事类之间的通信。一般包括一个或几个抽象的事件方法,并由子类去实现。
2.中介者实现类(Concrete Mediator): 即具体中介者。继承抽象中介者,并且实现抽象中介者中定义的事件方法。从一个同事类接收消息,然后通过消息影响其他同时类。因为它管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
spring中应用到中介模式场景:
1. 事件机制:
Spring 的事件机制就是一种中介模式的应用。在 Spring 中,可以定义自定义事件和事件监听器,通过 ApplicationContext(应用上下文)作为中介来触发事件和传递事件。这样,各个组件之间就可以通过事件进行通信,而不需要直接相互依赖。例如,当某个特定事件发生时,可以触发相应的事件,而不需要在事件源和事件监听器之间直接引入依赖关系。
2. Spring MVC
在 Spring MVC 中,控制器(Controller)可以看作是中介者,它负责接收请求并将其转发给相应的处理器(Handler)进行处理。通过控制器的中介,实现了请求和处理器之间的解耦。
3. 依赖注入:
过依赖注入,Spring 容器充当了中介者的角色,自动将组件之间的依赖关系注入到相应的对象中,从而解耦了组件之间的关系。这样,组件不需要自己创建或查找依赖的对象,而是由容器负责管理和注入依赖。
4. AOP(切面编程)
在 Spring AOP 中,切面(Aspect)是中介者,它将横切关注点(例如日志、安全性等)从主业务逻辑中解耦出来。切面可以被多个不同的类或模块使用,从而实现了横切关注点的复用和集中管理。
使用中介模式来实现一个中介者与多个日志框架交互可以帮助我们将日志记录逻辑与具体的日志框架实现解耦,使得我们可以方便地切换不同的日志框架,而不需要修改其他代码。以下是一个简单示例,展示如何实现这样的中介者与多个日志框架交互:
假设我们有三种不同的日志框架:Log4j、Java Util Logging 和 Logback。我们的目标是创建一个中介者 LogManager,它充当中介,与这三种日志框架进行交互,将日志消息传递给相应的日志框架。
首先,我们定义一个简单的日志记录器接口 Logger,以及三种不同的日志框架实现:
public interface Logger { void log(String level, String message); } public class Log4jLogger implements Logger { @Override public void log(String level, String message) { // Log4j 日志输出逻辑 System.out.println("Log4j: [" + level + "] " + message); } } public class JavaUtilLogger implements Logger { @Override public void log(String level, String message) { // Java Util Logging 日志输出逻辑 System.out.println("Java Util Logging: [" + level + "] " + message); } } public class LogbackLogger implements Logger { @Override public void log(String level, String message) { // Logback 日志输出逻辑 System.out.println("Logback: [" + level + "] " + message); } }
定义 LogManager 作为中介者,并在其中引入上述日志记录器接口:
public class LogManager { private Logger log4jLogger; private Logger javaUtilLogger; private Logger logbackLogger; public LogManager() { this.log4jLogger = new Log4jLogger(); this.javaUtilLogger = new JavaUtilLogger(); this.logbackLogger = new LogbackLogger(); } public void log(String framework, String level, String message) { switch (framework) { case "Log4j": log4jLogger.log(level, message); break; case "JavaUtil": javaUtilLogger.log(level, message); break; case "Logback": logbackLogger.log(level, message); break; default: System.out.println("Unknown logging framework: " + framework); } } }
在应用程序中使用 LogManager 来记录日志,而 LogManager 会根据传入的日志框架名称调用相应的日志记录器,实现了中介者与多个日志框架的交互。
public class Main { public static void main(String[] args) { LogManager logManager = new LogManager(); logManager.log("Log4j", "INFO", "This is a Log4j message."); logManager.log("JavaUtil", "ERROR", "This is a Java Util Logging error message."); logManager.log("Logback", "WARN", "This is a Logback warning message."); } }
也可以通过配置文件的形式来定义选择的日志框架。当日志框架过期或存在漏洞的时候,可以修改LogManager的实例化部分,就可以不需要更改其他的代码,实现日志框架与应用程序之间的解耦。
与代理模式区别:
代理模式是结构型设计模式,中介模式是行为型设计模式。
代理模式是提供一个代理对象,通过这个代理对象来间接地访问实际对象,并且在必要的时候对实际对象的访问进行控制。代理模式可以在不改变实际对象的情况下,增加一些额外的功能,比如权限验证、延迟加载、缓存等。
与桥接模式区别:
桥接模式是将抽象部分和实现部分分离,使它们可以独立地变化。该模式的目的是解耦抽象和实现,让它们可以独立地扩展,从而提高系统的灵活性和可维护性。
场景:
-
多维度变化:当一个类存在多个变化维度时,使用继承会导致类爆炸(类的数量成倍增加)。桥接模式通过将多个变化维度分离,使每个维度的变化可以独立进行扩展,避免了类的爆炸问题。
-
抽象和实现之间的解耦:桥接模式允许抽象部分和实现部分可以独立进行扩展和修改,它们之间通过组合的方式关联,而不是继承关系。这样可以降低两者之间的耦合性,使得系统更加灵活、可维护。
-
平台无关性:在需要跨多个平台或操作系统的场景中,桥接模式可以将抽象和实现分离,从而方便地在不同的平台上扩展和适配实现部分,而不影响抽象部分的代码。
享元模式
享元模式是一种结构型设计模式。“享元”顾名思义,即被共享单元。享元模式的意图是复用对象,节省内存,前提是享元的对象不可变。当一个系统中存在 大量的重复对象时,可以使用享元模式来减少系统的消耗。在内存中只保存一份实例,供多处代码使用,减少内存中对象的数量。
享元模式实现:
主要通过工厂类,通过一个Map后者List来缓存已经创建好的享元对象,达到复用目的。
优点:
-
- 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
- 享元模式中的外部状态相对独立,且不影响内部状态
缺点:
-
- 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂。(这也是本文为了方便大家理解,没去写很复杂代码来实现非享元角色的原因)
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源。
享元模式的应用场景
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
使用到享元模式栗子:
IntegerCache是Java中一个经典的应用了享元模式的缓存实现,用于缓存整型对象。在Java中,整数在范围[-128, 127]之间的对象会被缓存,避免频繁地创建新对象,节省内存空间和提高性能。这个缓存机制是为了优化频繁使用的整数对象,使得相同值的整数对象在范围内只会存在一个实例。
public final class Integer extends Number implements Comparable<Integer> {
// 其他代码...
private static class IntegerCache {
// 缓存范围的下界
static final int low = -128;
// 缓存范围的上界
static final int high;
// 缓存数组
static final Integer cache[];
static {
// high的值为127或者127和MAX_VALUE之间的最小值(取决于范围)
// 127是[-128, 127]范围内整数的个数
// MAX_VALUE是整型的最大值,确保在范围内的所有整数都缓存
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// 不超过整型的最大值
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// 如果解析失败,使用默认值
}
}
high = h;
// 初始化缓存数组cache
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
}
// ...其他代码...
// valueOf方法,通过缓存返回整数对象
public static Integer valueOf(int i) {
final int offset = 128;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + offset];
return new Integer(i);
}
// ...其他代码...
}
-
IntegerCache是一个私有的静态内部类,位于java.lang.Integer类中。 -
IntegerCache内部维护一个长度为cache数组,长度为256([-128, 127]范围的所有整数可能的个数)。 -
在Java类加载的时候,会先初始化
IntegerCache的缓存,将范围在[-128, 127]之间的整数对象放入缓存数组中。 -
通过静态代码块,
IntegerCache会初始化缓存数组中的元素。例如,在范围内,-128对应数组索引0,127对应数组索引255,这样在使用这些整数时,直接从缓存数组中获取,而不会频繁创建新对象。 -
在
valueOf(int)方法中,当整数值在[-128, 127]范围内时,会直接返回缓存数组中的对应整数对象。如果整数值不在这个范围内,会创建新的Integer对象。 -
这种缓存机制在一定程度上优化了频繁使用的整数对象,避免了不必要的对象创建和销毁,节省了内存空间,并提高了性能。
Durid数据库连接池使用到的享元模式,
连接池负责管理和维护数据库连接对象,而不是每次都重新创建新的连接。连接池充当了控制和共享连接对象的作用。
Druid数据库连接池通过将数据库连接对象池化并重用,来减少创建和销毁连接对象的开销。这样,连接对象在池中可以得到重复使用,避免了频繁创建和销毁连接的性能损耗。
Druid数据库连接池会控制连接对象的数量,以避免连接过多而造成资源浪费或者连接过少而导致性能瓶颈。通过设置最小连接数和最大连接数等参数,连接池可以在需要时创建新连接或销毁闲置连接,从而保持连接数量在合理的范围内。
数据库连接对象作为享元对象: 数据库连接对象在Druid数据库连接池中被视为享元对象,即DruidPooledConnection对象。这些连接对象的内部状态包含了数据库的连接URL、用户名、密码等信息。
DruidDataSource类
public class DruidDataSource { // 连接池中的数据库连接对象数组 private final DruidPooledConnection[] connections; // 其他属性... // 初始化连接池,创建数据库连接对象数组 private void initPool() { connections = new DruidPooledConnection[poolSize]; for (int i = 0; i < poolSize; i++) { connections[i] = new DruidPooledConnection(); } } // 从连接池中获取数据库连接对象 public DruidPooledConnection getConnection() { // ...其他代码... // 在此处使用享元模式,从连接池中获取可用的数据库连接对象 return getAvailableConnection(); } // ...其他方法... }
DruidPooledConnection类:这是数据库连接对象的类,它在连接池中被重用,作为享元对象。
public class DruidPooledConnection { // 数据库连接的内部状态,例如连接URL、用户名、密码等信息 private String url; private String username; private String password; // 其他属性... // ...构造方法和其他方法... }
可以看出Druid数据库连接池使用了享元模式来重用数据库连接对象。在DruidDataSource类中,创建了一个数据库连接对象数组connections,当调用getConnection()方法时,会从连接池中获取一个可用的数据库连接对象。在获取连接对象时,实际上是通过getAvailableConnection()方法来从连接池中选择可用的连接对象。这里的关键点是,连接池中的连接对象是被重用的,而不是每次都创建新的连接对象,这正是享元模式的应用。