设计模式之:享元模式
文章目录
- 什么是享元模式?
- 核心思想
- 生活中的享元模式
- 模式结构
- 基础示例:文字编辑器
- 1. 享元接口和具体享元
- 2. 享元工厂
- 3. 客户端使用
- 4. 测试客户端
- 完整示例:围棋游戏
- 1. 围棋棋子享元
- 2. 围棋棋盘
- 3. 游戏客户端
- 实际应用示例:数据库连接池
- 享元模式的优点
- 1. 大幅减少内存使用
- 2. 提高性能
- 3. 更好的对象管理
- 享元模式的缺点
- 1. 增加系统复杂度
- 2. 可能引入线程安全问题
- 适用场景
- 最佳实践
- 1. 合理划分内部状态和外部状态
- 2. 使用工厂管理享元对象
- 3. 考虑线程安全性
- 享元模式 vs 其他模式
- 总结
什么是享元模式?
享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享技术来有效地支持大量细粒度对象的复用。享元模式通过共享已经存在的对象来减少内存使用,提高系统性能。
核心思想
享元模式的核心思想是:将对象的属性分为内部状态和外部状态,内部状态可以共享,外部状态由客户端维护,从而减少内存中对象的数量。
生活中的享元模式
想象一下字处理软件:
- 每个字符都有字体、大小、颜色等属性
- 如果每个字符都存储所有属性,内存会爆炸
- 实际上,相同格式的字符共享格式信息
- 只有字符内容和位置是每个字符独有的
这就是享元模式的思想!
模式结构
享元模式包含四个核心角色:
- 享元接口(Flyweight):定义享元对象的接口
- 具体享元(ConcreteFlyweight):实现享元接口,包含内部状态
- 享元工厂(FlyweightFactory):创建和管理享元对象
- 客户端(Client):维护外部状态,使用享元对象
基础示例:文字编辑器
1. 享元接口和具体享元
/*** 字符享元接口*/
public interface CharacterFlyweight {/*** 显示字符* @param externalState 外部状态(位置、颜色等)*/void display(CharacterExternalState externalState);/*** 获取字符内容*/char getCharacter();/*** 获取内部状态*/CharacterInternalState getInternalState();
}/*** 字符内部状态 - 可以共享的部分*/
class CharacterInternalState {private final String fontFamily;private final int fontSize;private final boolean isBold;private final boolean isItalic;public CharacterInternalState(String fontFamily, int fontSize, boolean isBold, boolean isItalic) {this.fontFamily = fontFamily;this.fontSize = fontSize;this.isBold = isBold;this.isItalic = isItalic;}// Getter方法public String getFontFamily() { return fontFamily; }public int getFontSize() { return fontSize; }public boolean isBold() { return isBold; }public boolean isItalic() { return isItalic; }@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;CharacterInternalState that = (CharacterInternalState) o;return fontSize == that.fontSize && isBold == that.isBold && isItalic == that.isItalic && Objects.equals(fontFamily, that.fontFamily);}@Overridepublic int hashCode() {return Objects.hash(fontFamily, fontSize, isBold, isItalic);}@Overridepublic String toString() {return String.format("字体:%s, 大小:%d, 粗体:%s, 斜体:%s", fontFamily, fontSize, isBold ? "是" : "否", isItalic ? "是" : "否");}
}/*** 字符外部状态 - 不可共享的部分*/
class CharacterExternalState {private final int positionX;private final int positionY;private final String color;public CharacterExternalState(int positionX, int positionY, String color) {this.positionX = positionX;this.positionY = positionY;this.color = color;}// Getter方法public int getPositionX() { return positionX; }public int getPositionY() { return positionY; }public String getColor() { return color; }@Overridepublic String toString() {return String.format("位置(%d,%d), 颜色:%s", positionX, positionY, color);}
}/*** 具体字符享元*/
public class ConcreteCharacter implements CharacterFlyweight {private final char character;private final CharacterInternalState internalState;public ConcreteCharacter(char character, CharacterInternalState internalState) {this.character = character;this.internalState = internalState;}@Overridepublic void display(CharacterExternalState externalState) {System.out.printf("字符 '%c' [%s] [%s]%n", character, internalState, externalState);}@Overridepublic char getCharacter() {return character;}@Overridepublic CharacterInternalState getInternalState() {return internalState;}
}
2. 享元工厂
import java.util.HashMap;
import java.util.Map;/*** 字符享元工厂* 管理字符享元对象的创建和共享*/
public class CharacterFlyweightFactory {private static CharacterFlyweightFactory instance;private final Map<String, CharacterFlyweight> characterPool;private CharacterFlyweightFactory() {this.characterPool = new HashMap<>();}public static CharacterFlyweightFactory getInstance() {if (instance == null) {instance = new CharacterFlyweightFactory();}return instance;}/*** 获取字符享元对象*/public CharacterFlyweight getCharacter(char character, CharacterInternalState internalState) {String key = generateKey(character, internalState);// 如果池中已存在,直接返回if (characterPool.containsKey(key)) {System.out.printf("✓ 共享字符: '%c' [%s]%n", character, internalState);return characterPool.get(key);}// 否则创建新的享元对象并放入池中System.out.printf("➤ 创建新字符: '%c' [%s]%n", character, internalState);CharacterFlyweight flyweight = new ConcreteCharacter(character, internalState);characterPool.put(key, flyweight);return flyweight;}/*** 生成享元对象的键*/private String generateKey(char character, CharacterInternalState internalState) {return character + "|" + internalState.hashCode();}/*** 获取池中享元对象数量*/public int getPoolSize() {return characterPool.size();}/*** 显示池中所有享元对象*/public void displayPool() {System.out.println("\n📊 字符享元池内容:");System.out.println("-".repeat(50));characterPool.forEach((key, flyweight) -> {ConcreteCharacter character = (ConcreteCharacter) flyweight;System.out.printf("键: %s -> 字符: '%c' [%s]%n", key, character.getCharacter(), character.getInternalState());});System.out.println("总计: " + characterPool.size() + " 个享元对象");}
}
3. 客户端使用
import java.util.ArrayList;
import java.util.List;
import java.util.Random;/*** 文档编辑器 - 客户端*/
public class DocumentEditor {private final List<CharacterContext> documentContent;private final CharacterFlyweightFactory factory;private final Random random;public DocumentEditor() {this.documentContent = new ArrayList<>();this.factory = CharacterFlyweightFactory.getInstance();this.random = new Random();}/*** 向文档添加字符*/public void addCharacter(char character, CharacterInternalState internalState, int positionX, int positionY, String color) {// 获取享元对象CharacterFlyweight flyweight = factory.getCharacter(character, internalState);// 创建外部状态CharacterExternalState externalState = new CharacterExternalState(positionX, positionY, color);// 保存字符上下文CharacterContext context = new CharacterContext(flyweight, externalState);documentContent.add(context);}/*** 显示文档内容*/public void displayDocument() {System.out.println("\n📄 文档内容:");System.out.println("=" .repeat(60));for (CharacterContext context : documentContent) {context.display();}System.out.println("=" .repeat(60));System.out.printf("文档字符数: %d, 享元对象数: %d, 节省内存: %.1f%%%n",documentContent.size(), factory.getPoolSize(),(1 - (double) factory.getPoolSize() / documentContent.size()) * 100);}/*** 生成测试文档*/public void generateTestDocument() {System.out.println("🛠️ 生成测试文档...");// 定义几种格式CharacterInternalState[] formats = {new CharacterInternalState("宋体", 12, false, false), // 普通文本new CharacterInternalState("黑体", 14, true, false), // 标题new CharacterInternalState("楷体", 12, false, true), // 强调new CharacterInternalState("宋体", 10, false, false) // 小字};String[] colors = {"黑色", "红色", "蓝色", "绿色"};String text = "享元模式通过共享技术来有效地支持大量细粒度对象的复用";int position = 0;for (char c : text.toCharArray()) {// 随机选择格式和颜色CharacterInternalState format = formats[random.nextInt(formats.length)];String color = colors[random.nextInt(colors.length)];addCharacter(c, format, position * 20, 0, color);position++;}}
}/*** 字符上下文 - 维护外部状态*/
class CharacterContext {private final CharacterFlyweight flyweight;private final CharacterExternalState externalState;public CharacterContext(CharacterFlyweight flyweight, CharacterExternalState externalState) {this.flyweight = flyweight;this.externalState = externalState;}public void display() {flyweight.display(externalState);}public CharacterFlyweight getFlyweight() {return flyweight;}public CharacterExternalState getExternalState() {return externalState;}
}
4. 测试客户端
/*** 享元模式测试客户端*/
public class FlyweightClient {public static void main(String[] args) {System.out.println("=== 享元模式演示 - 文字编辑器 ===\n");DocumentEditor editor = new DocumentEditor();// 演示1:基础使用demonstrateBasicUsage(editor);// 演示2:性能对比demonstratePerformanceComparison();// 演示3:实际应用场景demonstrateRealWorldScenario();}/*** 演示基础使用*/private static void demonstrateBasicUsage(DocumentEditor editor) {System.out.println("1. 基础使用演示:");System.out.println("-".repeat(50));// 创建几种格式CharacterInternalState normal = new CharacterInternalState("宋体", 12, false, false);CharacterInternalState bold = new CharacterInternalState("黑体", 14, true, false);CharacterInternalState italic = new CharacterInternalState("楷体", 12, false, true);// 添加一些字符editor.addCharacter('H', bold, 0, 0, "红色");editor.addCharacter('e', normal, 20, 0, "黑色");editor.addCharacter('l', normal, 40, 0, "黑色");editor.addCharacter('l', normal, 60, 0, "黑色"); // 重复字符,应该共享editor.addCharacter('o', italic, 80, 0, "蓝色");editor.addCharacter('!', bold, 100, 0, "红色");// 显示文档editor.displayDocument();// 显示享元池CharacterFlyweightFactory.getInstance().displayPool();}/*** 演示性能对比*/private static void demonstratePerformanceComparison() {System.out.println("\n2. 性能对比演示:");System.out.println("-".repeat(50));// 测试不使用享元模式long startTime = System.currentTimeMillis();testWithoutFlyweight();long withoutFlyweightTime = System.currentTimeMillis() - startTime;// 测试使用享元模式startTime = System.currentTimeMillis();testWithFlyweight();long withFlyweightTime = System.currentTimeMillis() - startTime;System.out.println("\n⏱️ 性能对比结果:");System.out.printf("不使用享元模式: %d ms%n", withoutFlyweightTime);System.out.printf("使用享元模式: %d ms%n", withFlyweightTime);System.out.printf("性能提升: %.1f%%%n", (1 - (double) withFlyweightTime / withoutFlyweightTime) * 100);}private static void testWithoutFlyweight() {List<SimpleCharacter> characters = new ArrayList<>();CharacterInternalState format = new CharacterInternalState("宋体", 12, false, false);// 创建10000个字符对象for (int i = 0; i < 10000; i++) {char c = (char) ('A' + (i % 26));characters.add(new SimpleCharacter(c, format, i * 10, 0, "黑色"));}}private static void testWithFlyweight() {DocumentEditor editor = new DocumentEditor();CharacterInternalState format = new CharacterInternalState("宋体", 12, false, false);// 创建10000个字符,但共享享元对象for (int i = 0; i < 10000; i++) {char c = (char) ('A' + (i % 26));editor.addCharacter(c, format, i * 10, 0, "黑色");}}/*** 演示实际应用场景*/private static void demonstrateRealWorldScenario() {System.out.println("\n3. 实际应用场景演示:");System.out.println("-".repeat(50));DocumentEditor editor = new DocumentEditor();editor.generateTestDocument();editor.displayDocument();CharacterFlyweightFactory.getInstance().displayPool();}
}/*** 简单字符类 - 用于对比测试*/
class SimpleCharacter {private char character;private CharacterInternalState internalState;private int positionX;private int positionY;private String color;public SimpleCharacter(char character, CharacterInternalState internalState,int positionX, int positionY, String color) {this.character = character;this.internalState = internalState;this.positionX = positionX;this.positionY = positionY;this.color = color;}
}
完整示例:围棋游戏
让我们通过一个更复杂的围棋游戏示例来深入理解享元模式。
1. 围棋棋子享元
import java.util.HashMap;
import java.util.Map;/*** 围棋棋子享元接口*/
public interface GoPiece {/*** 显示棋子* @param externalState 外部状态(位置)*/void display(PieceExternalState externalState);/*** 获取棋子颜色*/String getColor();
}/*** 棋子内部状态*/
class PieceInternalState {private final String color; // 颜色是内部状态,可以共享private final String shape; // 形状(圆形)public PieceInternalState(String color) {this.color = color;this.shape = "圆形";}public String getColor() { return color; }public String getShape() { return shape; }@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;PieceInternalState that = (PieceInternalState) o;return Objects.equals(color, that.color) && Objects.equals(shape, that.shape);}@Overridepublic int hashCode() {return Objects.hash(color, shape);}@Overridepublic String toString() {return color + shape;}
}/*** 棋子外部状态*/
class PieceExternalState {private final int x;private final int y;public PieceExternalState(int x, int y) {this.x = x;this.y = y;}public int getX() { return x; }public int getY() { return y; }@Overridepublic String toString() {return String.format("位置(%d,%d)", x, y);}
}/*** 具体围棋棋子*/
class ConcreteGoPiece implements GoPiece {private final PieceInternalState internalState;public ConcreteGoPiece(PieceInternalState internalState) {this.internalState = internalState;}@Overridepublic void display(PieceExternalState externalState) {System.out.printf("○ %s棋子 %s%n", internalState.getColor(), externalState);}@Overridepublic String getColor() {return internalState.getColor();}public PieceInternalState getInternalState() {return internalState;}
}/*** 围棋棋子工厂*/
class GoPieceFactory {private static GoPieceFactory instance;private final Map<String, GoPiece> piecePool;private GoPieceFactory() {this.piecePool = new HashMap<>();}public static GoPieceFactory getInstance() {if (instance == null) {instance = new GoPieceFactory();}return instance;}public GoPiece getPiece(String color) {// 如果池中已存在,直接返回if (piecePool.containsKey(color)) {System.out.printf("✓ 共享%s棋子%n", color);return piecePool.get(color);}// 否则创建新的享元对象System.out.printf("➤ 创建新%s棋子%n", color);PieceInternalState internalState = new PieceInternalState(color);GoPiece piece = new ConcreteGoPiece(internalState);piecePool.put(color, piece);return piece;}public int getPoolSize() {return piecePool.size();}public void displayPool() {System.out.println("\n🎲 棋子享元池:");piecePool.forEach((color, piece) -> {System.out.printf("颜色: %s -> %s%n", color, piece.getColor());});System.out.println("总计: " + piecePool.size() + " 种棋子");}
}
2. 围棋棋盘
import java.util.ArrayList;
import java.util.List;/*** 围棋棋盘*/
public class GoBoard {private final List<PieceContext> pieces;private final GoPieceFactory factory;private static final int BOARD_SIZE = 19;public GoBoard() {this.pieces = new ArrayList<>();this.factory = GoPieceFactory.getInstance();}/*** 在棋盘上放置棋子*/public void placePiece(String color, int x, int y) {if (x < 0 || x >= BOARD_SIZE || y < 0 || y >= BOARD_SIZE) {System.out.println("❌ 无效位置: (" + x + "," + y + ")");return;}// 检查位置是否已有棋子if (hasPieceAt(x, y)) {System.out.println("❌ 位置 (" + x + "," + y + ") 已有棋子");return;}// 获取棋子享元GoPiece piece = factory.getPiece(color);// 创建外部状态PieceExternalState externalState = new PieceExternalState(x, y);// 保存棋子上下文PieceContext context = new PieceContext(piece, externalState);pieces.add(context);System.out.printf("✅ 在(%d,%d)放置%s棋子%n", x, y, color);}/*** 检查位置是否有棋子*/private boolean hasPieceAt(int x, int y) {return pieces.stream().anyMatch(context -> context.getExternalState().getX() == x && context.getExternalState().getY() == y);}/*** 显示棋盘状态*/public void displayBoard() {System.out.println("\n🎯 棋盘状态:");System.out.println("=" .repeat(40));for (PieceContext context : pieces) {context.display();}System.out.println("=" .repeat(40));System.out.printf("棋盘棋子数: %d, 享元对象数: %d%n", pieces.size(), factory.getPoolSize());}/*** 模拟一局围棋*/public void simulateGame() {System.out.println("🎮 开始模拟围棋对局...");// 黑棋先行placePiece("黑", 3, 3);placePiece("黑", 4, 4);placePiece("黑", 5, 5);// 白棋应对placePiece("白", 3, 4);placePiece("白", 4, 3);placePiece("白", 5, 4);// 继续对局placePiece("黑", 6, 6);placePiece("白", 6, 5);placePiece("黑", 7, 7);placePiece("白", 7, 6);// 尝试在已有位置放置棋子placePiece("黑", 3, 3); // 应该失败System.out.println("\n对局结束!");}
}/*** 棋子上下文*/
class PieceContext {private final GoPiece piece;private final PieceExternalState externalState;public PieceContext(GoPiece piece, PieceExternalState externalState) {this.piece = piece;this.externalState = externalState;}public void display() {piece.display(externalState);}public GoPiece getPiece() {return piece;}public PieceExternalState getExternalState() {return externalState;}
}
3. 游戏客户端
/*** 围棋游戏客户端*/
public class GoGameClient {public static void main(String[] args) {System.out.println("=== 享元模式演示 - 围棋游戏 ===\n");GoBoard board = new GoBoard();// 演示围棋游戏board.simulateGame();board.displayBoard();// 显示享元池GoPieceFactory.getInstance().displayPool();// 演示内存节省demonstrateMemorySaving();}/*** 演示内存节省效果*/private static void demonstrateMemorySaving() {System.out.println("\n💾 内存节省演示:");System.out.println("-".repeat(40));int totalPieces = 1000;GoPieceFactory factory = GoPieceFactory.getInstance();System.out.println("模拟放置 " + totalPieces + " 个棋子:");// 重置工厂// 在实际应用中,我们不会这样重置,这里只是为了演示System.out.println("创建黑白两种棋子享元...");// 模拟大量棋子放置GoPiece blackPiece = factory.getPiece("黑");GoPiece whitePiece = factory.getPiece("白");System.out.printf("实际创建的享元对象: %d 个%n", factory.getPoolSize());System.out.printf("内存节省: %d 个对象 -> %d 个享元, 节省 %.1f%% 内存%n",totalPieces, factory.getPoolSize(),(1 - (double) factory.getPoolSize() / totalPieces) * 100);}
}
实际应用示例:数据库连接池
import java.util.HashMap;
import java.util.Map;/*** 数据库连接池 - 享元模式实际应用*/
public class ConnectionPool {private static ConnectionPool instance;private final Map<String, DatabaseConnection> connectionPool;private final int maxPoolSize;private ConnectionPool(int maxPoolSize) {this.connectionPool = new HashMap<>();this.maxPoolSize = maxPoolSize;System.out.println("初始化数据库连接池,最大连接数: " + maxPoolSize);}public static ConnectionPool getInstance(int maxPoolSize) {if (instance == null) {instance = new ConnectionPool(maxPoolSize);}return instance;}/*** 获取数据库连接*/public DatabaseConnection getConnection(String databaseUrl, String username) {String key = databaseUrl + "|" + username;// 如果连接已存在且可用,直接返回if (connectionPool.containsKey(key) && connectionPool.get(key).isAvailable()) {System.out.println("✓ 复用数据库连接: " + key);return connectionPool.get(key);}// 如果连接池已满,等待或抛出异常if (connectionPool.size() >= maxPoolSize) {System.out.println("❌ 连接池已满,无法创建新连接");return null;}// 创建新连接System.out.println("➤ 创建新数据库连接: " + key);DatabaseConnection connection = new DatabaseConnection(databaseUrl, username);connectionPool.put(key, connection);return connection;}/*** 获取连接池状态*/public void displayPoolStatus() {System.out.println("\n📊 连接池状态:");System.out.println("当前连接数: " + connectionPool.size());System.out.println("最大连接数: " + maxPoolSize);System.out.println("使用率: " + (connectionPool.size() * 100 / maxPoolSize) + "%");}
}/*** 数据库连接 - 享元对象*/
class DatabaseConnection {private final String databaseUrl;private final String username;private boolean inUse;public DatabaseConnection(String databaseUrl, String username) {this.databaseUrl = databaseUrl;this.username = username;this.inUse = false;System.out.println("建立到 " + databaseUrl + " 的连接,用户: " + username);}public void executeQuery(String sql) {if (!inUse) {inUse = true;System.out.println("执行查询: " + sql);// 模拟查询执行try {Thread.sleep(100);} catch (InterruptedException e) {Thread.currentThread().interrupt();}inUse = false;} else {System.out.println("连接正在使用中,无法执行查询");}}public boolean isAvailable() {return !inUse;}public String getConnectionInfo() {return databaseUrl + " (" + username + ")";}
}
享元模式的优点
1. 大幅减少内存使用
// 没有享元模式:创建1000个独立对象
// 使用享元模式:只创建2个享元对象(黑白棋子)
// 内存节省:998个对象!
2. 提高性能
// 对象创建和垃圾回收开销大大减少
// 特别适合大量细粒度对象的场景
3. 更好的对象管理
// 通过工厂统一管理享元对象
// 可以轻松实现对象池等功能
享元模式的缺点
1. 增加系统复杂度
// 需要区分内部状态和外部状态
// 增加了设计和实现的难度
2. 可能引入线程安全问题
// 如果享元对象有状态,需要考虑线程安全
// 外部状态需要由客户端正确管理
适用场景
- 系统中有大量相似对象时
- 对象的大部分状态可以外部化时
- 需要缓冲池的场景
- 需要实现对象共享的场景
最佳实践
1. 合理划分内部状态和外部状态
// 内部状态:不变的、可共享的
// 外部状态:变化的、不可共享的
2. 使用工厂管理享元对象
public class FlyweightFactory {// 统一管理享元对象的创建和获取// 确保对象的正确共享
}
3. 考虑线程安全性
// 如果享元对象有状态变化,需要同步控制
public synchronized Flyweight getFlyweight(String key) {// 线程安全的获取方式
}
享元模式 vs 其他模式
| 模式 | 目的 | 特点 |
|---|---|---|
| 享元模式 | 对象共享 | 减少内存使用,提高性能 |
| 单例模式 | 唯一实例 | 确保一个类只有一个实例 |
| 原型模式 | 对象复制 | 通过复制现有对象创建新对象 |
总结
享元模式就像是"资源共享中心",让相同的资源可以被多次使用,避免重复创建。
核心价值:
- 大幅减少内存中对象的数量
- 提高系统性能
- 更好地管理相似对象
使用场景:
- 系统中有大量相似对象时
- 对象创建开销很大时
- 需要实现对象池功能时
简单记忆:
大量对象不用愁,享元模式来帮忙!
内部状态可共享,外部状态客户端扛。
掌握享元模式,能够让你在处理大量相似对象时游刃有余,显著提升系统性能!
