设计模式 结构型 享元模式(Flyweight Pattern)与 常见技术框架应用 解析
享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享尽可能多的对象来有效地支持大量的细粒度对象。它的主要目的是减少内存使用和提高性能,特别是当创建大量相似对象时。
在许多软件系统中,我们常常会遇到大量细粒度的对象,它们拥有一些相同的属性,却占用了大量的内存空间。例如,在一个文本处理系统中,需要频繁地创建和使用字符对象,如果每个字符都作为一个独立的对象实例,将会消耗巨额内存。再比如,游戏开发中,地图场景里可能有成千上万的相同树木、石头等装饰元素,若都以全新对象存在,系统不堪重负。此时,享元模式应运而生,它致力于通过共享对象来减少内存占用,提升系统性能。
一、核心思想
享元模式的核心思想是运用共享技术,将那些大量的细粒度且状态可以共享的对象进行复用。把对象的状态分为内部状态(可共享,不随环境改变)和外部状态(不可共享,依赖具体环境),通过共享内部状态相同的对象,在不同场景下依据外部状态进行适配,从而在节省内存的同时保证系统功能完整。
二、定义与结构
- 定义:运用共享技术有效地支持大量细粒度的对象。通过共享对象,避免大量相似对象的重复创建,以节约内存资源,提高系统性能。
- 结构:
- Flyweight(抽象享元):这是所有具体享元类的超类或接口,定义了业务方法以及接收并作用于外部状态的方法,通常以参数形式传入外部状态。
- ConcreteFlyweight(具体享元):实现抽象享元接口,它所代表的对象拥有可共享的内部状态,并实现业务逻辑。内部状态在对象创建后通常不再改变。
- FlyweightFactory(享元工厂):负责创建和管理享元对象。它维护一个享元池,存储已经创建的享元对象,当客户端请求对象时,工厂先在池中查找是否已有符合要求的对象,若有则直接返回共享对象,若没有则创建新对象并放入池中。
- Client(客户端):维护对享元对象的引用,负责计算或存储对象的外部状态,并在需要时调用享元对象的方法,传入相应外部状态。
三、角色
1、抽象享元(Flyweight)
职责:
- 声明公共方法,这些方法供具体享元类实现,用于提供对外的服务。
- 定义接收外部状态的方法接口,使得具体享元类能依据不同外部状态展现不同行为。
示例代码(以图形绘制系统为例):
// 抽象享元
interface Shape {
void draw(Color color);
}
这里假设在图形绘制场景,Shape
作为抽象享元,定义了draw
方法,通过传入颜色(外部状态)来绘制图形。
2、具体享元(ConcreteFlyweight)
职责:
- 实现抽象享元接口,具体化业务逻辑。
- 持有并维护自身的内部状态,内部状态通常在构造时初始化且后续不变。
示例代码(以图形绘制系统为例):
// 具体享元 - 圆形
class Circle implements Shape {
private final String type = "Circle";
@Override
public void draw(Color color) {
System.out.println("绘制 " + type + ",颜色:" + color.getName());
}
}
Circle
类作为具体享元,实现了Shape
接口,它有固定的内部状态“Circle”,根据传入的颜色(外部状态)绘制圆形。
3、享元工厂(FlyweightFactory)
职责:
- 创建并管理享元对象池,确保相同内部状态的对象被共享。
- 提供获取享元对象的方法,根据客户端需求,从池中检索或创建新的享元对象。
示例代码(以图形绘制系统为例):
// 享元工厂
class ShapeFactory {
private static final Map<String, Shape> shapeMap = new HashMap<>();
public static Shape getShape(String type) {
Shape shape = shapeMap.get(type);
if (shape == null) {
if ("Circle".equals(type)) {
shape = new Circle();
}
shapeMap.put(type, shape);
}
return shape;
}
}
ShapeFactory
类维护了一个Map
作为享元池,依据传入的形状类型查找或创建对应的形状(享元对象),若首次创建圆形,则放入池中供后续复用。
4、客户端(Client)
职责:
- 初始化系统,创建必要的享元工厂实例。
- 确定对象所需的外部状态,并在调用享元对象方法时传递。
- 维护对享元对象的使用,组织业务流程。
示例代码(以图形绘制系统为例):
public class Main {
public static void main(String[] args) {
ShapeFactory factory = new ShapeFactory();
Shape circle1 = factory.getShape("Circle");
Shape circle2 = factory.getShape("Circle");
circle1.draw(new Color("Red"));
circle2.draw(new Color("Blue"));
System.out.println(circle1 == circle2);
}
}
在main
方法中,客户端先创建享元工厂,然后获取两次圆形享元对象,分别传入不同颜色绘制,最后验证两次获取的是同一对象实例(因为共享)。
四、实现步骤及代码示例
1、以在线文档系统中的字符格式设置为例
步骤一:定义抽象享元(CharacterFormat)
假设在线文档有字体、字号、颜色等格式设置需求,先定义抽象享元接口。
interface CharacterFormat {
void applyFormat(String text);
}
这里applyFormat
方法用于将格式应用到文本上,文本作为外部状态传入。
步骤二:创建具体享元(FontFormat、SizeFormat、ColorFormat)
以字体格式为例:
class FontFormat implements CharacterFormat {
private final String font;
public FontFormat(String font) {
this.font = font;
}
@Override
public void applyFormat(String text) {
System.out.println("设置字体为 " + font + ":" + text);
}
}
类似地创建SizeFormat
(设置字号)和ColorFormat
(设置颜色)类,它们都实现CharacterFormat
接口,持有各自的内部状态(如固定的字体、字号、颜色值)。
步骤三:构建享元工厂(FormatFactory)
class FormatFactory {
private static final Map<String, CharacterFormat> formatMap = new HashMap<>();
public static CharacterFormat getFormat(String type, String value) {
String key = type + ":" + value;
CharacterFormat format = formatMap.get(key);
if (format == null) {
if ("Font".equals(type)) {
format = new FontFormat(value);
} else if ("Size".equals(type)) {
format = new SizeFormat(value);
} else if ("Color".equals(type)) {
format = new ColorFormat(value);
}
formatMap.put(key, format);
}
return format;
}
}
FormatFactory
维护格式对象池,根据传入的格式类型(如“Font”“Size”“Color”)和具体值,查找或创建对应的格式享元对象。
步骤四:使用享元模式
public class Main {
public static void main(String[] args) {
FormatFactory factory = new FormatFactory();
CharacterFormat fontFormat1 = factory.getFormat("Font", "Arial");
CharacterFormat fontFormat2 = factory.getFormat("Font", "Arial");
fontFormat1.applyFormat("Hello");
fontFormat2.applyFormat("World");
System.out.println(fontFormat1 == fontFormat2);
}
}
在main
方法中,客户端先创建工厂,获取两次“Arial”字体格式享元,分别应用到不同文本,验证两者是同一实例,实现了字体格式对象的共享。
五、常见技术框架应用
1、在Java String常量池中的应用
原理:
- Java中的
String
类运用了享元模式的思想。String
对象是不可变的,当创建字符串字面量时,Java虚拟机(JVM)会先在常量池中查找是否已存在相同内容的字符串,如果有,则直接返回该字符串的引用,避免重复创建。
示例代码(简单验证):
public class StringFlyweightExample {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
}
}
运行结果为true
,表明str1
和str2
指向常量池中同一个“Hello”字符串对象,体现了享元模式的共享机制。
2、在数据库连接池中的应用
原理:
- 数据库连接池是享元模式的典型应用场景。创建数据库连接是耗时且资源消耗大的操作。连接池作为享元工厂,维护着一定数量的数据库连接(享元对象),客户端(应用程序)请求连接时,连接池先查看是否有空闲连接,若有则直接返回供客户端使用,用完后归还到池中,若没有空闲连接且未达最大连接数,则创建新连接,避免每次操作都新建连接。
示例代码(简化的连接池示意,基于JDBC):
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
class ConnectionPool {
private static final String URL = "jdbc:mysql://localhost:3306/test";
private static final String USER = "root";
private static final String PASS = "password";
private static final int MAX_CONNECTIONS = 10;
private static List<Connection> connectionList = new ArrayList<>();
static {
try {
for (int i = 0; i < MAX_CONNECTIONS; i++) {
Connection connection = DriverManager.getConnection(URL, USER, PASS);
connectionList.add(connection);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static Connection getConnection() {
if (connectionList.isEmpty()) {
return null;
}
return connectionList.remove(0);
}
public static void releaseConnection(Connection connection) {
if (connectionList.size() < MAX_CONNECTIONS) {
connectionList.add(connection);
}
}
}
public class DatabaseFlyweightExample {
public static void main(String[] args) {
Connection connection1 = ConnectionPool.getConnection();
Connection connection2 = ConnectionPool.getConnection();
System.out.println(connection1 == connection2);
ConnectionPool.releaseConnection(connection1);
ConnectionPool.releaseConnection(connection2);
}
}
上述代码构建了一个简易的数据库连接池,初始化时创建一定数量连接放入池中,getConnection
方法获取连接,releaseConnection
方法归还连接,验证了从池中获取的连接可能是同一实例,体现共享特性。
3、前端 缓存组件或复用DOM
在JavaScript(例如React)中,尽管不是直接应用享元模式,但可以通过缓存组件或复用DOM节点来达到类似的效果:
const cachedComponents = {};
function getComponent(id, ComponentType, props) {
if (!cachedComponents[id]) {
cachedComponents[id] = <ComponentType {...props} />;
}
return cachedComponents[id];
}
// Usage in App.js
function App() {
const componentA = getComponent('componentA', MyComponent, { prop: 'value' });
return (
<div>
{componentA}
{/* ... */}
</div>
);
}
六、应用场景
- 大量相似对象场景:如游戏开发中大量重复的游戏元素(地形块、怪物模型等),使用享元模式可减少对象创建开销,提高加载速度与运行效率。
- 需要缓存数据场景:像网页缓存,对于频繁访问的网页页面元素,可作为享元对象缓存,下次访问直接取用,降低服务器压力与响应时间。
- 系统资源紧张场景:在内存受限的嵌入式系统或移动端应用中,通过共享对象避免内存耗尽,确保系统稳定运行,比如手机游戏里的小型道具对象复用。
七、优缺点
优点
- 显著降低内存消耗:通过共享对象,避免大量重复对象创建,节省宝贵内存资源,尤其在对象数量庞大时效果突出。
- 提升系统性能:减少对象创建与销毁的开销,使得系统运行更流畅,响应更快,如数据库连接池提升数据库操作效率。
- 易于维护与管理:享元对象相对稳定,集中在工厂类管理,便于代码调试、更新,新的共享需求加入也较便捷。
缺点
- 增加设计复杂度:引入抽象享元、具体享元、工厂等多层结构,相比简单直接创建对象模式,代码理解与设计难度上升。
- 依赖共享状态管理:需精准划分内部、外部状态,内部状态一旦出错共享失效,外部状态传递不当也会引发逻辑混乱,管理不善易出错。
- 可能引发线程安全问题:多线程环境下,多个线程同时访问、修改享元对象或享元工厂,若未做好同步处理,会导致数据不一致等问题。
综上所述,享元模式是一种有效的结构型设计模式,它通过共享对象来减少内存使用和提高系统性能。在需要处理大量相似对象的场景中,享元模式能够显著减少对象的数量,从而降低内存消耗和提高性能。然而,享元模式的实现也需要考虑对象的内部状态和外部状态的分离以及线程安全等问题。