享元模式
目录
本文的结构如下:
- 引言
- 什么是享元模式
- 模式的结构
- 典型代码
- 代码示例
- 单纯享元模式和复合享元模式
- 模式扩展
- 优点和缺点
- 适用环境
- 模式应用
一、引言
衣服小了,没有办法只能买新的,衣服破了一个小口,无伤大雅,则可以穿针引线缝补妥当。如果是黑色的衣服,选上黑色的细线是合适的,灰色的衣服配上灰色的细线是适宜的,白色的衣服搭上白色的细线也是恰好的......至于针,一直就是那根针。
在软件开发中,也有差不多的情况,比如一款电子围棋游戏,有很多白子黑子,除了颜色和位置不同外,棋子其它都是一样的,为每个棋子实例化一个对象显然是一种浪费,像“针线活”中的“针”一样被共享,才是教科学的做法,毕竟“勤俭持家”是我们的美德。当存在多个相同对象的时候,可以通过共享对象进而减少相同对象创建引起的内存消耗,提高程序性能。这就是设计模式中的享元模式。
二、什么是享元模式
“享”是共享的意思,“元”指的是元件,也就是小颗粒的东西,享元顾名思义便是共享小部件,很多系统或者程序包含大量对象,但是这些对象绝大多数都是差不多的,除了一些极个别的属性外。当一个软件系统在运行时产生的对象数量太多,将导致运行代价过高,带来系统性能下降等问题。享元模式正为解决这一类问题而诞生。。
享元模式以共享的方式高效地支持大量细粒度对象的重用,在享元模式中,存储这些共享实例对象的地方称为享元池(Flyweight Pool)。享元对象能做到共享的关键是区分了内部状态(Intrinsic State)和外部状态(Extrinsic State)。
- 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。就像“针线活”中的针。
- 外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。就像“针线活”中的线。
正因为区分了内部状态和外部状态,可以将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以实现共享的,需要的时候就将对象从享元池中取出,实现对象的复用。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内存中实际上只存储一份。
享元模式定义如下:
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
三、模式的结构
享元模式一般结合工厂模式一起使用,在它的结构图中包含了一个享元工厂类,UML类图如下:
在享元模式结构图中包含如下几个角色:
- Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。
四、典型代码
在享元模式中引入了享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,当用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。
public class FlyweightFactory {
//定义一个HashMap用于存储享元对象,实现享元池
private static final Map<String, Flyweight> FLYWEIGHTS = new ConcurrentHashMap();
private static final FlyweightFactory INSTANCE = new FlyweightFactory();
private FlyweightFactory(){}
public static FlyweightFactory getInstance(){
return INSTANCE;
}
public Flyweight getFlyweight(String key){
//如果对象存在,则直接从享元池获取
if (FLYWEIGHTS.containsKey(key)){
return FLYWEIGHTS.get(key);
}else {
//如果对象不存在,先创建一个新的对象添加到享元池中,然后返回
Flyweight flyweight = new ConcreteFlyweight("intrinsicState");
FLYWEIGHTS.put(key, flyweight);
return flyweight;
}
}
}
享元类的设计是享元模式的要点之一,在享元类中要将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量,而外部状态通过注入的方式添加到享元类中。典型代码如下:
public interface Flyweight {
String getIntrinsicState();
void operation(String extrinsicState);
}
public class ConcreteFlyweight implements Flyweight {
private String intrinsicState;
public ConcreteFlyweight(String intrinsicState){
this.intrinsicState = intrinsicState;
}
public String getIntrinsicState() {
return intrinsicState;
}
public void operation(String extrinsicState) {
System.out.println(extrinsicState);
}
}
五、代码示例
这里以扑克牌为例,假设没有大小王,一副扑克牌有52张。
5.1、不用享元模式
public class Card {
private String color;//花色
private String num;//点数
public Card(String color, String num){
this.color = color;
this.num = num;
}
public String getColor() {
return color;
}
public String getNum() {
return num;
}
public void setColor(String color) {
this.color = color;
}
public void setNum(String num) {
this.num = num;
}
public String toString(){
return "Card[牌色=" + color + ",牌数=" + num + "]";
}
}
随机分配4张:
public class Game {
public static void main(String[] args) {
String[] colors = {"黑桃", "草花", "红桃", "方块"};
List<Card> cards = new LinkedList<Card>();
for (int i=0; i<4; i++){
for (int j=1; j<=13; j++){
switch (j){
case 11:
cards.add(new Card(colors[i], "J"));
break;
case 12:
cards.add(new Card(colors[i], "Q"));
break;
case 13:
cards.add(new Card(colors[i], "K"));
break;
default:
cards.add(new Card(colors[i], String.valueOf(j)));
break;
}
}
}
System.out.println("随机分四张牌:");
for (int i=0; i<4; i++){
System.out.println(cards.get((int) (Math.random()*52)));
}
}
}
结果是:
随机分四张牌:
Card[牌色=方块,牌数=3]
Card[牌色=红桃,牌数=K]
Card[牌色=红桃,牌数=J]
Card[牌色=红桃,牌数=7]
显然这里要创建52个Card对象,但这里花色只有四种四固定的,不同的是大小,可以用享元模式来共享对象,减少内存消耗。
5.2、使用享元模式
抽象卡牌类:
public abstract class Card {
public abstract void showCards(String num);//享元类外部状态通过参数传入
}
具体卡牌类:
public class SpadeCard extends Card {
private String color = "黑桃";
public void showCards(String num) {
System.out.println("Card[牌色=" + color + ",牌数=" + num + "]");
}
}
public class ClubCard extends Card {
private String color = "草花";
public void showCards(String num) {
System.out.println("Card[牌色=" + color + ",牌数=" + num + "]");
}
}
public class HeartsCard extends Card {
private String color = "红桃";
public void showCards(String num) {
System.out.println("Card[牌色=" + color + ",牌数=" + num + "]");
}
}
public class DiamondsCard extends Card {
private String color = "方块";
public void showCards(String num) {
System.out.println("Card[牌色=" + color + ",牌数=" + num + "]");
}
}
享元工厂:
public class CardFactory {
public static final int SPADE = 1;
public static final int CLUB = 2;
public static final int HEARTS = 3;
public static final int DIAMONDS = 4;
private static Map<Integer, Card> cards = new ConcurrentHashMap<Integer, Card>();
private static CardFactory instance = new CardFactory();
private CardFactory(){}
public static CardFactory getInstance(){
return instance;
}
public Card getCard(Integer color){
if (cards.containsKey(color)){
System.out.println("复用对象");
return cards.get(color);
}else {
System.out.println("新建对象");
Card card;
switch (color){
case SPADE: card = new SpadeCard();break;
case CLUB: card = new ClubCard();break;
case HEARTS:card = new HeartsCard();break;
default:card = new DiamondsCard();break;
}
cards.put(color, card);
return card;
}
}
}
测试随机发10张:
public class Game {
public static void main(String[] args) {
CardFactory factory = CardFactory.getInstance();
for (int i=0; i<10; i++ ){
Card card = null;
//随机花色
switch ((int)(Math.random()*4)){
case 0: card = factory.getCard(CardFactory.SPADE);break;
case 1: card = factory.getCard(CardFactory.CLUB);break;
case 2: card = factory.getCard(CardFactory.HEARTS);break;
case 3: card = factory.getCard(CardFactory.DIAMONDS);break;
}
//随机大小
if (card != null){
int num = (int)(Math.random()*13) + 1;
switch (num){
case 11: card.showCards("J");break;
case 12: card.showCards("Q");break;
case 13: card.showCards("K");break;
default: card.showCards(String.valueOf(num));break;
}
}
}
}
}
结果是:
新建对象
Card[牌色=方块,牌数=2]
新建对象
Card[牌色=红桃,牌数=6]
复用对象
Card[牌色=红桃,牌数=6]
新建对象
Card[牌色=草花,牌数=3]
复用对象
Card[牌色=方块,牌数=K]
新建对象
Card[牌色=黑桃,牌数=7]
复用对象
Card[牌色=黑桃,牌数=2]
复用对象
Card[牌色=黑桃,牌数=6]
复用对象
Card[牌色=方块,牌数=J]
复用对象
Card[牌色=红桃,牌数=7]
这里有拿到相同的花色和大小,因为这里的random并没有去重复,不是很严谨,只是为了举例说明。
六、单纯享元模式和复合享元模式
标准的享元模式结构图中既包含可以共享的具体享元类,也包含不可以共享的非共享具体享元类(不共享的一半直接实例化即可)。但是在实际使用过程中,我们有时候会用到两种特殊的享元模式:单纯享元模式和复合享元模式。
6.1、单纯享元模式
在单纯享元模式中,所有的具体享元类都是可以共享的,不存在非共享具体享元类。它的UML类图如下:
6.2、复合享元模式
在单纯享元模式中,所有的享元对象都是单纯享元对象,也就是说都是可以直接共享的。而还有一种较为复杂的情况,将一些单纯享元使用合成模式加以复合,形成复合享元对象。这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享。它的UML类图如下:
通过复合享元模式,可以确保复合享元类CompositeConcreteFlyweight中所包含的每个单纯享元类ConcreteFlyweight都具有相同的外部状态,而这些单纯享元的内部状态往往可以不同。如果希望为多个内部状态不同的享元对象设置相同的外部状态,可以考虑使用复合享元模式。
这时的享元工厂一半有两个方法,一种用于提供单纯享元对象,另一种用于提供复合享元对象。
public class CompositeConcreteFlyweight implements Flyweight {
private List<Flyweight> flyweights = new ArrayList<Flyweight>();
public void add(Flyweight flyweight){
flyweights.add(flyweight);
}
public void remove(Flyweight flyweight){
flyweights.remove(flyweight);
}
public void operation(String extrinsicState) {
for (Flyweight flyweight : flyweights){
flyweight.operation(extrinsicState);
}
}
}
public class FlyweightFactory {
//定义一个HashMap用于存储享元对象,实现享元池
private static final Map<String, Flyweight> FLYWEIGHTS = new ConcurrentHashMap();
private static final FlyweightFactory INSTANCE = new FlyweightFactory();
private FlyweightFactory(){}
public static FlyweightFactory getInstance(){
return INSTANCE;
}
// 创建"复合享元"的工厂方法
public Flyweight getFlyweight(List<String> keys){
CompositeConcreteFlyweight compositeFly = new CompositeConcreteFlyweight();
int length = keys.size();
String key = null;
for (int i=0; i<length; i++) {
key = keys.get(i);
//调用"单纯享元"的工厂方法
compositeFly.add(this.getFlyweight(key));
}
return compositeFly;
}
// 创建"单纯享元"的工厂方法
public Flyweight getFlyweight(String key){
//如果对象存在,则直接从享元池获取
if (FLYWEIGHTS.containsKey(key)){
return FLYWEIGHTS.get(key);
}else {
//如果对象不存在,先创建一个新的对象添加到享元池中,然后返回
Flyweight flyweight = new ConcreteFlyweight("intrinsicState");
FLYWEIGHTS.put(key, flyweight);
return flyweight;
}
}
}
七、模式扩展
享元模式通常需要和其他模式一起联用,几种常见的联用方式如下:
- 在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。
- 在一个系统中,通常只有唯一一个享元工厂,因此可以使用单例模式进行享元工厂类的设计。
- 享元模式可以结合组合模式形成复合享元模式,统一对多个享元对象设置外部状态。
八、优点和缺点
8.1、优点
享元模式的主要优点如下:
- 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。
- 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
8.2、缺点
享元模式的主要缺点如下:
- 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
- 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。
九、适用环境
享元模式的使用频率并不算太高,但是作为一种以“节约内存,提高性能”为出发点的设计模式,它在软件开发中还是得到了一定程度的应用。
在以下情况下可以考虑使用享元模式:
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
十、模式应用
JDK类库中的String类使用了享元模式。
享元模式在编辑器软件中大量使用,如在一个文档中多次出现相同的图片,则只需要创建一个图片对象,通过在应用程序中设置该图片出现的位置,可以实现该图片在不同地方多次重复显示。