乐趣区

关于设计模式:设计模式学习笔记十四享元模式

1 概述

1.1 引言

当一个零碎中运行时的产生的对象太多,会带来性能降落等问题,比方一个文本字符串存在大量反复字符,如果每一个字符都用一个独自的对象示意,将会占用较多内存空间。

那么该如何避免出现大量雷同或类似的对象,同时又不影响客户端以面向对象的形式操作呢?

享元模式正为解决这一问题而生,通过共享技术实现雷同或类似对象的重用,在逻辑上每一个呈现的字符都有一个对象与之对
应,然而物理上却共享一个享元对象。

在享元模式中,存储共享实例的中央称为享元池 ,能够针对每一个不同的字符创立一个享元对象,搁置于享元池中,须要时取
出,示意图如下:

1.2 外部状态与内部状态

享元模式以共享的形式高效地反对大量细粒度对象的重用,能做到共享的要害是辨别了外部状态以及内部状态。

  • 外部状态:存储在享元对象外部并且不会随环境扭转而扭转,外部状态能够共享,例如字符的内容,字符 a 永远是字符 a,不会变为字符 b
  • 内部状态:可能随环境扭转而扭转,不能够共享的状态,通常由客户端保留,并在享元对象被创立之后,须要应用的时候再传入到享元对象外部。内部状态之间通常是互相独立的,比方字符的色彩,字号,字体等,能够独立变动,没有影响,客户端在应用时将内部状态注入到享元对象中

正因为辨别了外部状态以及内部状态,能够将具备雷同外部状态的对象存储在享元池中,享元池的对象是能够实现共享的,须要的时候从中取出,实现对象的复用。通过向取出的对象注入不同的内部状态,能够失去一系列类似的对象,而这些对象实际上只存储一份。

1.3 定义

享元模式:使用共享技术无效地反对大量细粒度对象的复用。

零碎只应用大量的对象,而这些对象都很类似,状态变动很小,能够实现对象的屡次复用。因为享元模式要求可能共享的对象必须是细粒度对象,因而又叫轻量级模式,是一种对象结构型模式。

1.4 结构图

享元模式个别联合工厂模式一起应用,结构图如下:

1.5 角色

  • Flyweights(形象享元类):通常是一个接口或者抽象类,在形象享元类中申明了具体享元类公共的办法,这些办法能够向外界提供享元对象的外部数据(外部状态),同时也能够通过这些办法来设置内部数据(内部状态)
  • ConcreteFlyweight(具体享元类):实现 / 继承形象共享类,实例称为共享对象,在具体享元类中为外部状态提供了存储空间,通常能够联合单例模式来设计具体享元类
  • UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的形象享元子类都须要被共享,不能被共享的子类可设计为非共享具体享元类,当须要一个非具体享元对象时能够间接实例化创立
  • FlyweightFactory(享元工厂类):享元工厂类用于创立并治理享元对象,针对形象享元类编程,将具体享元对象存储于享元池中。个别应用键值对汇合(比方 Java 中的HashMap)作为享元池,当客户端获取享元对象时,首先判断是否存在,存在则从汇合中取出并返回,不存在则创立新具体享元的实例,存储于享元池中并返回新实例

2 典型实现

2.1 步骤

  • 定义形象享元类:将形象享元类定义为接口或者抽象类,申明业务办法
  • 定义具体享元类:继承或实现形象享元,实现其中的业务办法,同时应用单例模式设计,确保每个具体享元类提供惟一的享元对象
  • (可选)定义非共享具体享元类:继承或实现形象享元类,不应用单例模式设计,每次客户端获取都会返回一个新实例
  • 定义享元工厂类:通常应用一个键值对汇合作为享元池,依据键值返回对应的具体享元对象或非共享具体享元对象

2.2 形象享元类

这里应用接口实现,蕴含一个 opeartion 业务办法:

interface Flyweight
{void operation(String extrinsicState);
}

2.3 具体享元类

简略设计两个枚举单例的具体享元类:

enum ConcreteFlyweight1 implements Flyweight
{INSTANCE("INTRINSIC STATE 1");
    private String intrinsicState;
    private ConcreteFlyweight1(String intrinsicState)
    {this.intrinsicState = intrinsicState;}

    @Override
    public void operation(String extrinsicState)
    {System.out.println("具体享元操作");
        System.out.println("外部状态:"+intrinsicState);
        System.out.println("内部状态:"+extrinsicState);
    }
}

enum ConcreteFlyweight2 implements Flyweight
{INSTANCE("INTRINSIC STATE 2");
    private String intrinsicState;
    private ConcreteFlyweight2(String intrinsicState)
    {this.intrinsicState = intrinsicState;}

    @Override
    public void operation(String extrinsicState)
    {System.out.println("具体享元操作");
        System.out.println("外部状态:"+intrinsicState);
        System.out.println("内部状态:"+extrinsicState);
    }
}

2.4 非共享具体享元类

两个简略的非共享具体享元类,不是枚举单例类:

class UnsharedConcreteFlyweight1 implements Flyweight
{
    @Override
    public void operation(String extrinsicState)
    {System.out.println("非共享具体享元操作");
        System.out.println("内部状态:"+extrinsicState);
    }
}

class UnsharedConcreteFlyweight2 implements Flyweight
{
    @Override
    public void operation(String extrinsicState)
    {System.out.println("非共享具体享元操作");
        System.out.println("内部状态:"+extrinsicState);
    }
}

2.5 享元工厂类

为了不便客户端以及工厂治理具体享元以及非共享具体享元,首先建设两个枚举类作为享元池的键:

enum Key {KEY1,KEY2}
enum UnsharedKey {KEY1,KEY2}

这里的工厂类应用了枚举单例:

enum Factory
{
    INSTANCE;
    private Map<Key,Flyweight> map = new HashMap<>();
    public Flyweight get(Key key)
    {if(map.containsKey(key))
            return map.get(key);
        switch(key)
        {
            case KEY1:    
                map.put(key, ConcreteFlyweight1.INSTANCE);
                return ConcreteFlyweight1.INSTANCE;
            case KEY2:
                map.put(key, ConcreteFlyweight2.INSTANCE);
                return ConcreteFlyweight2.INSTANCE;
            default:
                return null;
        }
    }

    public Flyweight get(UnsharedKey key)
    {switch(key)
        {
            case KEY1:
                return new UnsharedConcreteFlyweight1();
            case KEY2:
                return new UnsharedConcreteFlyweight2();
            default:
                return null;
        }
    }
}

应用 HashMap<String,Flyweight> 作为享元池:

  • 对于具体享元类,依据键值判断享元池中是否存在具体享元对象,如果存在间接返回,如果不存在把具体享元的单例存入享元池,并返回该单例
  • 对于非共享具体享元类,因为是“非共享”,不须要把实例对象存储于享元池中,每次调用间接返回新实例

2.6 反射简化

如果具体享元对象变多,工厂类的 get() 中的 switch 会变得很长,这时候能够将键值类以及工厂类的 get() 改良以简化代码,例如在下面的根底上又减少了两个具体享元类:

enum ConcreteFlyweight3 implements Flyweight {...}
enum ConcreteFlyweight4 implements Flyweight {...}

这样工厂类的 switch 须要减少两个Key

switch(key)
{
    case KEY1:    
        map.put(key, ConcreteFlyweight1.INSTANCE);
        return ConcreteFlyweight1.INSTANCE;
    case KEY2:
        map.put(key, ConcreteFlyweight2.INSTANCE);
        return ConcreteFlyweight2.INSTANCE;
    case KEY3:
        map.put(key, ConcreteFlyweight3.INSTANCE);
        return ConcreteFlyweight3.INSTANCE;
    case KEY4:
        map.put(key, ConcreteFlyweight4.INSTANCE);
        return ConcreteFlyweight4.INSTANCE;
    default:
        return null;
}

能够利用具体享元类的命名形式进行简化,这里应用了程序编号 1,2,3,4... 的形式,因而,利用反射获取对应的类后间接获取其中的单例对象:

public Flyweight get(Key key)
{if(map.containsKey(key))
        return map.get(key);
    try
    {Class<?> cls = Class.forName("ConcreteFlyweight"+key.code());
        Flyweight flyweight = (Flyweight)(cls.getField("INSTANCE").get(null));
        map.put(key,flyweight);
        return flyweight;
    }
    catch(Exception e)
    {e.printStackTrace();
        return null;
    }
}

在此之前须要批改一下 Key 类:

enum Key
{KEY1(1),KEY2(2),KEY3(3),KEY4(4);
    private int code;
    private Key(int code)
    {this.code = code;}
    public int code()
    {return code;}
}

减少一个 code 字段,作为辨别每一个具体享元的标记。
对于非共享具体享元相似,首先批改 UnsharedKey,同理增加code 字段:

enum UnsharedKey
{KEY1(1),KEY2(2),KEY3(3),KEY4(4);
    private int code;
    private UnsharedKey(int code)
    {this.code = code;}
    public int code()
    {return code;}
}

接着批改 get 办法:

public Flyweight get(UnsharedKey key)
{
    try
    {Class<?> cls = Class.forName("UnsharedConcreteFlyweight"+key.code());
        return (Flyweight)(cls.newInstance());
    }
    catch(Exception e)
    {e.printStackTrace();
        return null;
    }
}

因为笔者应用的是 OpenJDK11,其中 newInstance 被标记为过期了:


因而应用如下形式代替间接应用newInstance()

return (Flyweight)(cls.getDeclaredConstructor().newInstance());

区别如下:

  • newInstance:间接调用无参构造方法
  • getDeclaredConstructor().newInstance()getDeclaredConstructor()会依据传入的参数搜寻该类的构造方法并返回,没有参数就返回该类的无参构造方法,接着调用 newInstance 进行实例化

3 实例

围棋棋子的设计:一个棋盘中含有大量雷同的黑白棋子,只是呈现的地位不一样,应用享元模式对棋子进行设计。

  • 形象享元类:IgoChessman接口(如果想要具体享元类为枚举单例的话必须是接口,应用其余形式实现单例能够为抽象类),蕴含 getColor 以及 display 办法
  • 具体享元类:BlackChessman+WhiteChessman,枚举单例类
  • 非共享具体享元类:无
  • 享元工厂类:Factory,枚举单例类,蕴含简略的 get 作为获取具体享元的办法,加上了 white 以及 balck 简略封装,在构造方法中初始化享元池

代码如下:

// 形象享元接口
interface IgoChessman
{Color getColor();
    void display();}

// 具体享元枚举单例类
enum BlackChessman implements IgoChessman
{
    INSTANCE;
    
    @Override
    public Color getColor()
    {return Color.BLACK;}

    @Override
    public void display()
    {System.out.println("棋子色彩"+getColor().color());
    }
}

// 具体享元枚举单例类
enum WhiteChessman implements IgoChessman
{
    INSTANCE;
    
    @Override
    public Color getColor()
    {return Color.WHITE;}

    @Override
    public void display()
    {System.out.println("棋子色彩"+getColor().color());
    }
}

// 享元工厂枚举单例类
enum Factory
{
    INSTANCE;
    //HashMap<Color,IgoChessman> 作为享元池
    private Map<Color,IgoChessman> map = new HashMap<>();
    private Factory()
    {
        // 构造方法中间接初始化享元池
        map.put(Color.WHITE, WhiteChessman.INSTANCE);
        map.put(Color.BLACK, BlackChessman.INSTANCE);
    }
    public IgoChessman get(Color color)
    {
        // 因为在构造方法中曾经初始化,如果不存在能够返回 null 或者增加新实例到享元池并返回,这里抉择了返回 null
        if(!map.containsKey(color))
            return null;
        return (IgoChessman)map.get(color);
    }
    // 简略封装
    public IgoChessman white()
    {return get(Color.WHITE);
    }
    public IgoChessman black()
    {return get(Color.BLACK);
    }
}

enum Color
{WHITE("红色"),BLACK("彩色");
    private String color;
    private Color(String color)
    {this.color = color;}
    public String color()
    {return color;}
}

在初始化享元池时,如果具体享元类过多能够应用反射简化,不须要手动一一put

private Factory()
{map.put(Color.WHITE, WhiteChessman.INSTANCE);
    map.put(Color.BLACK, BlackChessman.INSTANCE);
}

依据枚举值数组,联合 ListforEach,一一利用数组中的值获取对应的类,进而获取实例:

private Factory()
{List.of(Color.values()).forEach(t->
    {String className = t.name().substring(0,1)+t.name().substring(1).toLowerCase()+"Chessman";
        try
        {map.put(t,(IgoChessman)(Class.forName(className).getField("INSTANCE").get(null)));
        }
        catch(Exception e)
        {e.printStackTrace();
            map.put(t,null);
        }    
    });
}

测试:

public static void main(String[] args) 
{
    Factory factory = Factory.INSTANCE;
    IgoChessman white1 = factory.white();
    IgoChessman white2 = factory.white();
    white1.display();
    white2.display();
    System.out.println(white1 == white2);

    IgoChessman black1 = factory.black();
    IgoChessman black2 = factory.black();
    black1.display();
    black2.display();
    System.out.println(black1 == black2);
}

4 退出内部状态

通过下面的形式曾经可能实现黑白棋子的共享了,然而还有一个问题没有解决,就是如何将雷同的黑白棋子搁置于不同的棋盘地位上?

解决办法也不难,减少一个坐标类 Coordinates,调用display 时作为要搁置的坐标参数传入函数。

首先减少一个坐标类:

class Coordinates
{
    private int x;
    private int y;    
    public Coordinates(int x,int y)
    {
        this.x = x;
        this.y = y;
    }
    //setter+getter...
}

接着须要批改形象享元接口,在 display 中退出 Coordinates 参数:

interface IgoChessman
{Color getColor();
    void display(Coordinates coordinates);
}

而后批改具体享元类即可:

enum BlackChessman implements IgoChessman
{
    INSTANCE;
    
    @Override
    public Color getColor()
    {return Color.BLACK;}

    @Override
    public void display(Coordinates coordinates)
    {System.out.println("棋子色彩"+getColor().color());
        System.out.println("显示坐标:");
        System.out.println("横坐标"+coordinates.getX());
        System.out.println("纵坐标"+coordinates.getY());
    }
}

对于客户端,创立享元对象的代码毋庸批改,只需批改调用了 display 的中央,传入 Coordinates 参数即可:

IgoChessman white1 = factory.white();
IgoChessman white2 = factory.white();
white1.display(new Coordinates(1, 2));
white2.display(new Coordinates(2, 3));

5 单纯享元模式与复合享元模式

5.1 单纯享元模式

规范的享元模式既能够蕴含具体享元类,也蕴含非共享具体享元类。
然而在单纯享元模式中,所有的具体享元类都是共享的,也就是不存在非共享具体享元类。
比方下面棋子的例子,黑白棋子作为具体享元类都是共享的,不存在非共享具体享元类。

5.2 复合享元模式

将一些单纯享元对象进行应用组合模式加以组合还能够造成复合享元对象 ,这样的复合享元对象自身不能共享,然而它们能够合成为单纯享元对象,而后者能够共享。
通过复合享元模式能够确保复合享元类所蕴含的每个单纯享元类都具备雷同的内部状态,而这些单纯享元的外部状态能够不一样,比方,下面棋子的例子中:

  • 黑棋子是单纯享元
  • 白棋子也是单纯享元
  • 这两个单纯享元的外部状态不同(色彩不同)
  • 然而能够设置雷同的内部状态(比方设置为棋盘上同一地位,然而这样没有什么实际意义,或者设置显示为同一大小)

例子如下,首先在形象享元中增加一个以 int 为参数的display

interface IgoChessman
{Color getColor();
    void display(int size);
}

在具体享元实现即可:

enum BlackChessman implements IgoChessman
{
    INSTANCE;
    
    @Override
    public Color getColor()
    {return Color.BLACK;}

    @Override
    public void display(int size)
    {System.out.println("棋子色彩"+getColor().color());
        System.out.println("棋子大小"+size);
    }
}

接着增加复合享元类,外面蕴含一个 HashMap 存储所有具体享元:

enum Chessmans implements IgoChessman
{
    INSTANCE;
    private Map<Color,IgoChessman> map = new HashMap<>();

    public void add(IgoChessman chessman)
    {map.put(chessman.getColor(),chessman);
    }

    @Override
    public Color getColor()
    {return null;}

    @Override
    public void display(int size)
    {map.forEach((k,v)->v.display(size));
    }
}

display 中,实际上是遍历了 HashMap,给每一个具体享元的display 传入雷同的参数。
测试:

public static void main(String[] args) {
    Factory factory = Factory.INSTANCE;
    IgoChessman white = factory.white();
    IgoChessman black = factory.black();
    Chessmans chessmans = Chessmans.INSTANCE;
    chessmans.add(white);
    chessmans.add(black);
    chessmans.display(30);
}

输入:

这样外部状态不同(色彩不同)的两个具体享元类(黑白棋)就被复合享元类(Chessmans)设置为具备雷同的内部状态(显示大小 30)。

6 补充阐明

  • 与其余模式联用:享元模式通常须要与其余模式联用,比方工厂模式(享元工厂),单例模式(具体享元枚举单例),组合模式(复合享元模式)
  • JDK 中的享元模式:JDK 中的 String 应用了享元模式。大家都晓得 String 是不可变类,对于相似 String a = "123" 这种申明形式,会创立一个值为 "123" 的享元对象,下次应用 "123" 时从享元池获取,在批改享元对象时,比方a += "1",先将原有对象复制一份,而后在新对象上进行批改,这种机制叫做 ”Copy On Write”。基本思路是,一开始大家都在共享内容,当某人须要批改时,把内容复制进来造成一个新内容并批改

7 次要长处

  • 升高内存耗费:享元模式能够极大地缩小内存中对象的数量,使得雷同或类似对象在内存中只保留一份,从而节约系统资源,提供零碎性能
  • 内部状态独立:享元模式内部状态绝对独立,不会影响到外部状态,从而使得享元对象能够在不同环境中被共享

8 次要毛病

  • 减少复杂度:享元模式使得零碎变简单,须要拆散出外部状态以及内部状态,使得程序逻辑复杂化
  • 运行工夫变长:为了使对象能够共享,享元模式须要将享元对象的局部状态内部化,而读取内部状态使得运行工夫变长

9 实用场景

  • 一个零碎有大量类似或雷同对象,造成大量内存节约
  • 对象的大部分状态都能够内部化,能够将这些内部状态传入对象中
  • 因为须要保护享元池,造成肯定的资源开销,因而在须要真正多次重复应用享元对象时才值得应用享元模式

10 总结

退出移动版