今天是立冬。天气预报说夜里有暴雪,白天最高也就 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 里常见的享元实现