设计模式-享元模式

内存溢出对java应用来说实在是太平常了,有以下两种可能。

  • 内存泄露
    无意识的代码缺陷,导致内存泄露,JVM不能获得连续的内存空间
  • 对象太多
    代码写的很烂,产生的对象太多,内存被耗完
    业务需求,一个考试系统,登陆后需要填写一下信息:

  • 考试科目,选择框

  • 考试地点,选择框,根据科目不同,列表不同
  • 准考证邮寄地址,输入框
    设计模式-享元模式_第1张图片
    很简单的工厂方法模式,表现层通过工厂方法模式创建对象,然后传递给业务层和持久层,最终保存到数据库中,为什么要使用工厂方法模式而不用直接new一个对象呢?因为在框架下编程,必须有一个对象工厂(ObjectFactory,Spring也有对象工厂)。我们先看报考信息,代码如下:
package com.example.xpeng.myapplication;

/**
 * Created by xpeng on 2018/7/8.
 */

public class SignInfo {
    //报名人员的ID
    private String id;
    //考试地点
    private String location;
    //考试科目
    private String subject;
    //邮寄地址
    private String postAddress;


    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public String getSubject() {
        return subject;
    }

    public void setSubject(String subject) {
        this.subject = subject;
    }

    public String getPostAddress() {
        return postAddress;
    }

    public void setPostAddress(String postAddress) {
        this.postAddress = postAddress;
    }
}

它是一个很简单的POJO对象(Plain Ordinary Java Object,简单Java对象)。我们再来看工厂类,代码如下:

public class SignInfoFactory {
//报名信息的对象工厂
public static SignInfo getSignInfo(){
return new SignInfo();
}
}

工厂类就这么简单?非也,这是我们的教学代码,真实的ObjectFactory要复杂得多,主要是注入了部分Handler的管理。表现层是如何创建对象的,代码如下:

public class Client {
public static void main(String[] args) {
//从工厂中获得一个对象
SignInfo signInfo = SignInfoFactory.getSignInfo();
//进行其他业务处理
}
}

就这么简单,但是简单为什么会出现问题呢?而且这样写也没有问题啊,很标准的工厂方法模式,应该不会有大问题。定位分析是内存对象太多,那应该想到使用一种共享的技术减少对象数量,那怎么共享呢?
对象池的实现有很多开源工具,我们自己来设计一个共享对象池,需要实现如下两个功能。

  • 容器定义
    我们要定义一个池容器,在这个容器中容纳哪些对象
    -提供客户端访问的接口
    我们要提供一个接口供客户端访问,池中有可用对象时,可以直接从池中获得,否则建立一个新的对象,并放置到池中。
    对象很多,必然有一些相同的属性,把对象的相同属性提取出来,不同属性在系统内进行赋值处理,是不是可以建立一个池了,先看类图:
    做了一个很小的改动,增加了一个子类,实现带缓冲池的对象的建立,同事在工厂类上增加了一个容器对象HashMap,保存池中的所有对象。我们先来看产品子类,代码如下:
package com.example.xpeng.myapplication;

/**
 * Created by xpeng on 2018/7/8.
 */

public class SignInfo4Pool extends SignInfo {
    //定义一个对象池提取的KEY的值
    private String key;
    //构造函数获取相同标志
    public SignInfo4Pool(String key){
        this.key = key;
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }
}

很简单,就是增加了一个key值,为什么要增加key值?为什么要使用子类,而不再SignInfo类上做修改?我们刚刚分析了所有的SignInfo对象都有一些共同的属性:考试科目和考试地点,我们把这些共性提取出来作为所有对象的外部状态,这个对象池中一个具体的外部状态只有一个对象。按照这个设计,我们定义key值的标准为:考试科目+考试地点的符合字符串作为唯一的池对象标准,也就是说在对象池中,一个key值唯一对应一个对象。
注意:
在对象池中,对象一旦产生,必然有一个唯一的、可访问的状态标志该对象,而且池中的对象声明周期是由池容器决定,而不是由使用者决定的。
你可能马上就要提出了,为什么不建立一个新的类,包含subject和location两个属性作为外部状态呢?嗯,这是一个办法,但不是最好的办法,有两个原因:

  • 修改的工作量太大,增加的这个类由谁来创建呢?同时,SignInfo类是否也要修改呢?你不可能让两端相同的POJO程序同事出现在同一模块中吧!
  • 性能问题,我们会在扩展模块中讲解
    说了这么多,我们还是继续来看程序,工厂类:
package com.example.xpeng.myapplication;

import android.util.Log;

import java.util.HashMap;

/**
 * Created by xpeng on 2018/7/8.
 */

public class SignInfoFactory {
    //池容器
    private static HashMap pool = new HashMap<>();

    //报名信息的对象工厂
    @Deprecated
    public static SignInfo getSignInfo(){
        return new SignInfo();
    }

    //报名信息的对象工厂
    public static SignInfo getStignInfo(String key){
        //设置返回对象
        SignInfo result = null;
        //池中没有该对象,则建立,并放入池中
        if (!pool.containsKey(key)){
            Log.e("xyz","---建立对象,并放置到池中");
            result = new SignInfo4Pool(key);
            pool.put(key,result);
        }else{
            result = pool.get(key);
            Log.e("xyz","--直接从池中取的");
        }
        return result;
    }
}

方法都很简单,不多解释。读者需要注意一点的是@Deprecated注解,不要有删除投产中代码的念头,如果方法或类确实不再使用了,增加该注解,表示该方法或类已经过时,尽量不要在使用了,我们应该保持历史原貌,同事也有助于版本向下兼容,特别是在产品研发中。
我们再来看看客户端是如何调用的,如下代码:

private void Client() {
        //初始化对象池
        for (int i = 0; i < 4; i++) {
            String subject = "科目" + i;
            //初始化地址
            for (int j = 0; j < 30; j++) {
                String key = subject + "考试地点" + j;
                SignInfoFactory.getStignInfo(key);
            }
        }
        SignInfo signInfo = SignInfoFactory.getStignInfo("科目1考试地点1");
    }

这就是享元模式

享元模式的定义
享元模式是池技术的重要实现方式,其定义如下:使用共享对象可有效的支持大量的细粒度的对象
享元模式的定义为我们提供了两个要求:细粒度的对象和共享对象。我们知道分配太多的对象到应用程序中将有损程序的性能,同时还容易造成内存溢出,那怎么避免呢?就是享元模式提到的共享技术。我们先来了解一下对象都内部状态和外部状态
要求细粒度对象,那么不可避免的使得对象数量多且性质相近,我们就将这些对象的信息分为两个部分:内部状态和外部状态

  • 内部状态
    内部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态,如我们例子中的考试科目+考试地点复合字符串,它是一批对象的统一标识,是唯一的一个索引值。
    有了对象的两个状态,我们就可以来看享元模式的通用类图,如下:
    设计模式-享元模式_第2张图片
    类图也很简单,我们先来看我们享元模式角色名称

  • Flyweight——抽象享元角色
    它简单的说就是一个产品的抽象类,同事定义出对象的外部状态和内部状态的接口或实现。

  • ConcreateFlyweight——具体享元角色
    具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的。

  • unsharedConcreateFlyweight——不可共享的享元角色
    不存在外部状态或者安全要求(如线程安全)不能够使用共享技术的对象,该对象一般不会出现在享元工厂中。

  • FlyweightFactory——享元工厂
    职责非常简单,就是构造一个池容器,同时提供从池中获得对象的方法。
    享元模式的目的在于运用共享技术,使得一些细粒度的对象可以共享,我们的设计确实也应该这样,多使用细粒度的对象,便于重用或重构。我们来看享元模式的通用代码,先看抽象享元角色,代码如下:
package com.example.xpeng.myapplication;

public abstract class Flyweight {
    //内部状态
    private String intrinsic;
    //外部状态
    protected final String Extrinsic;

    //要求享元角色必须接受外部状态
    public Flyweight(String extrinsic) {
        Extrinsic = extrinsic;
    }

    //定义业务操作
    public abstract void operate();
    //内部状态的getter/setter

    public String getIntrinsic() {
        return intrinsic;
    }

    public void setIntrinsic(String intrinsic) {
        this.intrinsic = intrinsic;
    }
}

抽象享元角色一般为抽象类,在实际项目中,一般是一个实现类,它是描述一类事物的方法。在抽象角色中,一般需要把外部状态和内部状态(当然了,可以没有内部状态,只有行为也是可以的)定义出来,避免子类的随意扩展。我们再来看看具体的享元角色,代码如下:

package com.example.xpeng.myapplication;

public class ConcreateFlyweight1 extends Flyweight {
    //接受外部状态
    public ConcreateFlyweight1(String extrinsic) {
        super(extrinsic);
    }

    //根据外部状态进行逻辑处理
    @Override
    public void operate() {

    }

}
package com.example.xpeng.myapplication;

public class ConcreateFlyweight2 extends Flyweight {
    //接受外部状态
    public ConcreateFlyweight2(String extrinsic) {
        super(extrinsic);
    }

    //根据外部状态进行逻辑处理
    @Override
    public void operate() {

    }

}

这很简单,实现自己的业务逻辑,然后接收外部状态,以便内部业务逻辑对外部状态的依赖。注意,我们在抽象享元中对外部状态加上了final关键字,防止意外发生,什么意外?获得了一个外部状态,然后无意修改了一下,池就混乱了!
注意 在程序开发中,确认只需要一次赋值的属性则设置为final类型,避免无意修改导致逻辑混乱,特别是Session级的常量或变量。
我们继续看享元工厂,代码如下:

package com.example.xpeng.myapplication;

import java.util.HashMap;

public class FlyweightFactory {
    //定义一个容器池
    private static HashMap pool = new HashMap<>();
    //享元工厂
    public static Flyweight getFlyweight(String Extrinsic){
        //需要返回的对象
        Flyweight flyweight = null;
        //在池中没有该对象
        if (pool.containsKey(Extrinsic)){
            flyweight = pool.get(Extrinsic);
        }else {
            //根据外部状态创建享元对象
            flyweight = new ConcreateFlyweight1(Extrinsic);
            //放置在池中
            pool.put(Extrinsic,flyweight);
        }
        return flyweight;
    }
}

享元模式的应用
(1)享元模式的优缺点
享元模式是一个非常简单的模式,它可以大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能,但是同时也提高了系统复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱
(2)享元模式的使用场景
在如下场景中可以选择使用享元模式

  • 系统中存在大量的相似对象
  • 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份
  • 需要缓冲池的场景

    享元模式的扩展
    (1)线程安全的问题
    线程安全是一个老生常谈的话题,只要使用JAVA开发否会遇到这个问题,我们之所以要在今天的享元模式中提到该问题,是因为该模式有太大的几率发生线程不安全,为什么呢?
    设置的享元对象数量太少,导致每个线程都到对象池中获取对象,然后都去修改其属性,于是就出现一些不和谐数据。我们在使用享元模式时,对象池中的享元对象尽量多,多到足够满足业务为止。
    (2)性能平衡
    尽量使用Java基本类型作为外部状态。

享元模式DEMO下载地址:
demo下载
设计模式之禅demo

你可能感兴趣的:(设计模式,设计模式,享元模式)