【设计模式】享元模式

230 阅读6分钟

本文主要介绍享元模式概念和用法,以及有状态享元模式,复合享元模式。

模式背景

享元模式算是一个比较复杂的模式。在一些系统中,可能会需要大量的对象。比如游戏中的小兵,围棋系统中的黑子白子,如果我们每需要一个这样的对象的时候都去new一个,那么系统会创建大量的对象。这会加重系统的负担,如果是Java的话还会触发大量的GC。所以就如何优化设计,避免系统中去创建大量相同或者相似的对象就是享元模式的目标。

一个最好的例子就是:在一个人很多城市,每个人都需要学习自行车。如果人手都配一个自行车,那么成本得多大。但是有了共享单车就不一样了。我们只需要少量的共享单车就能让所有人都能使用到自行车。这个共享单车就是享元模式的思想。

定义&概念

运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于 享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。

原理

享元模式的享元我理解为:共享元数据的元。什么是元数据?元数据描述数据属性信息的数据叫元数据,那元就是数据的属性。也就是对所有的数据来说,这个元都一样的(比如文件都有大小,类型,文件名这些属性)。

享元模式也是将系统中这些大量相似或者相同的对象中状态不变的部分给抽取出来,让状态变化的部分都共享这部分。所以享元模式能做到可以共享的关键是区分出该对象的内部状态外部状态

内部状态(不变):那些不会因为环境改变而改变的。比如说描述一段文字,文字内容ABC就是ABC。

外部状态(变):那会随着环境改变而跟着改变的那些无法共享的状态。比如ABC文字是宋体,黑体等。

**我们用享元模式将不变的内部状态创建单例或者少量。让变化的无法共享的外部状态通过传参等形式传递给共享对象。**这也是为什么能共享的对象必须是细粒度的,一般一堆对象中,那些相同的部分都比较小,这些部分能提出来做成共享对象就提出来做。

享元模式结构比较复杂,一般还需要结合工厂,单例模式使用。

组成要素

  • 抽象享元类(Flyweight)

    • 一个接口或者抽象类,申明公共方法,提供那些不变的内部状态,已经设置外部状态的方法。
  • 具体享元类(ConcreteFlyweight)

    • 就是那些需要被共享的,状态不变属性和方法构建的对象。通常使用单例模式来设计具体享元类。
  • 非共享具体享元类(UnsharedConcreteFlyweight)

    • 有些享元抽象类的子类我们并不需要共享,这些类就直接走new的。
  • 享元工厂类(FlyweightFactory)

    • 创建所有享元对象的工厂,内部有个缓存池,缓存了共享的享元对象。当需要对象的时候先从池中取,取不到再创建。其主要作用是创建充当池。
    • 就是2个作用:创建享元对象和做享元池缓存。
  • 外部状态类(非必须,如果享元对象有外部状态时候才需要,如果只是共享就不需要)

    • 那些无法被共享的状态,这些对象代表着享元对象变化的状态,需要传给享元对象使用。

依据享元对象定制化需求,享元模式分为两种:

  • 单纯享元模式:所有的具体享元类都是可以共享的,不存在非共享的具体享元类。

  • 复合享元模式:有时候我们希望享元池中的多个享元对象,都设置同时的外部状态,此时可以使用组合模式将这些享元对象组合起来。这样让这些享元对象同时拥有相同的外部状态。

UML

这是一个带状态的享元模式:

实现

抽象享元类

public interface Flyweight {
    /**内部变化*/
    void printIntrinsicState();
    /**@param extrinsicState 外部变化 让外部来控制*/
    void op(String extrinsicState);
}

具体享元类

public class ConcreteFlyweight1 implements Flyweight {
    /** 这个内部状态 就是该对象能被享元的分到的最小细粒度了。
    在这种状态下,系统需要多少个对象就要创建多少这样的享元类。*/
    @Override
    public void printIntrinsicState() {
        System.out.println("intrinsicState1");
    }
    /** 显示外部变化*/
    @Override
    public void op(String extrinsicState) {
        printIntrinsicState();
        System.out.println(extrinsicState);
    }
}

非共享具体享元类

public class UnsharedConcreteFlyweight implements Flyweight {

    /**不是享元内部的那些状态。*/
    private String otherStates;
    @Override
    public void printIntrinsicState() {
        System.out.println("intrinsicState");
    }
    @Override
    public void op(String extrinsicState) {
        //输出内部状态
        printIntrinsicState();
        //输出其他的状态
        System.out.println(otherStates);
        //输出外部控制的状态
        System.out.println(extrinsicState);
    }
}

享元工厂类

public class FlyweightFactory {
    //单例
    private static FlyweightFactory instance = new FlyweightFactory();
    //缓存池
    private HashMap<String, Flyweight> flyweights = new HashMap<>();
    //存入共享享元对象
    public FlyweightFactory() {
        Flyweight fw1 = new ConcreteFlyweight1();
        Flyweight fw2 = new ConcreteFlyweight2();
        flyweights.put("1", fw1);
        flyweights.put("2", fw2);

    }
    //获取
    public Flyweight getFlyweight(String key) {
        if (flyweights.containsKey(key)) {
            //缓存存在则直接返回
            return flyweights.get(key);
        } else {
            //不存在则创建并缓存再返回
            Flyweight fw = new ConcreteFlyweight1();
            flyweights.put(key, fw);
            return fw;
        }
    }
    public static FlyweightFactory getInstance() {
        return instance;
    }
}

加入外部变化:外部变化可以是具体类的对象,这里为了简化,只是使用字符串传进去。

Flyweight f1 = FlyweightFactory.getInstance().getFlyweight("1");
f1.op("you are one");
//这时候f1的op行为就被改变了,但是f1还是原来的f1
f1.op("you are not one");

优缺点

优点

  • 减少内存中的对象数量,使得相同相似的对象只保留一个。

缺点

  • 提高了系统的复杂度,需要分离内部外部状态,让人无法整体的去看待。让程序的逻辑复杂化,也让人更难以理解。
  • 缺点主要就是在内部状态和外部状态的关系上。

使用场景

  1. 当系统中存在大量相同或者重复对象的时候,可以尝试使用享元模式。主要出发点是为了节约内存,减少对象对内存的开销。
  2. 需要缓冲池的场景

JDK的String类就使用的是享元模式。String定义的字符串,两个一个样的字符串其实都是一个对象,都是同样的地址。相关的池技术其实都和享元有这相近的思想。

总结

**当系统中大量相同相似的对象的时候,尝试使用享元模式。第一件事情先将这些对象的内部状态和外部状态分开。内部状态就是享元对象,使用抽象工厂来创建。外部状态通过参数传给享元对象来让享元具备不同的状态(这个外部状态也是有可能不需要传给享元对象的)。**注意点就是:需要一个单例工厂创建享元对象。

相关代码:github.com/zhaohaoren/…

如有代码和文章问题,还请指正!感谢!