在软件开发中,当系统需要创建大量相似对象时,内存消耗会急剧增加,可能导致系统性能下降。享元模式正是为解决这一问题而生,它通过共享技术实现对象的复用,有效减少对象数量,降低内存占用,提升系统效率。
享元模式是一种结构型设计模式,其核心思想是通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率。
这里的 “享元” 指的是可以被多个对象共享的单元。比如在一个文字处理软件中,同一字体和大小的字符会频繁出现,如果为每个字符都创建一个独立对象,会浪费大量内存。而享元模式会将这些相同的字符对象进行共享,多个位置引用同一个对象,大大减少了对象的创建数量。
再比如围棋游戏中,棋盘上有大量的黑白棋子,它们除了位置不同外,其他属性(颜色)都相同。使用享元模式,就可以只创建两个棋子对象(黑棋和白棋),然后通过不同的位置信息来区分它们在棋盘上的不同位置,避免创建 361 个(围棋棋盘共有 361 个交叉点)棋子对象。
享元模式的核心原理是分离对象的内部状态和外部状态,通过共享内部状态相同的对象来实现复用。每个状态的含义如下:
(1)内部状态:是对象可共享的部分,它不随外部环境的变化而变化,存储在享元对象内部。比如棋子的颜色,对于围棋来说,黑和白两种颜色是固定的,属于内部状态。
(2)外部状态:是对象不可共享的部分,它会随外部环境的变化而变化,需要由客户端在使用时传入。比如棋子在棋盘上的位置,会根据落子的位置不同而变化,属于外部状态。
享元模式的结构主要包含以下几个部分:
(1)抽象享元角色:定义享元对象的接口或抽象类,声明了公共方法,这些方法可以向外界提供享元对象的内部状态,或者接收外部状态并进行相应处理。
(2)具体享元角色:实现抽象享元角色所定义的接口,存储内部状态。具体享元对象是可以被共享的,它需要确保内部状态的独立性,不会受到外部状态的影响。
(3)享元工厂角色:负责创建和管理享元对象,它会缓存已经创建的享元对象,当客户端需要使用享元对象时,首先从缓存中查找,如果存在则直接返回,如果不存在则创建新的享元对象并存入缓存。
(4)客户端角色:负责创建或获取享元对象,并为享元对象设置外部状态。
享元工厂就像一个对象仓库,当客户端需要对象时,先去仓库里找,有合适的就直接拿来用,没有再新造一个并存到仓库里,这样就实现了对象的共享。
享元模式在系统设计中有着重要的作用,主要体现在以下几个方面:
(1)减少对象数量:通过共享相似对象,大大减少了系统中需要创建的对象数量,避免了大量对象对内存资源的消耗。
(2)降低内存占用:由于减少了对象的数量,相应地降低了内存的占用,提高了系统的内存使用效率,有助于缓解内存紧张的问题。
(3)提高系统性能:减少对象创建和销毁的次数,降低了系统的开销,从而提高了系统的运行性能,使系统更加高效稳定。
(4)分离内部状态和外部状态:明确区分了对象的内部状态和外部状态,使内部状态可以共享,外部状态由客户端管理,增强了系统的灵活性。
享元模式的优缺点主要有:
(一)优点
(1)节省内存资源:这是享元模式最显著的优点,通过共享对象,减少了对象的创建数量,极大地节省了内存空间,尤其在需要创建大量相似对象的场景中效果明显。
(2)提高系统性能:减少了对象的创建和销毁操作,降低了系统的运行开销,使系统能够更高效地处理任务。
(3)便于集中管理对象:通过享元工厂对享元对象进行统一管理,便于对对象进行维护和控制,比如可以在工厂中对对象的创建和销毁进行监控和优化。
(二)缺点
(1)增加系统复杂度:需要分离对象的内部状态和外部状态,这会使系统的设计变得更加复杂,增加了理解和实现的难度。
(2)外部状态管理成本高:外部状态需要由客户端来维护和传递,当外部状态较为复杂时,会增加客户端的代码复杂度和管理成本。
(3)可能影响系统灵活性:由于享元对象需要共享内部状态,在某些情况下可能会限制对象的个性化定制,影响系统的灵活性。
下面以围棋游戏为例来演示享元模式的实现。围棋中有大量的黑白棋子,我们可以通过享元模式来共享这些棋子对象,只创建两个具体享元对象(黑棋和白棋),然后通过外部状态(位置)来区分它们在棋盘上的不同位置。
// 棋子接口(抽象享元角色)
interface ChessPiece {
// 落子,需要传入外部状态(位置)
void put(int x, int y);
}
// 黑棋(具体享元角色)
class BlackChessPiece implements ChessPiece {
// 内部状态:颜色
private String color = "黑色";
@Override
public void put(int x, int y) {
System.out.println("在位置(" + x + "," + y + ")放置" + color + "棋子");
}
}
// 白棋(具体享元角色)
class WhiteChessPiece implements ChessPiece {
// 内部状态:颜色
private String color = "白色";
@Override
public void put(int x, int y) {
System.out.println("在位置(" + x + "," + y + ")放置" + color + "棋子");
}
}
// 棋子工厂(享元工厂角色)
class ChessPieceFactory {
// 缓存享元对象
private static final Map<String, ChessPiece> chessPieces = new HashMap<>();
// 获取享元对象
public static ChessPiece getChessPiece(String color) {
ChessPiece chessPiece = chessPieces.get(color);
// 如果缓存中没有,则创建新的享元对象并放入缓存
if (chessPiece == null) {
if ("黑色".equals(color)) {
chessPiece = new BlackChessPiece();
} else if ("白色".equals(color)) {
chessPiece = new WhiteChessPiece();
}
chessPieces.put(color, chessPiece);
}
return chessPiece;
}
}
// 客户端
public class Client {
public static void main(String[] args) {
// 获取黑棋对象并落子
ChessPiece black1 = ChessPieceFactory.getChessPiece("黑色");
black1.put(1, 1);
ChessPiece black2 = ChessPieceFactory.getChessPiece("黑色");
black2.put(2, 2);
// 获取白棋对象并落子
ChessPiece white1 = ChessPieceFactory.getChessPiece("白色");
white1.put(3, 3);
ChessPiece white2 = ChessPieceFactory.getChessPiece("白色");
white2.put(4, 4);
// 判断两个黑棋对象是否为同一个
System.out.println("black1 和 black2 是否为同一个对象:" + (black1 == black2));
// 判断两个白棋对象是否为同一个
System.out.println("white1 和 white2 是否为同一个对象:" + (white1 == white2));
}
}
运行结果为:
在位置(1,1)放置黑色棋子
在位置(2,2)放置黑色棋子
在位置(3,3)放置白色棋子
在位置(4,4)放置白色棋子
black1 和 black2 是否为同一个对象:true
white1 和 white2 是否为同一个对象:true
从代码和运行结果可以看出,我们通过享元模式只创建了一个黑棋对象和一个白棋对象,多次获取相同颜色的棋子时,得到的是同一个对象,实现了对象的共享。棋子的颜色是内部状态,由享元对象自身存储;落子的位置是外部状态,由客户端在调用put方法时传入。当需要新增棋子时,只需传入不同的位置即可,无需创建新的棋子对象,有效减少了对象数量,节省了内存。