设计模式 -第1部分 避免浪费- 第1章 Flyweight 模式 - 共享对象避免浪费

66 阅读9分钟

第1部分 避免浪费

注:其内容主要来自于【日】-结城浩 著《图解设计模式》20章节 极力推荐大家阅读原著

博客中的所有部分图均来自书中,同时也对作者表示深深的敬重

第1章 Flyweight 模式 - 共享对象避免浪费

image-20230525135640753

1.1 Flyweight 模式

Flyweight 的意思"轻量级",其在英文中的原意指比赛中选手体重最轻等级的一种描述。所以顾名思义,该模式的主要作用就是为了让对象变得更"轻"。

那么这里的"轻"又如何理解呢?

在计算机的应用体系当中,所有的一切应用层的东西都是虚拟的,而这些虚拟的构建都存储在内存当中。所以这里的"轻"代指的就是一个对象所占内存的空间大小。占用空间的比率越大则越"重",反之则"轻"。

举个例子,比如说我们在 Java 当中需要创建一个对象,该对象的本身生成的中间过程比较复杂,也使得该对象比较庞大,内存分配给该对象的占用比也就响应的比距大。当程序在执行过程当中,如果大量的需要使用到该对象,先不谈,该对象每次一的 new 关键字所带来的资源上的负荷,就其该对象如果都使用 new 关键字来进行分配,将会消耗大量的内存空间。

所以关于 Flyweight 模式,其实最终的目的就是为了“尽量的通过共享实例取代 new 实例”。

相当于我们有一个共享池,在共享池中只存在唯一不同实例,公用已经存在的实例。这就是 Flyweight 模式的核心重点。

1.2 实例程序

这里先使用 Flyweight 模式设计一个简单的示例程序。

其具体的程序目的就是将其下 代码清单中 20-1 ~ 20-9 中的许多普通文本组合成一个 “大型字符”的类,而这些普通字符的实例就是重复的实例。

而下面的代码清单中的各个以 bigx.txt 命名的文件,是为了方便进行测试,而通过文本的格式将单个普通字符存储到本地文件当中,在拼接"大型字符"的时候,将其需要的文件内容加载读取到内存当中。

image-20230525140625980

image-20230525140649139

image-20230525140704274

关于类的构建说明如表 20-1 下图所示

这里我用其他字符格式代替需要的可复制或该网站生成 BootSchool

代码清单 1.1 BootSchool banner图

 __      _______    ______    
/  \    / ___   )  / ___  \   
/) )   /   )  |  /   \  \  
  | |       /   )     ___) /  
  | |     _/   /     (___ (   
  | |    /   _/          ) \  
__) (_  (   (__/\  /___/  /  
____/  _______/  ______/    
    ___      _______     ______ 
   /   )    (  ____ \   / ____ \
  / /) |    | (    /  ( (    /
 / (_) (_   | (____    | (____  
(____   _)  (_____ \   |  ___ \ 
     ) (          ) )  | (   ) )
     | |    /____) )  ( (___) )
     (_)    ______/    _____/ 
 ______      _____      _____
/ ___  \    / ___ \    / ___ \
/   )  )  ( (___) )  ( (   ) )
    /  /    \     /   ( (___) |
   /  /     / ___ \    ____  |
  /  /     ( (   ) )        ) |
 /  /      ( (___) )  /____) )
 _/        _____/   ______/

image-20230525140739332

BigChar 表示 “大型字符” 的类,该类的主要作用就是通过文件流的形式将其上述代码清单 20-8 中提到的内容读取到内存当中,然后通过 print 方法打印输出大型字符。而这些大型字符,在创建的过程当中会消耗大量的计算机内存资源,因此我们考虑的重点是在于,如何通过共享的形式,将这些唯一的 BigChar 实例作为其共享资源。

BigCharFactory 工厂的作用就是,会更具其具体的需要从而生成指定而 BigChar 实例,但是在其生成之前首先会进行相应的具体判断,如果其发现之前已经生成过该唯一的 BigChar 实例,则直接使用即可,否则生成新的唯一实例。而对于生成的实例,我们将其存储在 java.util.HashMap 类定义的 pool 字段当中,进行维护。

BigString 类的作用就是将其 BgiChar “大型字符” 组合成指定“大型字符串” 。

Main 类用于最终测试程序行为的类。

实例程序 UML 类图

image-20230525144022968

代码清单 1.1 BigChar 类 (BigChar.java)

package com.peggy.flyweight.example01;
​
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
​
/** 
 * @Projectname: designPatterns
 * @Filename: BigChar
 * @Author: peggy
 * @Data:2023/5/25 11:14
 * @Description: 根据字符生成字符串
 */public class BigChar {
​
    //字符名字
    private char charname;
​
    //大型字符对应的字符串
    private String fontdata;
​
    //字符串文件路径
    private static String PATH = "E:\workspaces\java\designPatterns\peggy-flyweight-pattren-01\src\main\resources\";
​
    public BigChar(char charname) {
        this.charname = charname;
        try {
            BufferedReader reader = new BufferedReader(
                    new FileReader(PATH + "big" + charname + ".txt"));
            String line;
            StringBuilder buf = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                //写入到内存
                buf.append(line);
                buf.append("\n");
            }
            this.fontdata = buf.toString();
​
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
​
    public void print() {
        System.out.println(fontdata);
    }
}

代码清单 1.1 BigCharFactory类 (BigCharFactory.class)

  • *!标注[1] *处 BigCharFactory 声明成静态单利模式,是为了保证全局只有一个静态工厂 BigCharFactory 实例。试想如果全局中为私有化其构造方法,就可以创建多个工厂实例,其每个工厂实例都会有一个 pool ,也就失去了 Flyweight 模式存在的意义。
  • !标注[2] 处对于 getBigchar 方法用其 synchronized 关键字进行修饰,其目的也是为了保证 pool 的对象不会重复创建。值得注意的是,虽然无论是否在并发情况下, pool 中的实例受 Map 集合的 Key / Value 键值对的约束,都是有且只有一个唯一的不同实例存在。但是这并不影响在并发情况下出现,BigChar 实例重复创建的可能性存在。所以这里使用 synchronized 关键字修饰是显得尤为重要的。
package com.peggy.flyweight.example01;
​
import java.util.HashMap;
import java.util.Map;
​
/** 
 * @Projectname: designPatterns
 * @Filename: BigCharFactory
 * @Author: peggy
 * @Data:2023/5/25 14:48
 * @Description: 生成  BigChar 实例的工厂 实现其共享功能
 */public class BigCharFactory {
​
    //管理已经生成的 BigChar 实例
    private Map<String, BigChar> pool = new HashMap<>();
    //单利模式,其保证只有一个工厂对象
    /*!标注[1]*/
    private static BigCharFactory singleton = new BigCharFactory();
​
    private BigCharFactory() {
    }
​
    public static BigCharFactory getSingleton() {
       return singleton;
    }
​
    //生成共享 BigChar 唯一对象
    /*!标注[2]*/
    public synchronized  BigChar getBigChar(char charname) {
        BigChar bc = pool.get(String.valueOf(charname));
        if (bc == null) {
            bc = new BigChar(charname);
            pool.put(String.valueOf(charname), bc);
        }
        return bc;
    }
}

代码清单 1.1 BigString类 (BigString.class)

package com.peggy.flyweight.example01;
​
/**
 * @Projectname: designPatterns
 * @Filename: BigString
 * @Author: peggy
 * @Data:2023/5/25 15:15
 * @Description: 大型字符串创建类
 */public class BigString {
    //"大型字符" 的数组
    private BigChar[] bigChars;
​
    //构造函数
    public BigString(String string) {
        bigChars = new BigChar[string.length()];
        BigCharFactory factory = BigCharFactory.getSingleton();
        for (int i = 0; i < bigChars.length; i++) {
            bigChars[i] = factory.getBigChar(string.charAt(i));
        }
    }
​
    //打印显示
    public void print() {
        for (int i = 0; i < bigChars.length; i++) {
            bigChars[i].print();
        }
    }
}

image-20230525152457323

Main 类

package com.peggy.flyweight.example01;
​
/**
 * @Projectname: designPatterns
 * @Filename: Main
 * @Author: peggy
 * @Data:2023/5/25 15:25
 * @Description: 案例测试类
 */public class Main {
    public static void main(String[] args) {
        BigString bs = new BigString("15569732xxx");
        bs.print();
    }
}

image-20230525154918578

1.3 Flyweight 模式各个角色之间的关系

通过上面的案例相比有一个直观的感受,Flyweight 模式就客户端Client(请求者)通过FlyweightFactory 工厂(轻量级工厂)将 Flyweight 实例(BigChar)创建生成,由于每一次的实例创建或取出都是通过 FlyweightFactory 工厂去控制的,所以保证了 Flyweighr 的唯一性,且不会重复实例化相同的对象,避免了 new 关键字所带来的对象过与臃肿而导致对计算机资源的浪费。

1.4 潜藏在 Flyweight 模式下的细节

Flyweight 的核心是“共享”,也就是说只要“共享”的实例本身只要发生变化,也就是意味着使用该“共享”的某些地方也会同步的发生被动改变。

可能这样描述并不是特别的清晰,这里我举个例子。

例如:假设在上述的案例当中我们修改了 BigString 类中 “3” 所对应的数据内容,那么 BigString 类中所有与 “3” 所关联的字体(形状)都会发生相应的改变,很显然这种被动的改变我们在可能一些情况下是不愿意看到的。

但是,这就是共享模型下共享实例所具有的特点。“修改一个地方多个地方都会发生相应的变动”。

因此,我们一般在决策共享实例对象的时候,需要考虑的更加细致一些才行,只要将那些真正在实际的业务需求当中需要共享的对象赋予 Flyweight 的角色才是最为恰当的做法。

这里我们对上述的案例中的业务需求做一些稍微的变动,假设在上述案例的需要在不变动的情况下,添加一个新的业务功能,“显示带有颜色的大型文字”。那么此时,颜色信息这一属性应该放到哪里呢?

第一种思路:

  • 将颜色信息放到 BigChar 类。由于 BigChar 是被赋予了 Flyweight 的角色,也就表示我们所有生成的 BigString 实例中每一个字符其颜色都是一样的。

第二种思路:

  • 如果我们将颜色的信息放到 BigString 类当中,这样我们就可以很好的控制每一个 BigChar 所具有的不同颜色。

以上两种思路,都是根据其不同的业务场景进行选择,若要说具体哪一种思路是正确的,需要其根据业务的需求而定。

所以这个例子的主要目的是在更好的说明,Flyweight (共享/享元)模式在不同的业务场景,需要对其共享的对象需再三斟酌。

1.5 不要让共享对象被垃圾回收

说到垃圾回收其这种本是 Java 语音所具有的一种特性,更好的让开发者将重点关注在其业务逻辑的开发上。

但对于共享实例对象,可能会显得并不是那么的友好。

我们上面的案例当中,其 BigChar 实例都是存储在 java.utils.HashMap 当中管理的。而像这样有 Java 自己进行“管理”的实例,就必须注意了,“不要让 JVM 垃圾回收机制将其对象实例回收”。

回忆一下我们的之前的案例在 BigCharFactory 中的 pool 字段管理生成的 BigChar 实例。因此 BigChar 的实例是不会被当做垃圾被回收的,也就是说每一个唯一的 BigChar 实例都是在内存当中的。当由于我们的程序整个运行生命周期是非常短的,所以 BigString 实例不会占用多少空间。

但是如果应用程序需要长期的运行或者说是需要以有限的内存空间来运行,那么在设计程序的过程当中,就需要额外的关注其内存回收了,“不要让共享对象的实例被垃圾回收器回收了” 。

1.6 本章所学小结

本章内容当中重点在描述 Flyweight 的核心。

  1. 共享实例减少内存使用量以及对于复杂对象节约计算机资源
  2. 如果改变共享实例,与之关联使用该实例的地方都会被动发生改变,因此要额外的注意共享对象的选择。
  3. 不要让共享对象被垃圾回收器回收,这样就失去了 Flyweight 模式本身存在的意义。