在软件开发中,我们常常需要创建大量的对象来实现某个功能。这些对象的创建过程可能会占用大量的内存和时间,导致程序运行效率低下。为了解决这个问题,设计模式中提供了一种称为享元模式(Flyweight Pattern)的解决方案。
享元模式的定义
官方文档的定义
"The Flyweight Pattern provides a way to use shared objects that can be used in multiple contexts at the same time. The pattern is used when the creation of many objects would put pressure on the available memory and may cause the application to stop responding."
翻译过来就是:
"享元模式提供了一种使用可在多个上下文中使用的共享对象的方法。当创建许多对象会对可用内存造成压力并可能导致应用程序停止响应时,使用此模式。"
通俗易懂地讲,享元模式就是通过共享相同的对象,避免重复创建对象,从而减少内存使用,提高程序性能。在实际应用中,如果程序需要频繁地创建相同或相似的对象,使用享元模式可以大大节省内存空间,提升程序运行效率。同时,享元模式还能避免由于创建过多对象导致内存溢出等问题。它适用于在大规模的对象创建、销毁、复用的场景中使用。
享元模式的组成部分
-
抽象享元(Flyweight):定义享元对象的接口,包括对象的状态(内部属性)和方法。在抽象享元接口中通常会定义对象的获取、设置内部状态等方法。
-
具体享元(ConcreteFlyweight):实现抽象享元接口中定义的方法,并且为内部状态提供存储空间。具体享元需要注意内部状态的可共享性,即需要确保状态可以被多个对象共享使用。
-
享元工厂(Flyweight Factory):负责创建和管理享元对象,确保可以在需要时共享对象。享元工厂通常会维护一个对象池,用于存储已经创建的享元对象,并且在请求时进行对象的复用。
-
客户端(Client):通过享元工厂获取享元对象,并且可以通过设置享元对象的内部状态来定制对象的行为。客户端需要注意的是,如果需要共享的状态已经存在,则应该从享元工厂中获取对象,而不是创建新的对象。
代码实现
假设我们在设计一个音乐播放器,其中有很多音频文件需要播放。这些音频文件包括不同的格式(例如MP3、WAV等)和不同的长度、大小、音质等属性。
使用享元模式,可以将这些音频文件的共性属性提取出来,例如音频格式、采样率等,避免重复创建相同的对象,提高程序的性能。
具体实现方式如下:
-
抽象享元类:定义音频文件的接口,包括名称、长度、大小、格式等属性以及播放音频的操作方法。
-
具体享元类:实现音频文件的接口,其中包括音频文件的特定属性和方法,例如格式、采样率等。
-
享元工厂类:负责创建和管理音频文件对象,维护一个音频文件池,用于存储已经创建的音频文件对象,并且在需要时进行对象的复用。
-
客户端:通过享元工厂获取音频文件对象,并且可以根据需要来设置音频文件的状态信息,例如名称、长度、大小等。客户端还可以设置音频文件的共性属性,例如格式、采样率等。
使用享元模式来设计音乐播放器,可以避免由于创建过多重复对象导致的程序性能问题,并且可以将音频文件的共性属性提取出来,提高代码复用性和可维护性。
// 定义音频文件接口
interface AudioFile {
void play();
}
// 定义具体的音频文件实现类
class Mp3AudioFile implements AudioFile {
private String name;
private int length;
private int size;
private String format;
public Mp3AudioFile(String name, int length, int size) {
this.name = name;
this.length = length;
this.size = size;
this.format = "MP3";
}
@Override
public void play() {
System.out.println("Playing " + format + " audio file " + name + " (" + length + " seconds, " + size + " KB)...");
}
}
class WavAudioFile implements AudioFile {
private String name;
private int length;
private int size;
private String format;
public WavAudioFile(String name, int length, int size) {
this.name = name;
this.length = length;
this.size = size;
this.format = "WAV";
}
@Override
public void play() {
System.out.println("Playing " + format + " audio file " + name + " (" + length + " seconds, " + size + " KB)...");
}
}
// 定义享元工厂类
class AudioFileFactory {
private static Map<String, AudioFile> pool = new HashMap<>();
public static AudioFile getAudioFile(String name, int length, int size, String format) {
String key = format + ":" + size;
AudioFile audioFile = pool.get(key);
if (audioFile == null) {
switch (format) {
case "MP3":
audioFile = new Mp3AudioFile(name, length, size);
break;
case "WAV":
audioFile = new WavAudioFile(name, length, size);
break;
// 可以添加其他音频文件格式的支持
default:
throw new IllegalArgumentException("Unsupported audio format: " + format);
}
pool.put(key, audioFile);
}
return audioFile;
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
AudioFile audioFile1 = AudioFileFactory.getAudioFile("my-song.mp3", 180, 4200, "MP3");
AudioFile audioFile2 = AudioFileFactory.getAudioFile("my-song.wav", 180, 21000, "WAV");
AudioFile audioFile3 = AudioFileFactory.getAudioFile("my-song.mp3", 180, 4200, "MP3");
audioFile1.play(); // Playing MP3 audio file my-song.mp3 (180 seconds, 4200 KB)...
audioFile2.play(); // Playing WAV audio file my-song.wav (180 seconds, 21000 KB)...
audioFile3.play(); // Playing MP3 audio file my-song.mp3 (180 seconds, 4200 KB)...
System.out.println("audioFile1 == audioFile2 ? " + (audioFile1 == audioFile2)); // false
System.out.println("audioFile1 == audioFile3 ? " + (audioFile1 == audioFile3)); // true
}
}
享元模式在spring源码中的体现
BeanFactory 的实现
在 Spring 框架中,BeanFactory 是一个核心接口,它负责创建和管理 Spring Bean 对象。为了提高创建 Bean 对象的效率,Spring 使用了享元模式来共享已经创建的 Bean 实例。
在 Spring 中,BeanFactory 中有一个名为 beanDefinitionMap 的属性,该属性用于存储 Bean 的定义信息。当第一次通过 getBean() 方法获取一个 Bean 实例时,Spring 会根据该 Bean 的定义信息创建一个新的 Bean 实例,并将其放入一个名为 singletonObjects 的缓存中。当后续再次获取该 Bean 实例时,Spring 会直接从 singletonObjects 缓存中获取,而不是再次创建。
这样做的好处是,避免了重复创建 Bean 实例的开销,提高了应用的性能。同时,Spring 还提供了一种配置方式,可以让我们控制 Bean 是否可以被共享。如果一个 Bean 定义被标记为不可共享,则每次调用 getBean() 方法都会创建一个新的实例,而不是从缓存中获取。这样可以避免出现多个组件共享同一个实例时可能出现的并发问题。
-
BeanFactory:这是 Spring 框架的核心接口之一,它负责创建和管理 Spring Bean 对象。在使用享元模式的过程中,BeanFactory 主要负责维护一个名为 singletonObjects 的缓存,用于存储已经创建的 Bean 实例。同时,BeanFactory 还会从 BeanDefinitionMap 中获取 Bean 的定义信息,并根据该信息创建新的 Bean 实例。
-
BeanDefinitionMap:这是一个名为 beanDefinitionMap 的属性,用于存储 Bean 的定义信息。在使用享元模式的过程中,BeanDefinitionMap 主要作为 BeanFactory 的辅助组件,用于存储和管理 Bean 的定义信息。
-
singletonObjects:这是一个缓存,用于存储已经创建的 Bean 实例。在使用享元模式的过程中,singletonObjects 主要作为 BeanFactory 的核心组成部分,用于提高应用的性能。
总结
享元模式的优点:
-
减少对象的创建:享元模式通过共享已有对象,避免了重复创建新对象的开销,从而减少了内存的占用和系统开销。
-
提高系统性能:由于减少了对象的创建和销毁,系统的性能得到了显著提升。
-
支持大量细粒度对象:享元模式能够将对象拆分为内部状态和外部状态,从而支持大量细粒度对象的共享。
享元模式的缺点:
-
需要进行额外的工作:在使用享元模式时,需要对对象进行分类,区分内部状态和外部状态,并将内部状态作为享元对象的成员变量,这需要进行额外的工作。
-
增加系统复杂度:由于需要进行对象分类和额外的工作,使用享元模式会增加系统的复杂度,降低代码的可读性。
-
限制了对象的变化:由于多个对象共享同一份内部状态,所以当修改内部状态时,需要修改多个对象,这会限制对象的变化。