设计模式之享元模式

享元模式又称蝇量模式或者羽量模式,属于结构型模式;是指以共享的方式高效的支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

享元对象能做到共享的关键是区分内蕴状态( Internal State)外蕴状态(External State)

内蕴状态是存储在享元对象内部的,并且是不会随环境改变而有所不同的。因此,一个享元可以具有得内蕴状态并可以共享。

外蕴状态是随环境的改变而改变的、不可以共享的状态。享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。

外蕴状态不可以影响享元对象的内蕴状态。换句话说,它们是相互独立的。

根据所涉及的享元对象的内部表象不同,享元模式可以分成单纯享元模式复合享元模式两种模式。

单纯享元模式

在单纯享元模式中,所有的享元对象都是可以共享的。类图如下图所示:

单纯享元模式涉及到抽象享元角色、具体享元角色、享元工厂角色、客户端角色以下四种角色:

  • 抽象享元(Flyweight)角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口。那些需要外蕴状态的操作可以通过调用商业方法以参数形式传入。
  • 具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享的。
  • 享元工厂(FlyweightFactory〉角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象的时候,享元工厂角色会检查系统中是否已经有一个复合要求的享元对象。如果己经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个合适的享元对象。
  • 客户端((Client)角色:本角色需要维护一个对所有享元对象的引用。

咖啡摊例子

在一个咖啡摊(Coffee Stall)所使用的系统里,有一系列的咖啡“风味(Flavor)”。客人到摊位上购买咖啡,所有的咖啡均放在台子上,客人自己拿到咖啡后就离开摊位。咖啡有内蕴状态,也就是咖啡的风味;咖啡没有环境因素,也就是说没有外蕴状态。

如果系统为每一杯咖啡都创建一个独立的对象的话,那么就需要创建出很多的细小对象来。这样就不如把咖啡按照种类(即“风味”)划分,每一种风味的咖啡只创建一个对象,并实行共享。

使用咖啡摊主的语言来讲,所有的咖啡都可按“风味”划分成如Capucino、Espresso等,每一种风味的咖啡不论卖出多少杯,都是全同、不可分辨的。所谓共享,就是咖啡风味的共享,制造方法的共享等。因此,享元模式对咖啡摊来说,就意味着不需要为每一份单独调制。摊主可以在需要时,一次性地调制出足够一天出售的某一种风味的咖啡。

单纯享元模式例子的UML类图:

抽象享元角色:

package com.charon.flyweight.simple;

/**
 * @className: Order
 * @description: 抽象享元角色
 * @author: charon
 * @create: 2022-03-23 21:44
 */
public abstract class Order {

    /**
     * 提供咖啡
     */
    public abstract void serve();

    /**
     * 返回咖啡的口味
     * @return
     */
    public abstract String getFlavor();
}

具体享元角色:

package com.charon.flyweight.simple;

/**
 * @className: Flavor
 * @description: 具体享元角色
 * @author: charon
 * @create: 2022-03-23 21:47
 */
public class Flavor extends Order{

    /**
     * 咖啡口味
     */
    private String flavor;

    /**
     * 内蕴状态以参数方式传入
     * @param flavor
     */
    public Flavor(String flavor) {
        this.flavor = flavor;
    }

    @Override
    public void serve() {
        System.out.println("提供的咖啡口味:" + flavor);
    }

    @Override
    public String getFlavor() {
        return this.flavor;
    }
}

享元工厂角色:

package com.charon.flyweight.simple;

/**
 * @className: FlavorFactory
 * @description: 享元工厂角色
 * @author: charon
 * @create: 2022-03-23 21:49
 */
public class FlavorFactory {

    private Flavor[] flavors = new Flavor[10];

    private int orderMade = 0;

    private int totalFlavors = 0;

    /**
     * 根据口味提供咖啡
     * @param flavor
     * @return
     */
    public Order getOrder(String flavor){
        if(orderMade > 0){
            for (int i = 0; i < orderMade; i++) {
                if(flavor.equalsIgnoreCase(flavors[i].getFlavor())){
                    return flavors[i];
                }
            }
        }
        flavors[orderMade] = new Flavor(flavor);
        totalFlavors++;
        return flavors[orderMade++];
    }

    /**
     * 返回创建过的风味咖啡的个数
     * @return
     */
    public int getTotalFlavorsMade(){
        return totalFlavors;
    }
}

客户端:

package com.charon.flyweight.simple;

/**
 * @className: Client
 * @description: 客户端
 * @author: charon
 * @create: 2022-03-23 19:52
 */
public class Client {

    /**
     * 卖出的咖啡总数
     */
    private static Order[] flavors = new Flavor[20];

    private static int orderMade = 0;

    private static FlavorFactory factory;

    public static void main(String[] args) {
        // 创建风味工厂对象
        factory = new FlavorFactory();
        // 创建咖啡对象
        takeOrders("Black Coffee");
        takeOrders("Capucino");
        takeOrders("Espresso");
        takeOrders("Espresso");
        takeOrders("Capucino");
        takeOrders("Capucino");
        takeOrders("Black Coffee");
        
        // 将创建的咖啡卖给客人
        for (int i = 0; i < orderMade; i++) {
            flavors[i].serve();
        }
        System.out.println("卖出的咖啡总数:" + factory.getTotalFlavorsMade());
    }

    /**
     * 提供咖啡
     * @param flavor
     */
    private static void takeOrders(String flavor) {
        flavors[orderMade++] = factory.getOrder(flavor);
    }
}

打印:
    提供的咖啡口味:Black Coffee
    提供的咖啡口味:Capucino
    提供的咖啡口味:Espresso
    提供的咖啡口味:Espresso
    提供的咖啡口味:Capucino
    提供的咖啡口味:Capucino
    提供的咖啡口味:Black Coffee
    卖出的咖啡总数:3

从上面的打印可以看出,虽然咖啡摊提供了7种咖啡,但是所有的咖啡口味却只有三种。

复合享元模式

复合享元模式是将一些单纯享元模式使用组合模式加以复合形成的。这样的复合享元对象本身不能共享,但是他们可以分解成单纯享元对象,而后者可以共享。复杂享元模式的类图如下:

复合享元模式所涉及的角色有抽象享元角色、具体享元角色、复合享元角色、享元工厂角色,以及客户端角色五种角色:

  • 抽象享元(Flyweight)角色:此角色是所有的具体享元类的超类,为这些类规定出需要实现的公共接口。那些需要外蕴状态的操作可以通过方法的参数传入。抽象享元的接口使得享元变得可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的
  • 具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内蕴状态的话,必须负责为内蕴状态提供存储空间。享元对象的内蕴状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。有时候具体享元角色又叫做单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的。
  • 复合享元(UnsharableFlyweight)角色:复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称做不可共享的享元对象。
  • 享元工厂(FlyweightFactory)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象请求一个享元对象的时候,享元工厂角色需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象。
  • 客户端(Client)角色:本角色需要自行存储所有享元对象的外蕴状态。

咖啡屋例子

还是上面的咖啡摊的例子,随着业务的扩大,店家将咖啡摊升级为咖啡屋了。在屋子里提供了很多桌子供客人坐,系统除了需要提供咖啡的口味外,还需要跟踪咖啡被送到哪一桌上。于是,咖啡就有了桌子作为外蕴状态了。

复合享元模式的UML类图:

抽象享元角色:

package com.charon.flyweight.composite;

/**
 * @className: Order
 * @description: 抽象享元模式
 * @author: charon
 * @create: 2022-03-23 21:44
 */
public abstract class Order {

    /**
     * 提供咖啡
     */
    public abstract void serve(Table table);

    /**
     * 返回咖啡的口味
     * @return
     */
    public abstract String getFlavor();
}

具体享元角色:

package com.charon.flyweight.composite;


/**
 * @className: Flavor
 * @description: 具体享元角色
 * @author: charon
 * @create: 2022-03-23 21:47
 */
public class Flavor extends Order {

    /**
     * 咖啡口味
     */
    private String flavor;

    /**
     * 内蕴状态以参数方式传入
     * @param flavor
     */
    public Flavor(String flavor) {
        this.flavor = flavor;
    }
    
	/**
     * 将咖啡卖给客人并备注客人的座位号
     * @param table
     */
    @Override
    public void serve(Table table) {
        System.out.println("提供的咖啡口味:" + flavor + " 桌位在:" + table.getNumber());
    }

    @Override
    public String getFlavor() {
        return this.flavor;
    }
}

享元工厂角色代码不变。

复合享元角色:

package com.charon.flyweight.composite;

/**
 * @className: Table
 * @description:
 * @author: charon
 * @create: 2022-03-23 22:11
 */
public class Table {

    /**
     * 桌子号码
     */
    private int number;

    public Table(int number) {
        this.number = number;
    }

    /**
     * Gets the value of number
     *
     * @return the value of number
     */
    public int getNumber() {
        return number;
    }

    /**
     * Sets the number
     *
     * @param number number
     */
    public void setNumber(int number) {
        this.number = number;
    }
}

客户端:

package com.charon.flyweight.composite;

/**
 * @className: Client
 * @description: 客户端
 * @author: charon
 * @create: 2022-03-23 19:52
 */
public class Client {

    /**
     * 卖出的咖啡总数
     */
    private static Order[] flavors = new Flavor[20];

    private static int orderMade = 0;

    private static FlavorFactory factory;

    public static void main(String[] args) {
        // 创建风味工厂对象
        factory = new FlavorFactory();
        // 创建咖啡对象
        takeOrders("Black Coffee");
        takeOrders("Capucino");
        takeOrders("Espresso");
        takeOrders("Espresso");
        takeOrders("Capucino");
        takeOrders("Capucino");
        takeOrders("Black Coffee");
        
        // 将创建的咖啡卖给客人,并将享元对象的外蕴状态赋值给享元对象
        for (int i = 0; i < orderMade; i++) {
            flavors[i].serve(new Table(i));
        }
        System.out.println("卖出的咖啡总数:" + factory.getTotalFlavorsMade());
    }

    /**
     * 提供咖啡
     * @param flavor
     */
    private static void takeOrders(String flavor) {
        flavors[orderMade++] = factory.getOrder(flavor);
    }
}

打印:
    提供的咖啡口味:Black Coffee 桌位在:0
    提供的咖啡口味:Capucino 桌位在:1
    提供的咖啡口味:Espresso 桌位在:2
    提供的咖啡口味:Espresso 桌位在:3
    提供的咖啡口味:Capucino 桌位在:4
    提供的咖啡口味:Capucino 桌位在:5
    提供的咖啡口味:Black Coffee 桌位在:6
    卖出的咖啡总数:3

享元模式的主要优点是:

  1. 相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。

其主要缺点是:

  1. 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
  2. 读取享元模式的外部状态会使得运行时间稍微变长。

享元模式的应用场景

当系统中多处需要同一组信息时,可以把这些信息封装到一个对象中,然后对该对象进行缓存,这样,一个对象就可以提供给多处需要使用的地方,避免大量同一对象的多次创建,降低大量内存空间的消耗。

享元模式其实是工厂方法模式的一个改进机制,享元模式同样要求创建一个或一组对象,并且就是通过工厂方法模式生成对象的,只不过享元模式为工厂方法模式增加了缓存这一功能。

下面分析它适用的应用场景。享元模式是通过减少内存中对象的数量来节省内存空间的,所以以下几种情形适合采用享元模式。

  1. 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
  2. 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
  3. 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。
posted @ 2022-03-23 22:48  pluto_charon  阅读(211)  评论(0编辑  收藏  举报