一、享元模式概述
\quad 在软件设计中,享元模式(Flyweight Pattern)的核心思想是通过共享来有效地支持大量细粒度对象的重用。这里的"享"体现在共享,"元"则体现在这些可以共享的基本元素上。正如共享单车系统一样,享元模式会维护一个对象池,其中存储可以复用的对象,当需要时直接从池中获取,而不是重新创建。
\quad 上图展示了享元模式的基本架构,我们可以看到多个客户端都在复用对象池中的共享对象。这些共享对象具有两种状态:
- 内部状态:对象可共享的、固定不变的属性,就像自行车的基本构造。
- 外部状态:对象不可共享的、随环境改变的属性,就像自行车的位置和使用状态。
\quad 这种模式特别适合处理需要创建大量相似对象的场景。通过识别对象的内部状态和外部状态,将可共享的部分集中管理,不仅可以显著减少内存占用,还能提升系统性能。
二、享元模式分类
\quad 在享元模式中,根据对象是否可以共享,我们可以将享元分为两种类型:共享享元和非共享享元,就像上图所展示的那样。
- 共享享元是享元模式的核心,它代表那些可以被多个环境共享使用的对象。这类对象的特点是它们的内部状态是一致的,不会因为使用环境的改变而改变。就像围棋中的黑白棋子,每个黑棋的颜色和形状都是完全相同的,我们没必要为每个位置都创建新的棋子对象,而是可以共享使用现有的棋子,只需要改变它们的位置信息即可。
- 非共享享元则是那些不能被共享的对象。这类对象可能具有特定的、不可共享的状态。虽然它们不共享,但仍然可以通过享元工厂来统一管理。比如在文字编辑器中,每个字符的字体样式可能都不相同,这时就需要使用非共享享元来处理这些特殊情况。
\quad 共享享元和非共享享元经常一起使用,它们各自处理不同的业务场景。共享享元主要用于那些需要大量创建相似对象的场景,通过共享来减少内存占用;而非共享享元则用于处理那些虽然结构相似,但状态必须独立的对象。
三、享元模式角色组成
\quad 如上图所示,享元模式主要由四个核心角色组成,它们共同协作来实现对象的高效共享和管理。
- Flyweight(享元接口)是所有具体享元类的公共接口,它定义了享元对象需要实现的方法。这个接口通常包含一个传入外部状态的操作方法,使享元对象能够根据外部状态改变其行为。
- ConcreteFlyweight(具体享元类)是实现了Flyweight接口的具体类。它包含内部状态,也就是那些可以共享的、不会随环境改变的信息。例如,在围棋程序中,棋子的颜色就是内部状态。这个类的实例会被多个客户端共享使用。
- UnsharedConcreteFlyweight(非共享具体享元类)也实现了Flyweight接口,但它的实例不会被共享。这个类包含了不能共享的状态信息。比如在文本编辑器中,虽然字符’A’可以被共享,但如果这个’A’有特殊的样式,就需要使用非共享享元来处理。
- FlyweightFactory(享元工厂)负责创建和管理享元对象。它通常维护一个享元池(用Map实现),用于存储已创建的享元对象。当客户端请求一个享元对象时,工厂会先检查池中是否存在满足要求的对象,如果存在就直接返回,否则才创建新的对象。这保证了相同内部状态的享元对象只会被创建一次。
四、享元模式案例
\quad 让我们通过实现一个简单的围棋程序来深入理解享元模式。在围棋中,棋子只有黑白两色,但要放置在棋盘的不同位置上。这里棋子的颜色就是内部状态,可以被共享;而位置则是外部状态,需要在使用时指定。
图片
\quad 首先定义棋子的位置类:
// 棋子位置类-外部状态
public class Position {private int x;private int y;public Position(int x, int y) {this.x = x;this.y = y;}public int getX() { return x; }public int getY() { return y; }
}
\quad 接下来定义围棋的棋子共享接口以及实现类:
// 围棋棋子接口
public interface GoChessPiece {void display(Position position);
}
// 具体的围棋棋子类
public class ConcreteChessPiece implements GoChessPiece {private String color; // 内部状态public ConcreteChessPiece(String color) {this.color = color;}@Overridepublic void display(Position position) {System.out.printf("棋子颜色:%s,位置:(%d, %d)%n", color, position.getX(), position.getY());}
}
\quad 然后是工厂类:
import java.util.HashMap;
import java.util.Map;public class GoChessPieceFactory {private static final Map<String, GoChessPiece> pieces = new HashMap<>();public static GoChessPiece getChessPiece(String color) {GoChessPiece piece = pieces.get(color);if (piece == null) {piece = new ConcreteChessPiece(color);pieces.put(color, piece);}return piece;}
}
\quad 使用示例:
public class Client {public static void main(String[] args) {// 获取白色棋子并放在(2, 3)位置GoChessPiece white1 = GoChessPieceFactory.getChessPiece("白色");white1.display(new Position(2, 3));// 获取另一个白色棋子放在(3, 6)位置GoChessPiece white2 = GoChessPieceFactory.getChessPiece("白色");white2.display(new Position(3, 6));// 判断是否是同一个对象System.out.println("两个白棋是否共享同一个对象:" + (white1 == white2));}
}
\quad 测试结果:
\quad 在这个案例中,我们可以看到:
-
棋子的颜色(内部状态)被共享,每种颜色的棋子只会创建一个对象
-
棋子的位置(外部状态)在使用时由客户端指定
五、享元模式优缺点
优点
\quad 享元模式的核心优势是通过共享对象来减少内存占用,特别适合需要创建大量相似对象的场景。使用享元模式可以集中管理可复用的对象,使得对象的创建和维护更加规范和高效。
缺点
\quad 这种模式增加了系统的复杂度,需要额外的工厂类来管理对象池,同时还需要仔细区分内部状态和外部状态。在对象数量较少的场景下,这种模式带来的收益可能无法抵消其带来的开发成本。
六、享元模式适用场景
\quad 享元模式最适合应用在系统需要创建大量相似对象,且这些对象可以分离出共享部分的场景。典型的应用场景包括:
- 文字编辑器中的字符渲染:相同的字符可以共享字形信息,只需要改变位置和样式
- 游戏中的素材管理:相同的游戏素材(如树木、建筑)可以在不同位置重复使用
- 地图应用中的图标:相同类型的地标可以共享图标资源
- 网页中的图片缓存:相同的图片可以在多处被重复使用
\quad 当系统中存在大量重复对象,且这些对象的大部分状态都可以外部化时,使用享元模式可以显著降低内存占用并提高性能。
七、总结
\quad 享元模式通过对象共享来提高系统性能,是一种以时间换空间的设计模式。它将对象的状态分为内部状态和外部状态,通过共享内部状态来减少对象创建。在实现时,需要通过享元工厂来统一管理对象池,确保相同内部状态的对象只被创建一次。这种模式特别适合需要大量创建相似对象的场景,但在使用时需要权衡其带来的复杂性和收益。