软件设计模式系列之十三——享元模式
1 模式的定义
享元模式(Flyweight Pattern)是一种结构型设计模式,它旨在减少内存占用或计算开销,通过共享大量细粒度对象来提高系统的性能。这种模式适用于存在大量相似对象实例,但它们的状态可以外部化(extrinsic),并且可以在多个对象之间共享的情况。
2 举例说明
为了更好地理解享元模式,让我们举一些现实生活中的例子。
咖啡店的咖啡杯和碟子的例子。在咖啡店中,咖啡杯和碟子通常具有相同的设计和形状,但它们可能具有不同的颜色或图案。咖啡店可以使用享元模式来共享相同设计的杯子和碟子,以减少存储和管理的成本。
公共交通卡的例子。城市中的公共交通卡(如地铁卡、公共汽车卡)通常具有相同的功能和外观,但每张卡可能包含不同的余额和个人信息。这些卡可以被视为享元对象,公共交通系统可以共享卡的通用功能。
电子书阅读器的字体和样式的例子。电子书阅读器可以使用享元模式来管理字体、字号和样式。多本电子书可以共享相同的字体和样式设置,以提供一致的阅读体验。
这些例子都涉及到具有相似属性和功能的对象,它们可以通过享元模式来共享通用部分,从而减少资源消耗并提高效率。这在设计和生产中可以节省时间和成本。
3 结构
享元模式的结构包括以下主要组件:
享元工厂(Flyweight Factory):享元工厂负责创建和管理享元对象。它维护一个享元池,其中包含已经创建的享元对象,并根据客户端请求共享已经存在的对象或创建新的享元对象。
享元接口(Flyweight Interface):享元接口是享元对象的抽象,通常声明了享元对象的公共方法,以便客户端能够访问和操作享元对象。
具体享元(Concrete Flyweight):具体享元是享元接口的实现,包含了内部状态和外部状态。内部状态是可以被共享的,而外部状态是不可共享的,它在运行时传递给享元对象。
客户端(Client):客户端是使用享元模式的应用程序或模块,它通过享元工厂来获取或共享享元对象,并根据需要传递外部状态。
4 实现步骤
要实现享元模式,可以按照以下步骤进行操作:
确定内部状态和外部状态:首先,确定对象的内部状态和外部状态。内部状态是可以被多个对象共享的部分,而外部状态是不可共享的。
创建享元接口:定义享元接口,声明享元对象的公共方法,包括操作内部状态和外部状态的方法。
创建具体享元类:实现具体享元类,它包含了内部状态和外部状态的具体实现。内部状态可以在多个对象之间共享,而外部状态需要在运行时传递。
创建享元工厂:创建享元工厂,负责创建和管理享元对象。享元工厂可以维护一个享元池,用于存储已经创建的享元对象。
客户端使用享元对象:在客户端中,通过享元工厂来获取或共享享元对象。客户端需要提供外部状态作为参数,并根据需要操作享元对象。
5 代码实现
以下是一个简单的 Java 代码示例,演示了如何使用享元模式来实现公共交通卡的共享功能。在这个示例中,我们创建了一个 TransportCardFactory 工厂类来管理交通卡对象,以及一个 TransportCard 接口表示交通卡。
// 1. 定义交通卡接口
interface TransportCard {
void swipe();
}
// 2. 创建具体的交通卡类
class SubwayCard implements TransportCard {
private String ownerName;
private int balance;
public SubwayCard(String ownerName) {
this.ownerName = ownerName;
this.balance = 0;
}
public void swipe() {
System.out.println("刷地铁卡,扣除票价,余额:" + balance);
}
}
// 3. 创建享元工厂类
class TransportCardFactory {
private Map<String, TransportCard> cards = new HashMap<>();
public TransportCard getCard(String ownerName) {
if (cards.containsKey(ownerName)) {
System.out.println("使用现有的交通卡:" + ownerName);
return cards.get(ownerName);
} else {
System.out.println("创建新的交通卡:" + ownerName);
TransportCard card = new SubwayCard(ownerName);
cards.put(ownerName, card);
return card;
}
}
}
// 4. 客户端代码
public class Client {
public static void main(String[] args) {
TransportCardFactory cardFactory = new TransportCardFactory();
// 乘客1刷卡
TransportCard card1 = cardFactory.getCard("zhanngsan");
card1.swipe();
// 乘客2刷卡
TransportCard card2 = cardFactory.getCard("lisi");
card2.swipe();
// 再次刷卡
TransportCard card3 = cardFactory.getCard("zhanngsan");
card3.swipe();
}
}
在这个示例中,我们首先定义了 TransportCard 接口,表示交通卡的通用功能。然后,我们创建了一个具体的交通卡类 SubwayCard,它实现了 TransportCard 接口,并包含了特定于地铁卡的属性。
接下来,我们创建了享元工厂类 TransportCardFactory,它负责管理和共享交通卡对象。当客户端需要一个交通卡时,工厂类会首先检查是否已经存在具有相同拥有者姓名的卡,如果存在则返回现有的卡,否则创建一个新的卡对象。
最后,我们在客户端代码中演示了如何使用享元模式,创建并刷卡,观察到当两位乘客使用相同姓名刷卡时,会共享同一个交通卡对象,从而减少了卡对象的创建和内存占用。
6 典型应用场景
6.1 享元模式通常在以下情况下得到广泛应用
-
大量对象。当系统中存在大量相似对象实例时,使用享元模式可以显著减少内存占用,因为相似对象的内部状态可以共享。
-
内部状态与外部状态。当对象可以分为内部状态和外部状态时,享元模式特别有用。内部状态是对象的固定部分,可以被多个对象共享,而外部状态是对象的可变部分,每个对象可以根据需要个性化。
-
性能优化。在需要高性能和低内存消耗的情况下,享元模式可以用于共享重复使用的对象,从而提高系统的性能。
-
缓存管理。在需要缓存大量对象以提高系统响应时间的情况下,可以使用享元模式来管理缓存对象。
-
资源池管理。当需要管理共享资源池(如数据库连接池、线程池)中的资源对象时,享元模式可以用于有效地共享和重用资源。
享元模式在需要管理大量相似对象、共享内部状态、提高性能和减少内存占用的情况下非常有用。它允许对象在不同上下文中共享内部状态,而外部状态可以根据需要进行个性化定制。通过合理使用享元模式,可以改善系统的效率和资源利用率。
6.2 java中的字符串应用享元模式场景
在Java中,字符串是使用享元模式的经典示例。享元模式的核心思想是共享相似对象的内部状态,以减少内存占用。字符串的使用正是基于这个思想。
下面是Java中字符串如何使用享元模式的一些关键特点:
不可变性:Java中的字符串是不可变的,也就是说一旦创建了一个字符串对象,它的值就不能被修改。这意味着如果两个字符串具有相同的字符序列,它们可以共享相同的内部字符数组。
字符串常量池:Java维护了一个字符串常量池(String Pool),用于存储字符串字面量。当你创建一个字符串字面量时,Java会首先检查常量池中是否已经存在相同值的字符串。如果存在,它将返回常量池中的字符串引用,而不会创建新的对象。
共享相同的字符串对象:由于字符串的不可变性和字符串常量池的存在,多个字符串变量可以共享相同的字符串对象。这意味着如果你有多个字符串变量引用相同的字符串值,它们实际上共享同一个字符串对象。
下面是一个示例,演示了字符串如何使用享元模式:
String s1 = "Hello"; // 创建一个字符串字面量,存储在常量池中
String s2 = "Hello"; // 与s1共享相同的字符串对象
String s3 = new String("Hello"); // 创建一个新的字符串对象,不存储在常量池中
String s4 = new String("Hello"); // 创建另一个新的字符串对象,也不存储在常量池中
System.out.println(s1 == s2); // true,s1和s2共享相同的字符串对象
System.out.println(s1 == s3); // false,s1和s3引用不同的字符串对象
在上面的示例中,s1 和 s2 共享相同的字符串对象,因为它们引用相同的字符串字面量,而 s3 和 s4 创建了新的字符串对象,因为它们使用了 new 操作符。这种共享内部状态的方式减少了内存占用,并提高了性能,特别是当处理大量字符串时。
Java中的字符串是一个典型的享元模式的例子,通过不可变性和字符串常量池,它实现了字符串对象的共享,以减少内存占用和提高性能。这种设计对于处理字符串操作非常高效,并且保证了字符串值的安全性,因为它们不可被修改。
7 优缺点
享元模式具有一些优点和缺点,让我们来看看:
优点:
减少内存占用,享元模式通过共享相似对象的内部状态,可以大大减少内存占用,提高系统的性能和效率。提高性能,通过共享对象,减少了对象的创建和销毁,从而提高了系统的性能。分离内部状态和外部状态,享元模式允许将内部状态和外部状态分开,外部状态可以在运行时传递给享元对象,使系统更灵活。
缺点:
增加复杂性,享元模式引入了共享对象和外部状态的概念,可能增加了系统的复杂性。可能导致线程安全问题,如果多个线程同时访问共享对象并修改其外部状态,可能会导致线程安全问题。
8 类似模式
享元模式通常与其他设计模式一起使用,以解决更复杂的问题或实现更全面的系统。以下是一些常见的设计模式,以及如何与享元模式一起使用它们。
工厂模式。享元模式通常需要一个工厂来创建和管理共享对象。你可以使用工厂模式来创建享元对象,确保对象的创建和初始化过程是封装的,并且客户端不需要直接创建对象。
单例模式。在享元模式中,享元工厂可以是一个单例,以确保只有一个享元工厂实例用于管理共享对象。这样可以确保对象的唯一性和一致性。
装饰者模式:装饰者模式可以与享元模式一起使用,以动态地添加功能或状态到享元对象。装饰者模式允许你在不改变对象结构的情况下,为对象添加额外的行为。
代理模式:代理模式可以与享元模式一起使用,以提供对享元对象的访问控制或延迟加载。代理可以用于监控或限制对共享对象的访问。
组合模式:组合模式用于将对象组织成树形结构,享元模式可以用于共享组合中的相似对象,以减少内存占用。这在图形和图像处理应用中特别有用。
享元模式可以与许多其他设计模式一起使用,具体取决于系统的需求。它通常与创建型模式(如工厂模式、单例模式)和结构型模式(如装饰者模式、代理模式)结合使用,以实现更灵活、高效和可维护的系统。在实际应用中,将多种模式结合使用可以更好地满足复杂系统的需求。
9 小结
享元模式是一种有助于减少内存占用和提高系统性能的结构型设计模式。通过共享大量细粒度的对象,它可以有效地降低系统的资源消耗,特别适用于存在大量相似对象的场景。在设计和开发中,当需要创建大量相似对象时,可以考虑使用享元模式以提高系统的效率和性能。这种模式的核心思想是将对象的内部状态与外部状态分离,从而实现对象的共享和复用。