寂然解读设计模式 - 享元模式

I walk very slowly, but I never walk backwards 

设计模式 - 享元模式


寂然

大家好,我是寂然,本节课,我们来聊设计模式中的享元模式,老规矩,首先我们先通过一个案例需求来引入

案例演示 - 网站项目

首先我们来看这样一个需求:

我们接了一个小型的外包项目,给客户老王做一个产品展示网站,老王的朋友们感觉效果不错,也希望做这样的产品展示网站,但是他们要求都有些不同

1)有客户要求以新闻的形式发布

2)有客户人要求以博客的形式发布

3)有客户希望以微信公众号的形式发布

解决方案一:一般实现

OK,拿到这样一个需求以后,我们先不要考虑今天要聊的享元模式,首要目标是解决需求,因为需求中虽然网站的发布形式不一样 ,但是基本的形式是一致的,所以最容易想到的方式是直接复制粘贴一份,然后根据客户不同要求,进行定制修改,接着,放到不同的虚拟空间中,如下示意图


寂然解读设计模式 - 享元模式


方案分析

其实上面实现方式是我们经常用的,复制一份进行定制修改,就要求客户需要的网站结构相似度很高,只是展示的形式和部分微观结构需要定制,而且这些都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,会造成服务器的资源浪费


那其实我们可以考虑这样做,整合到一个网站中,共享其相关的代码和数据,对于硬盘、内存、CPU、数据库空间等服务器资源都尝试达成一个共享的效果,这样不仅减少了需要的服务器资源,而且对于代码来说,由于是一份实例,维护和扩展都更加容易,这种解决思路其实就是享元模式

基本介绍

享元模式(Flyweight Pattern)也叫蝇量模式:运用共享技术有效地支持大量细粒度的对象
属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式,享即共享,元即对象

常用于系统底层开发,解决系统的性能问题,像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则重新创建


再一个,享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似的对象,需要缓冲池时,不需要总是创建新对象,可以从缓存池里拿,这样可以降低系统内存,同时提高效率,所以,享元模式最经典的应用场景就是池技术了,String 常量池,数据库连接池,缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式

原理类图

享元模式的类图如下图所示


寂然解读设计模式 - 享元模式


享元模式角色分析

  • Flyweight 是抽象的享元角色,它是产品的抽象类,同时它会定义出对象的外部状态和内部状态

  • ConcreteFlyweight 是具体的享元角色,是具体的产品类,实现抽象角色定义相关业务

  • UnsharedConcreteFlyweight 是不可共享的角色,这个角色可能会出现在享元模式中,但是一般不会出现在享元工厂中

  • FlyweightFactory 是享元工厂类,用于构建一个池的容器(以集合的形式展现),同时提供从池中获取对象的相关方法

外部状态&内部状态

享元模式提出了两个要求,细粒度和共享对象,这里涉及到了两个概念,内部状态与外部状态,即将对象的信息分为两个部分,内部状态与外部状态

内部状态指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变

外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态


比如围棋、五子棋、跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,所以棋子颜 色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同,当我们落子后,落子颜色是定的,但位置是变化 的,所以棋子坐标就是棋子的外部状态

为什么要这样做

围棋理论上有 361 个空位可以放棋子,每盘棋都有可能有两三百个棋子对象产生,因为内存空间有限,一台服务器很难支持更多的玩家玩围棋游戏,如果用享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例,这样就减少了创建对象的数量,降低了重复对象的创建,减少内存占用和提高性能

解决方案二:享元模式

下面我们使用享元模式重构案例需求,首先我们通过类图来演示解决的思路

原理类图

寂然解读设计模式 - 享元模式


代码演示
//抽象的享元角色
public abstract class Website {
​
 public abstract void show(User user);//抽象方法
​
}
​
//具体的享元角色
public class ConcreteWebsite extends Website {
​
 //共享的部分,不会变属于内部状态
 private String type = "";//网站发布的形式
​
 //构造器
 public ConcreteWebsite(String type) {
 this.type = type;
 }
​
 //外部状态??例如用户
 @Override
 public void show(User user) {
​
 System.out.println("以" + type + "形式发布网站项目,用户为:" + user.getName());
​
 }
}
​
//享元工厂类
public class WebsiteFactory {
​
 //创建集合,充当池的角色
 private HashMap<String,ConcreteWebsite> pool = new HashMap<>();
​
 //根据网站的类型,返回对应的网站,如果没有就创建网站并放入池中
 public Website getWebsite(String type){
​
 if (!pool.containsKey(type)){
 //如果没有对应的类型,就创建一个放入
 pool.put(type,new ConcreteWebsite(type));
​
 }
​
 return (Website)pool.get(type);
 }
​
 //获取网站分类的总数
 public int getWebsiteCount(){
​
 return pool.size();
 }
}
​
//用户
public class User {
​
 private String name;
​
 public User(String name) {
 this.name = name;
 }
​
 public String getName() {
 return name;
 }
​
 public void setName(String name) {
 this.name = name;
 }
}
​
//客户端
public class Client {
​
 public static void main(String[] args) {
​
 //创建一个工厂
 WebsiteFactory websiteFactory = new WebsiteFactory();
​
​
 //客户要一个以新闻形式发布的网站
 Website news = websiteFactory.getWebsite("news");
​
 news.show(new User("老李"));
​
 //客户需要以微信公众号形式发布的网站
 Website wechat = websiteFactory.getWebsite("Wechat");
​
 wechat.show(new User("老王"));
​
 //多个客户需要微信公众号形式发布
 Website wechat1 = websiteFactory.getWebsite("Wechat");
​
 wechat1.show(new User("老赵"));
​
 Website wechat2 = websiteFactory.getWebsite("Wechat");
​
 wechat2.show(new User("老沈"));
​
 //看看池子里的总数
 System.out.println(websiteFactory.getWebsiteCount());
​
 }
}

注意事项

优势
  • 节省内存空间,重复对象需要频繁被创建时,由于只会被创建一次,所以对系统内存的需求也大大减小

  • 提高效率,对于可重复的对象只会被创建一次,再次访问时先从缓冲池中获取,响应速度更快,效率更高

注意点

享元模式提高了系统的复杂度,需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方,使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制,如果盲目使用, 会提高系统逻辑的复杂度

使用场景
  • 享元模式经典的应用场景就是各类池技术

  • 系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用 HashMap/HashTable等来进行存储

Integer源码分析

OK,其实我们最常用的 java.lang.Integer 中,就使用到了享元模式,这节课我们一起来看下 Integer 源码中,享元模式的使用,在此之前呢,我们一起来聊几道 Integer 相关的面试,作为前置基础来引出我们要聊的内容

Int&Integer面试题解析

首先我们来看如下几个案例,对结果进行判定

Integer i = new Integer(100);
​
Integer j = new Integer(100); 
​
System.out.print(i == j); //false 

由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)

Integer i = new Integer(100); 
​
int j = 100;  
​
System.out.print(i == j); //true 

Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)

Integer i = new Integer(100);
​
Integer j = 100;
​
System.out.print(i == j); //false 

非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)

Integer i = 100;
​
Integer j = 100;
​
System.out.print(i == j); //true 
​
Integer i = 128;
​
Integer j = 128;
​
System.out.print(i == j); //false

对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false


java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueof(100);通过源码我们得知,默认对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,不会再次进行new操作

测试代码

OK,明确了上面的几点后,我们来看这样一段测试代码

public class Test {
​
 public static void main(String[] args) {
​
 Integer a = Integer.valueof(127);
​
 Integer b = new Integer(127);
​
 Integer c = Integer.valueof(127);
​
 System.out.println(a.equals(b)); //比较大小,为true
​
 System.out.println(a == b); //false
​
 System.out.println(a == c); //true
 }
}

有了上面的前置基础,判断就很容易了,同时上面我们聊到,说对于传入的127会进行缓存,a是通过valueof方法传入127返回的对象实例,c也是如此,所以为true,下面我们通过源码来验证一下这个结论,而且同时大家也会想到,这其中就使用到了享元模式

Integer源码解析

我们进入 Integer 的 valueof 方法,如果在定义好的范围以内,会直接从下面的缓存池里去拿,如果不再该范围以内,return new Integer(i);

public static Integer valueof(int i) {
 if (i >= IntegerCache.low && i <= IntegerCache.high)
 return IntegerCache.cache[i + (-IntegerCache.low)];
 return new Integer(i);
 }

默认定义好的范围是多少呢?我们接着往下看,默认缓存的范围是-128到127之间的数


寂然解读设计模式 - 享元模式


也就是说,如果 Integer.valueOf(x) ,x满足这个范围,就是使用享元模式返回,而享元模式中,如果第一次没有,进行创建,有就直接返回,如果不在该范围,仍然创建一个新的对象

接着看,它在第一次创建 IntegerCache 的时候,首先创建了 cache 数组,大小为 (high - low) + 1,是因为数据下标不可以为负数,也就是创建了可以存放 -128 - 127 之间的数组,然后往里面一个个添加数据


寂然解读设计模式 - 享元模式


valueOf 方法,就使用到享元模式,如果使用 valueOf 方法得到一个 Integer 实例,范围在 -128 - 127,直接返回已经创建好的,执行速度是要大于 new 操作的,当然,不光是 Integer 内部维护的缓冲池,String 常量池,数据库连接池,等等都是享元模式的应用,享元模式是池技术的重要实现方式

下节预告

OK,到这里,享元模式的相关内容就结束了,下一节,我们开启代理模式的学习,最后,希望大家在学习的过程中,能够感觉到设计模式的有趣之处,高效而愉快的学习,那我们下期见~

上一篇:寂然解读设计模式 - 外观模式


下一篇:寂然解读设计模式 - 组合模式