设计模式与生活⑭:一栋楼,共用一份热

今天是立冬。天气预报说夜里有暴雪,白天最高也就 4 度。

早上起来,屋里比外面强不了多少。窗玻璃上蒙着一层水汽,摸一把暖气片,凉的。

物业群里发了通知:今天开始试供暖,暖气片会慢慢热起来,顶层记得排气,边户留意自家热不热。

隔了会儿再摸,暖气片上热下凉,温温的,不烫手。再晚一点,整间屋子总算有了点热乎气。

一天里最踏实的一件事,是知道这栋楼里不只自己一户在等热。热力站烧的是一锅水,顺着管网送到每家,各家只管拧自己那个阀门。

试供暖第一天,暖气片温而不烫

今天聊的享元模式(Flyweight):把大家都能用的那份抽出来只留一份,各家只管自己那一小份。系列第十四篇。

01 先看"每家自己烧"

最直接的做法,是每户各自备一个热源。

java 复制代码
class Apartment {
    private final HeatSource heatSource = new HeatSource("三号热力站", 45);

    double roomTemp(double valve) {
        // 没供暖时白天屋里 15℃ 上下,再加上阀门开度分到的温升
        return 15 + heatSource.maxRise() * valve;
    }
}

一栋楼一百户,就是一百个 HeatSource。锅炉、管道、烟囱,每家一套。

能跑,但这一百份东西的参数几乎一模一样:同一个热力站、同一个供水温度。真正各不相同的,只有阀门开度和房间面积。

一模一样的那部分被复制了一百遍,占内存、占资源,改一处还得改一百处。

02 抽出一份共享的

把一样的部分抽出来,只留一份;不一样的部分,留在各家。

java 复制代码
final class HeatSource {
    private final String station;
    private final double supplyTemp;

    // 阀门开满时,这份水最多能把室温焐热几度
    double maxRise() {
        // 供水比不供暖的屋子(15℃)热出 30℃,
        // 但热量大半顺着窗户、墙缝散掉了,留在屋里的约十分之一
        return (supplyTemp - 15) / 10;
    }
}

class HeatSourcePool {
    private final Map<String, HeatSource> pool = new HashMap<>();

    HeatSource get(String station, double supplyTemp) {
        String key = station + "@" + supplyTemp;
        return pool.computeIfAbsent(key, k -> new HeatSource(station, supplyTemp));
    }
}

两个细节。HeatSource 的字段全是 final,没有 setter。共享的东西一旦可变,302 把水温调到 70,全楼跟着烫。池子用 computeIfAbsent,有就拿出来,没有才新建。

每户仍然是独立对象,只是不再自带锅炉:

java 复制代码
class Apartment {
    private final String name;
    private final HeatSource source;   // 共享的那份
    private final double valve;        // 各家自己的
    private final double area;

    double roomTemp() {
        // 基准 15℃ + 阀门开度分到的温升
        return 15 + source.maxRise() * valve;
    }
}

公式本身得交代一句:这不是热力学公式,是照着生活直觉估的。45℃ 的水进了 15℃ 的屋子,屋子不会变成 45℃------热量一直顺着窗户、墙缝往外跑,试供暖的水又只是温温的,最后留在屋里的也就十分之一上下,3℃。阀门开八成,就分到这 3℃ 的八成:15 + 3 × 0.8 = 17.4℃。别嫌少,试供暖本来就不是奔着暖和去的,是先把管网焐热、把气排掉;真要暖到穿短袖,得等正式供暖水温上到六七十度。

构造器和打印方法这里省略,组装起来跑一遍:

java 复制代码
HeatSourcePool pool = new HeatSourcePool();
HeatSource source = pool.get("三号热力站", 45);

Apartment a  = new Apartment("302",  source, 0.8, 89);
Apartment b  = new Apartment("501",  source, 0.6, 89);
Apartment c  = new Apartment("1201", source, 0.9, 89);

System.out.println("共享热源数:" + pool.size());
a.print();
b.print();
c.print();

运行结果:

text 复制代码
共享热源数:1
302 · 89 平米 · 阀门 80% · 室温 17.4 度
501 · 89 平米 · 阀门 60% · 室温 16.8 度
1201 · 89 平米 · 阀门 90% · 室温 17.7 度

三户室温不同,热源对象只有一个。差异不在共享的那部分,在各家自己那部分。

共享的在左边,各家的在右边

03 内部状态和外部状态

享元把对象的状态分成两半。

内部状态(intrinsic):能共享、不随使用者变的。热力站、供水温度、管网压力。它放进共享对象里,所有使用者看到的是同一份。

外部状态(extrinsic):每个使用者各自不同的。阀门开度、房间面积、朝南还是朝北。它不进共享对象,留在各家自己身上,用的时候带上。

一句话:享元 = 共享内部状态 + 外部状态外置。 这两句就是这套模式的全部。

04 这份热,不是物理意义上的共享

这里得挑明几处失真。

第一,现实里共享的是热量这个物理量,会被消耗、会被分配;代码里共享的是对象实例,多用一次引用,对象也不会少一块。暖气管里的水是实打实流走的,HeatSource 对象不会。

第二,现实的热力站会随时调温,代码里的共享对象必须尽量不可变。这是模式的要求,不是现实的约束。

第三,现实里每户的暖气片是独立实体;代码里 Apartment 也仍然是独立对象,共享的只有 HeatSource。别理解成三户并成了一户。

第四,试供暖时有的家里暖气片上热下凉,是管道里有空气,跟模式无关。共享对象不会堵气。

05 跟单例,差在哪儿

单例(Day 1)是全局只有一个实例,谁来拿都是同一个。

享元是按一组键共享,一个键一份。 三号热力站一份,五号热力站另一份;同一个站的住户共用那一份。所以享元可以有很多实例,只是同类共享者之间不重复创建。

JDK 里就有现成的:Integer.valueOf(-128 到 127) 返回缓存对象,超出范围才新建。这也是为什么 Integer a = 127; Integer b = 127; 两个引用一定相等,换成 128 就不一定了。

06 什么时候,别急着建池子

状态大部分各不相同、而且一直在变,共享不了。 阀门开度每家每刻都在调,这种状态就不该往共享对象里塞。

对象没几个、创建又便宜,别建池。 就两户人家,犯不上先搭个热源池。

共享对象里塞了可变集合,容易串。 今天这家改、明天那家改,共享就变成了互相影响。

判断标准落在一句话:是否有一大批对象,其中大部分状态相同、可以共享,而少数状态必须各家各管? 是,才轮到享元。

07 屋里热了,夜也没那么长

暖气片热起来,屋里不再缩手缩脚。窗外阴着天,风已经起来了。

立冬之后,天黑得一天比一天早。屋里有这份热乎气,夜就没那么长。

跟前面十三篇对一句:单例管全局唯一,工厂管怎么造,模板方法管流程,策略管选法,装饰器管加层,命令管把请求做成对象,代理管访问,观察者管通知,外观管入口,状态管当前怎么做,责任链管谁接住,原型管复制,桥接管解耦,享元管把能共享的部分只留一份。

带走一句话:享元 = 共享的是那口热,不是每家的阀门。

立冬这天,屋外冷,屋里慢慢热。

一锅水,暖一栋楼。

参考资料

  • 《设计模式:可复用面向对象软件的基础》 (GoF 原著):book.douban.com/subject/105...------享元模式的原始定义:运用共享技术有效地支持大量细粒度的对象
  • Refactoring Guru · Flyweight :refactoring.guru/design-patt...------享元模式的结构:内部状态与外部状态如何拆分,工厂如何缓存共享对象
  • Oracle Java API · Integer (Java 21 文档):docs.oracle.com/en/java/jav...------valueOf 对 -128 到 127 返回缓存实例,是 JDK 里常见的享元实现
相关推荐
是2的10次方啊1 天前
设计模式与生活⑬:你坐几号线,谁来运营是另一回事
设计模式
Tt有了t2 天前
设计模式速查
java·python·设计模式
一条Go2 天前
四个进程,一把锁:Alioth 的告警评测为什么不需要消息队列
后端·设计模式·架构
一缕82年的清风2 天前
被中产唾弃的“十元快剪”,藏着最狠的极客工程学
人工智能·设计模式
是2的10次方啊2 天前
设计模式与生活⑫:周报又到手,别从空白开始
设计模式
拆房老料3 天前
DeepSeek Super-agent 架构解析:从单任务 Agent 到持续运行的任务系统
人工智能·设计模式·系统架构
工一木子3 天前
设计模式与生活⑩:地铁没变,你换了个状态
设计模式
是2的10次方啊3 天前
设计模式与生活⑪:地铁里被打扰,别总靠自己硬扛
设计模式
高晶4 天前
SPMI 控制器 RTL 设计与验证新手入门
设计模式·程序员·架构