定义:
享元模式(Flyweight Pattern)是一种结构型设计模式,用于优化性能和内存使用。它通过共享尽可能多的相似对象来减少内存占用,特别是在有大量对象时。这种模式通常用于减少应用程序中对象的数量,并在多个对象间共享尽可能多的状态。
享元模式的关键是区分内在状态(Intrinsic State)和外在状态(Extrinsic State):
- 内在状态(Intrinsic State):
- 这部分状态是对象共享的。享元对象的内在状态不依赖于享元对象的上下文,即它们是不变的。这使得一个享元对象可以在不同的上下文中共享。
- 外在状态(Extrinsic State):
- 这部分状态则依赖于具体的上下文,并且不能在享元对象之间共享。每个对象实例将有自己独特的外在状态。
享元模式通常涉及以下几个角色:
- 享元接口(Flyweight):定义了享元对象的新操作,它通常接收并作用于外在状态。
- 具体享元(Concrete Flyweight):实现享元接口,并存储内在状态。具体享元对象必须是可共享的,它的状态不应当随上下文变化。
- 享元工厂(Flyweight Factory):负责创建和管理享元对象,确保合理地共享享元。
享元模式适用于程序中存在大量相似对象的情况,可以有效地减少内存占用,提高性能。典型的应用场景包括字符串常量池、数据库连接池、缓存等。
解决的问题:
- 减少内存占用:
- 当系统中存在大量相似或相同的对象时,这些对象占用大量内存。享元模式通过共享相似对象来减少内存消耗。
- 提高性能:
- 减少对象创建的数量意味着降低了内存占用和系统垃圾回收的负担,从而提高应用性能。
- 管理共享对象:
- 在享元模式中,共享对象的管理变得更加集中和统一,使得对共享对象的维护和更新更加高效。
- 优化数据结构:
- 在处理大量小粒度对象的场景中,享元模式帮助减少应用程序中的对象数量,从而优化数据结构和提高运行效率。
- 减少系统复杂性:
- 通过减少对象实例的数量,系统变得更加简单,易于理解和维护。
享元模式通过区分内在状态和外在状态,并共享内在状态,解决了大量对象存在时的性能和内存问题。这种模式在诸如文本处理、图形渲染、数据库连接池等需要大量小粒度对象的场景中尤为有用。
使用场景:
- 大量相似对象的场景:
- 当应用程序需要创建大量相似的对象,并且这些对象的大部分状态可以被共享时,使用享元模式可以显著减少内存占用。这种情况常见于处理大量小粒度对象的系统。
- 内存限制严格的应用:
- 在资源有限或内存受限的应用程序(如移动设备或嵌入式系统)中,享元模式能有效减少内存消耗。
- 重复对象的数据库存储:
- 当相同的数据需要在数据库中多次存储时,可以使用享元模式来共享这些数据,减少数据库的存储负担。
- 图形相关应用:
- 在图形相关的应用程序中,例如图形编辑器或游戏,常常需要创建大量相似的图形对象,如图标、线条、字符等。享元模式能够减少这些对象的数量,优化性能和资源使用。
- 字符串池:
- 在程序中,字符串常量池是享元模式的一个典型应用。例如,Java中的
String
对象实现采用享元模式来避免重复创建相同的字符串字面量。
- 在程序中,字符串常量池是享元模式的一个典型应用。例如,Java中的
- 对象缓存:
- 当对象的创建和销毁成本较高时,可以将这些对象缓存起来供后续重用,这是享元模式的另一种应用场景。
- 系统状态共享:
- 在需要在多个对象间共享某些状态(如配置信息、环境设置等)时,享元模式可以避免重复存储这些状态。
享元模式的关键在于识别哪些状态是可以共享的(内在状态),哪些状态是依赖于上下文的(外在状态),并且只共享内在状态。正确应用享元模式可以带来显著的性能提升和内存使用优化。
示例代码 1 - 简单享元模式:
// 享元接口
public interface Flyweight {void operation(String extrinsicState);
}// 具体享元类
class ConcreteFlyweight implements Flyweight {private String intrinsicState;public ConcreteFlyweight(String intrinsicState) {this.intrinsicState = intrinsicState;}@Overridepublic void operation(String extrinsicState) {System.out.println("Intrinsic State = " + intrinsicState +", Extrinsic State = " + extrinsicState);}
}// 享元工厂
class FlyweightFactory {private Map<String, Flyweight> flyweights = new HashMap<>();public Flyweight getFlyweight(String key) {if (!flyweights.containsKey(key)) {flyweights.put(key, new ConcreteFlyweight(key));}return flyweights.get(key);}
}// 客户端代码展示了如何使用享元模式
public class FlyweightPatternDemo {public static void main(String[] args) {FlyweightFactory factory = new FlyweightFactory();Flyweight flyweight1 = factory.getFlyweight("Key1");flyweight1.doOperation("Operation1");Flyweight flyweight2 = factory.getFlyweight("Key2");flyweight2.doOperation("Operation2");Flyweight flyweight3 = factory.getFlyweight("Key1");flyweight3.doOperation("Operation3");}
}
在此示例中,FlyweightFactory
管理着享元对象的集合。当客户端请求一个享元时,工厂首先检查是否已经创建了具有相应内在状态的享元对象。如果是,工厂返回现有的对象;如果不是,工厂将创建一个新的享元对象。这种方式确保具有相同内在状态的享元对象在系统中只有一个实例,从而实现对象的共享。
此示例中,ConcreteFlyweight
类的实例是根据内在状态(intrinsicState
)共享的。客户端通过传递外在状态(extrinsicState
)给享元对象的方法,实现了对状态的操作。这种分离内在状态和外在状态的方法是享元模式的核心,它允许系统有效地共享对象,减少内存占用,提高性能。
示例代码 2 - 享元模式在文本格式化中的应用:
// 字符享元
class CharacterFlyweight {private final char intrinsicChar;public CharacterFlyweight(char intrinsicChar) {this.intrinsicChar = intrinsicChar;}public void display(int fontSize) {System.out.println("Character: " + intrinsicChar + ", Font size: " + fontSize);}
}// 享元工厂
class CharacterFactory {private final Map<Character, CharacterFlyweight> pool = new HashMap<>();public CharacterFlyweight getCharacter(char ch) {CharacterFlyweight flyweight = pool.get(ch);if (flyweight == null) {flyweight = new CharacterFlyweight(ch);pool.put(ch, flyweight);}return flyweight;}
}
主要符合的设计原则:
- 开闭原则(Open-Closed Principle):
- 享元模式支持在不修改现有代码的情况下,增加新的享元实现。可以扩展享元工厂以支持新类型的享元对象,而无需修改现有的享元类和客户端代码。
- 单一职责原则(Single Responsibility Principle):
- 享元模式中的享元对象专注于实现内在状态的行为,而享元工厂则专注于管理和创建享元对象。这样的分工使每个类只有一个原因引起变化,符合单一职责原则。
- 最少知识原则(Principle of Least Knowledge)/迪米特法则(Law of Demeter):
- 享元模式鼓励使用最少的知识去完成任务,客户端不需要知道享元对象的内部结构和存储方式,只需通过享元工厂来获取所需的享元对象。
享元模式通过有效地共享对象来优化内存和性能,特别适用于大量类似对象频繁创建和销毁的场景。通过区分内在状态和外在状态,享元模式确保共享对象的高效管理。
在JDK中的应用:
- 字符串常量池(String Constant Pool):
- 在Java中,字符串常量池是享元模式的一个典型例子。相同的字符串字面量只存储一次。当创建相同内容的字符串时,Java会首先检查字符串常量池中是否存在该字符串,如果存在,则返回对池中字符串的引用,而不是创建一个新的对象。
- 包装类型的值缓存:
- Java的自动装箱过程使用享元模式来缓存一定范围内的包装对象。例如,
Integer.valueOf()
方法会缓存-128
到127
之间的Integer
对象。当在这个范围内创建Integer
对象时,会直接返回缓存中的对象而不是每次都创建新对象。
- Java的自动装箱过程使用享元模式来缓存一定范围内的包装对象。例如,
java.awt.Font
类:- AWT库中的
Font
类使用享元模式来减少创建字体实例的内存开销。由于字体创建代价较大,共享相同属性的字体实例可以显著提高性能和减少内存消耗。
- AWT库中的
这些应用展示了享元模式在Java标准库中的普遍性和实用性。特别是在处理大量细粒度对象时,通过共享对象来优化内存和性能,从而提升应用的效率和响应速度。
在Spring中的应用:
- Spring Bean的作用域:
- 在Spring框架中,单例模式是Bean默认的作用域。虽然这是单例模式的应用,但它也体现了享元模式的核心思想 —— 对象共享。单例Bean在Spring容器中只创建一次,之后每次请求都使用相同的实例,从而减少了对象创建的开销。
- Spring Security的上下文持有者:
- Spring Security中使用的
SecurityContextHolder
策略,可以看作是享元模式的一个应用。它为不同的安全上下文提供统一的访问点,并在内部通过线程局部变量共享安全上下文信息。
- Spring Security中使用的
- 缓存抽象:
- Spring的缓存抽象允许在应用中轻松地添加和管理缓存,这也是一种享元模式的体现。缓存的目的是重用之前计算的结果,减少资源密集型操作的重复执行。
- 数据访问对象(DAO):
- 在Spring框架中,DAO类通常被设计为无状态的单例Bean,这意味着它们在应用中被共享。虽然这本质上是单例模式的应用,但是它也体现了享元模式的思想,即共享对象以减少内存占用和提高性能。
尽管Spring框架中享元模式的应用可能不如其他设计模式那样显著,但在处理对象共享和重用方面,享元模式的思想仍然在Spring的设计和实现中发挥着作用。