秦怀杂货店

General Store

  • 首页
  • 文章归档
  • 标签
  • 分类
  • 关于页面

  • 搜索
随便聊聊 数据结构 小游戏 数据库 Docker Springboot 系统设计 雪花算法 分布式 海量ip 最长回文子串 算法 面试题 线程池 多线程 线程 java学习 布隆过滤器 github 架构设计 docsify Git JVM LeetCode 杂货思考 设计模式 Lambda native isAssignableFrom 反射 剑指Offer mybatis SPI JDBC 编程工具 Java基础 集合

设计模式【10】-- 顺便看看享元模式

发表于 2022-01-09 | 分类于 设计模式 | 0 | 阅读次数 312

设计模式系列:http://aphysia.cn/categories/designpattern

1

开局还是那种图,各位客官往下看...

享元模式是什么?

享元模式(FlyWeight),是结构型模式的一种,主要是为了减少创建对象的数量,减少内存占用以及提高性能。说到这里,不知道你是否会想到池技术,比如String 常量池,数据库连接池,缓冲池等等,是的,这些都应用了享元模式。

比如,有一些对象,创建时候需要资源比较多,创建成本比较高,内存开销比较大,如果我们一直创建,机器吃不消,那么我们就想到了池化技术,把创建好的对象放在里面,需要时,去池子里面取就可以了,也就是大家共享了池子里面的对象,这就是共享。

听名字,就很共享单车:

享元模式的特点

一般而言,享元对象需要在不同的场景下使用,那状态如果可随意修改,就容易造成混乱,出错的概率大大增加。但是如果所有的内部属性都是不可修改的,貌似也不是十分灵活,因此为了在稳定和灵活性之间找到平衡点,一般的享元对象,都会将内部属性划分为两大类:

  • 内部状态:不可变,且在多个地方中共享,重复使用的部分,只能通过构造函数设值
  • 外部状态:每个对象,在不同场景下,可能存在不一样的状态,可以修改
  • 单纯享元模式:在单纯享元模式中,所有的具体享元类都是可以共享的,不存在非共享具体享元类。
  • 复合享元模式:将一些单纯享元对象使用组合模式加以组合,还可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享

这里我们说的是单纯享元模式,享元模式一般会有几种对象:

  • 享元接口或则抽象类(Flyweight):在接口或者抽象类中声明定义了公共的方法,可以对外提供部分能力,或者按需提供数据。
  • 具体的享元实现类(ConcreteFlyweight):实现了抽象享元类,在内部有一部分数据是不可变的,实现接口的时候,会对外提供一部分能力或者数据。
  • 享元工厂(FlyweightFactory): 享元工厂主要是用来创建和管理享元对象的,将各种类型的享元对象放到一个池子里,一般是键值对的形式存在,当然也可以是其他的类型,如果初次获取一个对象,需要先创建,如果池子里已经有该对象,那么就可以直接返回了。

实现

举个小栗子,比如我们出去玩耍需要购买飞机票,假设一架航班的唯一性是与航班号,出发时间,到达时间相关,用户喜欢通过航班号,来查询航班的相关信息,首先我们需要创建航班一个接口:

public interface IFlight {
    void info();
}

具体的航班类Flight:

public class Flight implements IFlight {

    private String flightNo;

    private String start;

    private String end;

    private boolean isDelay;

    public Flight(String flightNo, String start, String end) {
        this.flightNo = flightNo;
        this.start = start;
        this.end = end;
        isDelay = Math.random() > 0.5;
    }

    @Override
    public void info() {
        System.out.println(String.format("从[%s]到[%s]的航班[%s]: %s ",
                start, end, flightNo, isDelay ? "延误起飞" : "正常起飞"));
    }
}

航班搜索工厂类FlightSearchFactory:

public class FlightSearchFactory {
    public static IFlight searchFlight(String flightNo,String start,String end){
        return new Flight(flightNo,start,end);
    }
}

模拟客户端请求:

public class ClientTest {
    public static void main(String[] args) {
        IFlight flight = FlightSearchFactory.searchFlight("C9876","北京","上海");
        flight.info();
    }
}

我们可以看到打印出了以下信息:

从[北京]到[上海]的航班[C9876]: 延误起飞 

但是,上面的有一个问题,每次来访问,都会创建一个对象,坐同一个航班的人,理论上查询的是相同的数据才对,这部分其实可以共享的,复用来提高效率,何乐而不为呢?

怎么缓存呢?

我们一般用HashMap来缓存,只需要将唯一识别的key定义好即可:

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

public class FlightSearchFactory {
    private static Map<String, IFlight> maps = new HashMap<>();

    public static IFlight searchFlight(String flightNo, String start, String end) {
        String key = getKey(flightNo, start, end);
        IFlight flight = maps.get(key);
        if (flight == null) {
            System.out.print("缓存中没有,需要重新构建:");
            flight = new Flight(flightNo, start, end);
            maps.put(key, flight);
        }else{
            System.out.print("从缓存中读取数据:");
        }
        return flight;
    }

    private static String getKey(String flightNo, String start, String end) {
        return String.format("%s_%s_%s", flightNo, start, end);
    }
}

测试代码:

public class ClientTest {
    public static void main(String[] args) {
        IFlight flight = FlightSearchFactory.searchFlight("C9876","北京","上海");
        flight.info();

        IFlight flight1 = FlightSearchFactory.searchFlight("C9876","北京","上海");
        flight1.info();

        IFlight flight2 = FlightSearchFactory.searchFlight("H1213","北京","广州");
        flight2.info();
    }
}

测试结果:

缓存中没有,需要重新构建:从[北京]到[上海]的航班[C9876]: 正常起飞 
从缓存中读取数据:从[北京]到[上海]的航班[C9876]: 正常起飞 
缓存中没有,需要重新构建:从[北京]到[广州]的航班[H1213]: 正常起飞 

可以看到如果缓存里面有,那么就不会重新构建对象,可以达到共享对象的目的,我们平时在项目里面使用的各种连接池,比如Redis连接池,Mysql连接池等等,这些资源本质上都比较宝贵,我们可以共享。

JDK中Integer其实也用了缓存的技术,因为大家常用的都是较小的数值,所以默认Integer如果使用valuesOf(int i)方法获取,就会优先读取缓存内容:

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

我们可以看到如果在low 和high范围内的数据,就会从缓存里面获取,否则会直接新建一个对象,那么low和high的范围多大呢?

        static final int low = -128;
        static final int high;

high是动态变化的,但是high是有断言的,必须大于等于127:assert IntegerCache.high >= 127;,而范围可以从java.lang.Integer.IntegerCache.high这个配置项读取出来:

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

测试一下:

public class IntegerTest {
    public static void main(String[] args) {
        // 不相等
        Integer integer = Integer.valueOf(128);
        Integer integer1 = Integer.valueOf(128);
        System.out.println(integer == integer1);

        // 相等
        Integer integer2 = Integer.valueOf(127);
        Integer integer3 = Integer.valueOf(127);
        System.out.println(integer2 == integer3);

        // 相等
        Integer integer4 = Integer.valueOf(0);
        Integer integer5 = Integer.valueOf(0);
        System.out.println(integer4 == integer5);

        // 相等
        Integer integer6 = Integer.valueOf(-128);
        Integer integer7 = Integer.valueOf(-128);
        System.out.println(integer6 == integer7);

        // 不相等
        Integer integer8 = Integer.valueOf(-129);
        Integer integer9 = Integer.valueOf(-129);
        System.out.println(integer8 == integer9);
    }
}

从上面的结果可以看出实际上Integer从-128到127被缓存了,也验证了我们的结果,注意必须使用Integer.valueOf()这个办法,要是使用构造器new Integer(),创建出来必定是新的对象。

总结

  • 优点:如果有很多相似或者重复的对象,使用享元模式,可以节省空间
  • 缺点:如果重用很多,不同地方还做了特殊化处理,代码复杂度增加

设计模式其实是在软件工程的不断摸索中,总结出来的常用的一种设计思路,并不是非用不可,不是银弹,但是总有值得我们学习的地方,了解它这般设计的好处,不断的改进我们写代码,即使每次一点点改进。曾经听过一句话:看见别人写得不优雅的代码就有想重构它的冲动,可以多读读自己写的代码,然后写得更好(大致是这个意思)。共勉!

【作者简介】:
秦怀,公众号【秦怀杂货店】作者,个人网站:http://aphysia.cn,技术之路不在一时,山高水长,纵使缓慢,驰而不息。

剑指Offer全部题解PDF

开源编程笔记

  • 本文作者: 秦怀杂货店
  • 本文链接: http://aphysia.cn/archives/she-ji-mo-shi-10--shun-bian-kan-kan-xiang-yuan-mo-shi-
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# 随便聊聊 # 数据结构 # 小游戏 # 数据库 # Docker # Springboot # 系统设计 # 雪花算法 # 分布式 # 海量ip # 最长回文子串 # 算法 # 面试题 # 线程池 # 多线程 # 线程 # java学习 # 布隆过滤器 # github # 架构设计 # docsify # Git # JVM # LeetCode # 杂货思考 # 设计模式 # Lambda # native # isAssignableFrom # 反射 # 剑指Offer # mybatis # SPI # JDBC # 编程工具 # Java基础 # 集合
设计模式【9】-- 外观模式?没那么高大上
java集合【13】——— Stack源码分析走一波
  • 文章目录
  • 站点概览
秦怀杂货店

秦怀杂货店

纵然缓慢,驰而不息。

145 日志
19 分类
37 标签
Github E-mail
Creative Commons
0%
© 2022 秦怀杂货店
由 Halo 强力驱动
|
主题 - NexT.Pisces v5.1.4