15.享元模式

429 阅读9分钟

享元模式

一. 什么是享元模式

面向对象可以很好地解决一些灵活性或可扩展性带来的问题, 但在很多情况下, 面向对象会使系统的类个数增加. 系统中类的个数越来越多, 就会影响程序运行的性能。享元模式通过共享技术提高相同或者相似对象重用的方式来提高系统资源的利用率.

享元(Flyweight)模式的定义:运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

享元模式有两个要求: 细粒度和共享对象。粒度细了,不可避免地会产生很多对象, 而这些对象性质有相似之处,因此我们就将这些对象的信息分为两个部分:内部状态和外部状态。

在享元模式中可以共享的相同内容称为 内部状态(Intrinsic State),而那些需要外部环境来设置的不能共享的内容称为 外部状态(Extrinsic State),其中外部状态和内部状态是相互独立的,外部状态的变化不会引起内部状态的变化。由于区分了内部状态和外部状态,因此可以通过设置不同的外部状态使得相同的对象可以具有一些不同的特征,而相同的内部状态是可以共享的。也就是说,享元模式的本质是分离与共享 : 分离变与不变,并且共享不变。

理解上面的内部状态和外部状态了么?
我们先来理解一下内部状态和外部状态.
比如: 我是张三. 每天上班回家.
内部状态是: 我的不变的属性, 我叫张三.
外部状态是: 我要上班, 回家, 地理位置的变化

再比如: 下围棋. 围棋有黑白子
内部状态是: 围棋的固定属性, 棋子的颜色, 黑子/白子
外部状态是: 走在棋盘上的位置坐标的变化

再比如: 连接池中的连接对象,
内部状态是: 保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变
外部状态是: 当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。

姓名和围棋的黑白子, 连接池的连接信息是固定属性. 他们都是内部状态. 而外部状态是变化的.

像这种, 有内部状态和外部状态的区别, 我们就可以将内部状态作为共享的一部分

享元模式的本质是缓存共享对象,降低内存消耗。

再来理解一下上面这段话.

  1. 在享元模式中可以共享的相同内容称为 内部状态(Intrinsic State),而随外部环境变化而变化不能共享的内容称为 外部状态(Extrinsic State) 解析: 这里围棋的颜色, 人的姓名都是不会变化的, 他们可以作为内部状态. 位置的变化, 坐标的变化是随环境场景变化而变化的, 是外部状态.

  2. 外部状态和内部状态是相互独立的,外部状态的变化不会引起内部状态的变化。 解析: 如果内部状态随外部状态的变化而变化了. 那这样的内部状态是不可共享的. 比如: 水变成冰变成蒸汽.这种外部状态变化以后, 内部状态也变了, 不能使用享元模式

  3. 由于区分了内部状态和外部状态,因此可以通过设置不同的外部状态使得相同的对象可以具有一些不同的特征,而相同的内部状态是可以共享的。也就是说,享元模式的本质是分离与共享 : 分离变与不变,并且共享不变。

    解析: 这句话是对上面的总结. 使用内部状态作为共享对象. 享元模式的本质是分离内外部对象, 共享内部对象.

    在享元模式中通常会出现工厂模式,需要创建一个享元工厂来负责维护一个享元池(Flyweight Pool)(用于存储具有相同内部状态的享元对象)。

    在享元模式中,共享的是享元对象的内部状态,外部状态需要通过环境来设置。在实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种对象也称为 细粒度对象。

    享元模式的目的就是使用共享技术来实现大量细粒度对象的复用。

享元模式主要解决的问题: 在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。

二. 享元模式何时使用

1、系统中有大量对象, 这些对象消耗大量内存, 并且这些对象有相似之处 2、这些对象可以分为内部状态和外部状态, 可以按照内部状态分为很多组,当把外部状态从对象中剔除时,这些对象可以使用一个对象来代替。

三. 案例

我们要画20个圆, 一共有五种颜色. 每个圆的颜色和坐标随机.

1. 常规实现方式.

第一步: 定义一个形状接口

package com.lxl.www.designPatterns.flyweight.circle;

/**
 * 形状
 */
public interface Shape {

    void draw();
}

第二步: 定义圆形实现类

package com.lxl.www.designPatterns.flyweight.circle;

public class Circle implements Shape{

    /**
     * 颜色
     */
    private String color;
    /**
     * x 轴坐标
     */
    private int x;

    /**
     * y 轴坐标
     */
    private int y;

    /**
     * 半径
     */
    private int radius;

    public Circle(String color, int x, int y, int radius) {
        this.color = color;
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    public void draw() {
        System.out.println("圆心: {" + x + ", " + y + "}, 半径: " + radius);
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getRadius() {
        return radius;
    }

    public void setRadius(int radius) {
        this.radius = radius;
    }
}

第三步: 随机创建20个圆, 颜色5种中随机

package com.lxl.www.designPatterns.flyweight.circle;

public class client {
    private static final String colors[] =
            { "Red", "Green", "Blue", "White", "Black" };

    private static String getRandomColor() {
        return colors[(int)(Math.random()*colors.length)];
    }
    private static int getRandomX() {
        return (int)(Math.random()*100 );
    }
    private static int getRandomY() {
        return (int)(Math.random()*100);
    }

    private static int getRadius() {
        return (int)(Math.random()*10+1);
    }


    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            Shape circle = new Circle(getRandomColor(), getRandomX(), getRandomY(), getRadius());
            circle.draw();
        }
    }
}

运行结果:

圆心: {49, 86}, 半径: 8
圆心: {97, 15}, 半径: 5
圆心: {63, 38}, 半径: 9
圆心: {76, 43}, 半径: 2
圆心: {98, 47}, 半径: 7
圆心: {2, 96}, 半径: 3
圆心: {58, 86}, 半径: 3
圆心: {64, 83}, 半径: 4
圆心: {56, 21}, 半径: 7
圆心: {57, 77}, 半径: 3
圆心: {40, 29}, 半径: 9
圆心: {23, 42}, 半径: 9
圆心: {89, 35}, 半径: 3
圆心: {50, 16}, 半径: 4
圆心: {65, 9}, 半径: 5
圆心: {33, 9}, 半径: 7
圆心: {27, 82}, 半径: 9
圆心: {93, 97}, 半径: 1
圆心: {30, 99}, 半径: 3
圆心: {17, 18}, 半径: 4

分析: 这个是简单粗暴的方式, 我们看到要画20个圆, 创建了20个对象. 要是画100个圆就要创建100个对象. 如果是围棋下棋, 那要创建的对象就更多了. 这样系统肯定承受不住.

接下来, 我们使用享元模式优化

2. 享元模式优化

第一步: 定义形状接口

package com.lxl.www.designPatterns.flyweight.flyweightCicle;

/**
* 形状接口
*/
public interface Shape {
   /**
    * 画
    */
   void draw();
}

第二步: 定义原型实现类

package com.lxl.www.designPatterns.flyweight.flyweightCicle;

/**
 * 圆形实现类
 */
public class Circle implements Shape{

    /**
     * 圆形的颜色
     */
    private String color;

    /**
     * x 轴坐标
     */
    private int x;

    /**
     * y 轴坐标
     */
    private int y;

    /**
     * 半径
     */
    private int radius;

    /**
     * 画
     */
    @Override
    public void draw() {
        System.out.println("圆心: {" + x + ", " + y + "}, 半径: " + radius);
    }

    public Circle(String color) {
        this.color = color;
    }

    public Circle(String color, int x, int y, int radius) {
        this.color = color;
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getRadius() {
        return radius;
    }

    public void setRadius(int radius) {
        this.radius = radius;
    }
}

第三步: 享元工厂

package com.lxl.www.designPatterns.flyweight.flyweightCicle;

import java.util.HashMap;
import java.util.Map;

/**
 * 享元工厂
 */
public class ShapeFactory {

    public static Map<String, Shape> shapeMap = new HashMap<>();

    public static Shape createShape(String color) {
        Shape shape = shapeMap.get(color);
        if (shape == null) {
            shape = new Circle(color);
            shapeMap.put(color, shape);
        }

        return shape;
    }

}

在享元工厂定义了一个Map集合, key是颜色, value是Shape形状. 在创建形状的时候, 传一个颜色过来, 去map集合中获取颜色对象, 如果有则直接取出, 如果没有就创建一个, 然后放入到map中.

第四步: 客户端调用

package com.lxl.www.designPatterns.flyweight.flyweightCicle;

public class Client {
    public static String[] colors = new String[] {"red", "blue", "green", "purple", "yellow"};

    private static String getRandomColor() {
        return colors[(int)(Math.random()*colors.length)];
    }
    private static int getRandomX() {
        return (int)(Math.random()*100 );
    }
    private static int getRandomY() {
        return (int)(Math.random()*100);
    }

    private static int getRadius() {
        return (int)(Math.random()*10+1);
    }

    public static void main(String[] args) {
        for (int i=0; i < 20; i++) {
            Circle circle = (Circle)ShapeFactory.getShape(getRandomColor());
            circle.setX(getRandomX());
            circle.setY(getRandomY());
            circle.setRadius(getRadius());
            circle.draw();
        }
    }
}

这次再来分析, 看看一共创建了几个对象呢? 有几种颜色就创建了几个对象. 最多创建5个对象. 和原来的20个相比, 大大减少了对象的个数. 像围棋只有黑白两子, 那就只需要创建2个对象.

第五步: 运行结果

圆心: {10, 25}, 半径: 5
圆心: {99, 74}, 半径: 8
圆心: {13, 59}, 半径: 1
圆心: {62, 58}, 半径: 6
圆心: {98, 97}, 半径: 9
圆心: {1, 44}, 半径: 8
圆心: {26, 97}, 半径: 10
圆心: {27, 68}, 半径: 8
圆心: {33, 68}, 半径: 7
圆心: {69, 23}, 半径: 9
圆心: {93, 27}, 半径: 6
圆心: {72, 90}, 半径: 1
圆心: {61, 80}, 半径: 2
圆心: {36, 88}, 半径: 3
圆心: {79, 8}, 半径: 5
圆心: {87, 29}, 半径: 6
圆心: {48, 62}, 半径: 9
圆心: {7, 71}, 半径: 1
圆心: {0, 67}, 半径: 7
圆心: {59, 22}, 半径: 2

总结: 这里面Shape是享元抽象接口, Circle是享元对象, 在circle对象中又有两部分, 一部分是可共享的状态,即: 颜色; 另一部分是不可共享的状态, 即: 圆心+半径. 这两部分组合构成了享元对象.

四. 享元模式的结构

1. 享元模式总结

来看一下享元模式的UML图:

从上图可以看出, 享元模式通常包含以下几个角色:

  1. 享元抽象对象FlyWeight: 是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
  2. 可共享的享元具体对象ConcreteFlyWeight: 实现享元抽象对接接口, 接收外部状态
  3. 不可共享的享元具体对象UnShareConcreteFlyWeight: 是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中
  4. 享元工厂FlyWeightFactory: 负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
  5. 客户端调用Client

享元模式源码

第一步: 不可共享的享元对象

package com.lxl.www.designPatterns.flyweight.demo;

/**
 * 不可共享的享元对象
 * 这里是享元对象中的外部状态. 再说的具体点就可以理解为是上面画圆中的圆心和半径
 */
public class UnShareConcreteFlyWeight {
    /**
     * 这里面可以定义任意对象, 命名为info只是为了说明问题
     */
    private String info;

    public UnShareConcreteFlyWeight(String info) {
        this.info = info;
    }

    @Override
    public String toString() {
        return "UnShareConcreteFlyWeight{" +
                "info='" + info + '\'' +
                '}';
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(String info) {
        this.info = info;
    }
}

不可共享的享元对象, 也就是外部状态, 会发生变化的那部分

第二步: 享元抽象接口

package com.lxl.www.designPatterns.flyweight.demo;

/**
 * 享元接口
 */
public interface FlyWeight {
    /**
     * 享元对象会因为不可共享状态产生不同的行为结果
     * @param unshare
     */
    void operate(UnShareConcreteFlyWeight unshare);
}

享元接口定义了一个operate方法, 会随着外部状态而变化的方法

第三步: 可共享的享元对象, 实现了享元抽象接口

package com.lxl.www.designPatterns.flyweight.demo;

/**
 * 可共享的具体享元对象
 */
public class ConcreteFlyWeight implements FlyWeight{

    /**
     * 内部状态
     * 可以根据内部状态将享元对象归类
     */
    private String innerState;

    /**
     * 每一个对象被创建的时候就拥有一个内部状态
     * @param innerState
     */
    public ConcreteFlyWeight(String innerState) {
        this.innerState = innerState;
    }

    @Override
    public void operate(UnShareConcreteFlyWeight unshare) {
        System.out.println("共享状态是:"+ innerState + ", 非共享状态是: " + unshare.toString());
    }

    public String getInnerState() {
        return innerState;
    }

    public void setInnerState(String innerState) {
        this.innerState = innerState;
    }

    @Override
    public String toString() {
        return "ConcreteFlyWeight{" +
                "innerState='" + innerState + '\'' +
                '}';
    }
}

这里定义了一个可共享的状态, 这是对象自己特有的. 同时重写了父类的operate方法.

第四步: 享元工厂

package com.lxl.www.designPatterns.flyweight.demo;

import java.util.HashMap;

public class FlyWeightFactory {
    private HashMap<String, FlyWeight> flyweights = new HashMap<String, FlyWeight>();
    public FlyWeight getFlyweight(String key) {
        FlyWeight flyweight = (FlyWeight) flyweights.get(key);
        if (flyweight != null) {
            System.out.println("具体享元" + key + "已经存在,被成功获取!");
        } else {
            System.out.println("具体享元" + key + "不存在,创建一个!");
            flyweight = new ConcreteFlyWeight(key);
            flyweights.put(key, flyweight);
        }
        return flyweight;
    }
}

享元工厂定义的map, key是享元共享对象, value是享元对象. 这样相同内部状态的对象就可共享一个对象.

第五步: 享元客户端

package com.lxl.www.designPatterns.flyweight.demo;

public class FlyWeightClient {
    public static void main(String[] args) {
        FlyWeightFactory factory = new FlyWeightFactory();
        FlyWeight f01 = factory.getFlyweight("key1");
        FlyWeight f02 = factory.getFlyweight("key1");
        FlyWeight f03 = factory.getFlyweight("key1");
        FlyWeight f11 = factory.getFlyweight("key2");
        FlyWeight f12 = factory.getFlyweight("key2");
        f01.operate(new UnShareConcreteFlyWeight("第1次调用key1"));
        f02.operate(new UnShareConcreteFlyWeight("第2次调用key1"));
        f03.operate(new UnShareConcreteFlyWeight("第3次调用key1"));
        f11.operate(new UnShareConcreteFlyWeight("第1次调用key2"));
        f12.operate(new UnShareConcreteFlyWeight("第2次调用key2"));
    }
}

运行结果:

具体享元key1不存在,创建一个!
具体享元key1已经存在,被成功获取!
具体享元key1已经存在,被成功获取!
具体享元key2不存在,创建一个!
具体享元key2已经存在,被成功获取!
共享状态是:key1, 非共享状态是: UnShareConcreteFlyWeight{info='第1次调用key1'}
共享状态是:key1, 非共享状态是: UnShareConcreteFlyWeight{info='第2次调用key1'}
共享状态是:key1, 非共享状态是: UnShareConcreteFlyWeight{info='第3次调用key1'}
共享状态是:key2, 非共享状态是: UnShareConcreteFlyWeight{info='第1次调用key2'}
共享状态是:key2, 非共享状态是: UnShareConcreteFlyWeight{info='第2次调用key2'}

五. 享元模式的类型

享元模式有两种类型: 单纯享元模式和复合享元模式

1. 单存享元模式

我们在上面讲述的内容都是单纯享元模式, 这里不再赘述. 接下来看看复合享元模式.

2. 复合享元模式

复合享元模式, 不好理解. 一定要配合一个案例