享元模式 (Flyweight Pattern)
概述
享元模式是一种结构型设计模式,它运用共享技术有效地支持大量细粒度的对象。享元模式通过共享技术避免大量拥有相同内容对象的开销,提高系统资源的利用率。
意图
- 运用共享技术有效地支持大量细粒度的对象
适用场景
- 一个系统有大量相同或相似的对象,造成内存的大量耗费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 使用享元模式需要维护一个存储享元对象的享元池,而这需要耗费资源,因此,应当在多次重复使用享元对象时才值得使用享元模式
结构
┌─────────────┐ ┌─────────────┐
│ Client │──────────>│ FlyweightFactory│
├─────────────┤ ├─────────────┤
│ │ │ - flyweights│
└─────────────┘ │ + getFlyweight()│
└─────────────┘
▲
│
┌─────────────┐ ┌─────────────┐
│UnsharedConcreteFlyweight│ │ Flyweight │
├─────────────┤ ├─────────────┤
│ - intrinsicState│ │ + operation()│
│ + operation()│ └─────────────┘
└─────────────┘ ▲
│
┌─────────────┐
│ConcreteFlyweight│
├─────────────┤
│ - intrinsicState│
│ + operation()│
└─────────────┘
参与者
- Flyweight:描述一个接口,通过这个接口flyweight可以接受并作用于外部状态
- ConcreteFlyweight:实现Flyweight接口,并为内部状态增加存储空间。ConcreteFlyweight对象必须是可共享的,它所存储的状态必须是内部的
- UnsharedConcreteFlyweight:并非所有的Flyweight子类都需要被共享,Flyweight接口使共享成为可能,但它并不强制共享
- FlyweightFactory:创建并管理flyweight对象,确保合理地共享flyweight。当用户请求一个flyweight时,FlyweightFactory对象提供一个已创建的实例或者创建一个
- Client:维持一个对flyweight的引用,计算或存储一个flyweight的外部状态
示例代码
下面是一个完整的享元模式示例,以围棋游戏为例:
java
import java.util.HashMap;
import java.util.Map;
// Flyweight - 享元接口
public interface ChessPiece {
void display(int x, int y);
}
// ConcreteFlyweight - 具体享元类
public class ConcreteChessPiece implements ChessPiece {
private String color; // 内部状态
private String name; // 内部状态
public ConcreteChessPiece(String color, String name) {
this.color = color;
this.name = name;
}
@Override
public void display(int x, int y) {
System.out.println(color + " " + name + " 在位置 (" + x + ", " + y + ")");
}
}
// FlyweightFactory - 享元工厂
public class ChessPieceFactory {
private static Map<String, ChessPiece> pieces = new HashMap<>();
public static ChessPiece getChessPiece(String color, String name) {
String key = color + "_" + name;
ChessPiece piece = pieces.get(key);
if (piece == null) {
piece = new ConcreteChessPiece(color, name);
pieces.put(key, piece);
System.out.println("创建新的棋子: " + color + " " + name);
} else {
System.out.println("使用已存在的棋子: " + color + " " + name);
}
return piece;
}
public static int getTotalPieces() {
return pieces.size();
}
}
// Client - 客户端
public class Client {
public static void main(String[] args) {
// 创建棋子
ChessPiece blackKing = ChessPieceFactory.getChessPiece("黑色", "国王");
blackKing.display(1, 1);
ChessPiece whiteKing = ChessPieceFactory.getChessPiece("白色", "国王");
whiteKing.display(8, 8);
ChessPiece blackQueen = ChessPieceFactory.getChessPiece("黑色", "皇后");
blackQueen.display(1, 4);
ChessPiece whiteQueen = ChessPieceFactory.getChessPiece("白色", "皇后");
whiteQueen.display(8, 4);
// 使用已存在的棋子
ChessPiece blackKing2 = ChessPieceFactory.getChessPiece("黑色", "国王");
blackKing2.display(2, 2);
ChessPiece whiteQueen2 = ChessPieceFactory.getChessPiece("白色", "皇后");
whiteQueen2.display(7, 4);
System.out.println("总共创建了 " + ChessPieceFactory.getTotalPieces() + " 个不同的棋子对象");
}
}
另一个示例 - 文本编辑器
java
import java.util.HashMap;
import java.util.Map;
// Flyweight - 享元接口
public interface Character {
void display(int fontSize, String color);
}
// ConcreteFlyweight - 具体享元类
public class ConcreteCharacter implements Character {
private char symbol; // 内部状态
public ConcreteCharacter(char symbol) {
this.symbol = symbol;
}
@Override
public void display(int fontSize, String color) {
System.out.println("字符 '" + symbol + "' - 字体大小: " + fontSize + ", 颜色: " + color);
}
}
// FlyweightFactory - 享元工厂
public class CharacterFactory {
private static Map<Character, Character> characters = new HashMap<>();
public static Character getCharacter(char symbol) {
Character character = characters.get(symbol);
if (character == null) {
character = new ConcreteCharacter(symbol);
characters.put(symbol, character);
System.out.println("创建新的字符对象: '" + symbol + "'");
} else {
System.out.println("使用已存在的字符对象: '" + symbol + "'");
}
return character;
}
public static int getTotalCharacters() {
return characters.size();
}
}
// UnsharedConcreteFlyweight - 非共享的具体享元类
public class Paragraph {
private String text;
private int fontSize;
private String color;
public Paragraph(String text, int fontSize, String color) {
this.text = text;
this.fontSize = fontSize;
this.color = color;
}
public void display() {
System.out.println("段落: " + text + " - 字体大小: " + fontSize + ", 颜色: " + color);
}
}
// Client - 客户端
public class Client {
public static void main(String[] args) {
// 创建文档
String document = "Hello World! This is a test document.";
// 显示文档中的每个字符
for (int i = 0; i < document.length(); i++) {
char c = document.charAt(i);
Character character = CharacterFactory.getCharacter(c);
character.display(12, "black");
}
System.out.println("总共创建了 " + CharacterFactory.getTotalCharacters() + " 个不同的字符对象");
// 创建非共享的段落对象
Paragraph paragraph = new Paragraph("This is a paragraph.", 14, "blue");
paragraph.display();
}
}
优缺点
优点
- 享元模式的优点在于它可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份
- 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享
缺点
- 享元模式使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化
- 享元模式将享元对象的状态外部化,而读取外部状态使得运行时间变长
相关模式
- 组合模式:享元模式通常与组合模式一起使用,共享组合结构中的叶节点
- 单例模式:享元工厂通常可以设计为单例
- 策略模式:享元对象可以作为策略对象使用
实际应用
- Java中的String常量池
- Java中的Integer缓存
- 数据库连接池
- 线程池
- 游戏中的大量相同对象(如子弹、树木等)
- 文本编辑器中的字符对象
内部状态与外部状态
享元模式的核心是区分内部状态和外部状态:
- 内部状态:存储在享元对象内部,不会随环境改变而改变,可以被共享
- 外部状态:随环境改变而改变,不可以被共享,由客户端保存并在需要时传入享元对象
在实现享元模式时,需要仔细分析哪些状态是内部的,哪些是外部的,以便正确地实现共享。
享元模式的实现步骤
- 分析应用中哪些对象可以被共享
- 将这些对象的状态分为内部状态和外部状态
- 创建享元接口,定义操作外部状态的方法
- 创建具体享元类,实现享元接口,存储内部状态
- 创建享元工厂,管理享元对象的创建和共享
- 客户端通过享元工厂获取享元对象,并传入外部状态
注意事项
- 享元模式适用于大量相似对象的场景,如果对象数量不多,使用享元模式可能得不偿失
- 享元模式会增加系统的复杂性,需要仔细分析内部状态和外部状态
- 享元模式需要维护一个享元池,这会增加一定的内存开销
- 享元模式通常与工厂模式一起使用,由工厂来管理享元对象的创建和共享