- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
Monostate(单态,又名 Borg)是一种创建型设计模式,它通过把类中所有数据成员声明为static,使任意数量的实例都共享同一份状态,从而在保留"可自由 new 多个对象"的外观下获得类似 Singleton 的全局一致状态。本文以 java-design-patterns 仓库的 monostate 模块 为骨架,结合 LoadBalancer 源码 与测试用例,完整讲解该模式的定义、典型实现、适用场景、权衡取舍及与 Singleton、Flyweight 的对比,读完后你将能独立实现并正确评估 Monostate 的适用性。
一、模式概述:它是什么,又叫什么
Monostate 模式是面向对象设计中实现"单例行为"的另一种思路:它不限制实例数量,而是强制所有实例共享同一份状态。
- 别名:Borg(源自《The ThoughtWorks Anthology》中的叫法)。
- 本质:wiki.c2.com 给出的经典定义是——"monostate 是一个'概念单例',它的所有数据成员都是静态的,因此该类的所有实例都使用相同(静态)数据。使用 monostate 的应用可以创建任意数量的实例,因为每个实例使用的都是同一份数据。"
- 与 Singleton 的根本差异:Singleton 通过私有构造器把类的实例数量限制为一个;Monostate 则允许创建多个实例,但确保它们状态完全一致。正如 App.java 的注释 所写:"Monostate 模式确保类的所有实例拥有相同的状态,可以直接作为 Singleton 模式的替代品。"
在仓库中,该模式归入Creational(创建型)分类,同时被标注了 Encapsulation、Instantiation、Object composition、Persistence、Polymorphism 等多个标签,这暗示它既能做创建层面的状态共享,也涉及封装与持久化语义。
二、现实世界类比与直观理解
图书馆类比:想象一座图书馆里摆放着多张查询台,读者可以在任意一张台上查阅馆藏目录。虽然每张台看起来彼此独立,但任何台对目录的修改(如新增或下架一本书)都会立刻反映到所有台上。无论读者使用哪张台,看到的都是同一份最新目录——这正对应 Monostate 模式:类的多个实例共享同一状态,确保所有实例看到的数据始终一致。
用一句大白话说:Monostate 让一个类的多个实例共享同一状态,实现方式是把所有状态管理都汇聚到一个公共的共享结构(静态字段)中,从而在维持"独立对象"外观的同时保证状态全局一致。
这种设计带来的体验是:调用方拿到的是普通实例,写代码的方式与使用普通类完全一致,但背后所有读写都落在类级别的共享内存上。
三、模式流程图
下图是仓库中 Monostate 模式原理说明流程图(monostate/etc/monostate-flowchart.png):分别创建实例 A、实例 B,两个实例都访问共享的静态状态;实例 A 更新共享状态后,实例 B 读取到的是更新后的值——两个对象看似独立,行为却完全基于全局共享状态。
四、仓库源码级实现:LoadBalancer 负载均衡器
monostate 模块 用"负载均衡器 + 轮询派发请求"的场景演示 Monostate。核心类如下:
| 类 | 文件 | 职责 |
|---|---|---|
LoadBalancer | LoadBalancer.java | Monostate 载体,持有全部静态状态,按 Round Robin 轮询派发请求 |
Server | Server.java | 后端服务器,含 host、port、id,serve(Request)处理请求并打印日志 |
Request | Request.java | Javarecord,封装请求负载 |
App | App.java | 程序入口,演示两个实例共享状态 |
4.1 LoadBalancer 核心实现
仓库中的真实实现(与 README 示例略有差异,这里以源码为准)如下:
public class LoadBalancer { private static final List<Server> SERVERS = new ArrayList<>(); private static int lastServedId; static { var id = 0; for (var port : new int[] {8080, 8081, 8082, 8083, 8084}) { SERVERS.add(new Server("localhost", port, ++id)); } } /** Add new server. */ public final void addServer(Server server) { synchronized (SERVERS) { SERVERS.add(server); } } public final int getNoOfServers() { return SERVERS.size(); } public int getLastServedId() { return lastServedId; } /** Handle request. */ public synchronized void serverRequest(Request request) { if (lastServedId >= SERVERS.size()) { lastServedId = 0; } var server = SERVERS.get(lastServedId++); server.serve(request); } }要点拆解:
- 静态字段即共享状态:
SERVERS(服务器列表)与lastServedId(下一次派发的游标)都是static,它们不属于任何单个实例,而是属于类本身。所有LoadBalancer实例读写的是同一份内存。 - 静态初始化块:类加载时一次性向
SERVERS注册 5 台localhost:8080~8084的服务器,id 依次为 1~5。这段初始化只执行一次,与实例数量无关。 - 轮询派发逻辑:
serverRequest取出lastServedId指向的服务器,派发请求后将游标+1;当游标越界(>= SERVERS.size())时重置为 0,实现循环轮询(Round Robin)。多个实例交替调用时,会轮流命中 Server 1、Server 2、Server 3…… - 并发安全:
addServer对SERVERS加同步锁,serverRequest是synchronized方法,避免多线程下静态游标被并发改写导致错乱。静态共享状态天然是"全局临界区",因此并发控制必不可少。 - 透明的实例外观:调用方完全不知道状态是静态的,
new LoadBalancer()出来的对象用起来与普通对象无异——这正是"Monostate"(单一状态)命名的由来。
4.2 Server 与 Request:被共享状态驱动的配角
public record Request(String value) {}@Slf4j @Getter public class Server { public final String host; public final int port; public final int id; public Server(String host, int port, int id) { this.host = host; this.port = port; this.id = id; } public void serve(Request request) { LOGGER.info( "Server ID {} associated to host : {} and port {}. Processed request with value {}", id, host, port, request.value()); } }Request用 Java 16+ 的record简洁封装请求负载;Server用 Lombok 的@Getter/@Slf4j生成访问器与日志器,serve负责打印"哪台服务器处理了哪个请求",便于观察轮询结果。
4.3 程序入口:两个实例如何共享状态
App.java 只做三件事:创建两个LoadBalancer实例,然后依次派发两个请求:
public static void main(String[] args) { var loadBalancer1 = new LoadBalancer(); var loadBalancer2 = new LoadBalancer(); loadBalancer1.serverRequest(new Request("Hello")); loadBalancer2.serverRequest(new Request("Hello World")); }预期行为(README 原话):通过loadBalancer1发出的请求由Server 1处理;由于lastServedId已被第一个实例更新,通过loadBalancer2发出的请求由Server 2处理;若再创建第三个实例并派发请求,则会轮到Server 3。这就直观证明了"实例不同、状态同一":每个新实例看到的是已被前一个实例推进过的共享游标。
五、测试用例:如何验证"所有实例状态一致"
LoadBalancerTest.java 给出了两个可复用的验证思路:
1. 跨实例状态一致性测试
final var firstBalancer = new LoadBalancer(); final var secondBalancer = new LoadBalancer(); firstBalancer.addServer(new Server("localhost", 8085, 6)); // Both should have the same number of servers. assertEquals(firstBalancer.getNoOfServers(), secondBalancer.getNoOfServers()); // Both Should have the same LastServedId assertEquals(firstBalancer.getLastServedId(), secondBalancer.getLastServedId());只通过firstBalancer添加了一台服务器,但断言secondBalancer的服务器数量与游标与之相等——因为二者读的是同一份静态数据。
2. 轮询派发行为测试:用 Mockito 构造 mockServer,连续派发getNoOfServers() * 2次请求,验证每台 mock 服务器恰好被调用 2 次(times(2)),证实轮询按周而复始的循环推进。
此外,AppTest.java 以assertDoesNotThrow(() -> App.main(new String[] {}))保证入口可无异常运行。整个模块依赖 slf4j-api、logback-classic 与 JUnit 5、Mockito(见 monostate/pom.xml),并配置了 maven-assembly-plugin,主类为com.iluwatar.monostate.App,可在模块目录下直接打包运行:
mvn -pl monostate clean package java -jar monostate/target/monostate-1.26.0-SNAPSHOT-jar-with-dependencies.jar运行后日志会依次显示 Server 1、Server 2 分别处理Hello与Hello World两个请求。
六、何时应该使用 Monostate
根据 monostate/README.md,以下场景适合采用 Monostate:
- 需要跨实例共享状态:类的所有实例必须共享同一状态,某个实例的修改要立即反映到所有实例上。相比传统 Singleton,Monostate 更灵活——不限制实例数量。
- 希望透明使用:Singleton 的"全局唯一实例"在使用上不够透明;Monostate 让客户端像使用普通实例一样操作,无需感知共享状态的存在。
- 需要子类扩展灵活性:这是 Monostate 相对 Singleton 的明显优势——Monostate 的子类可以新增或修改行为,同时仍与基类实例共享同一份状态,从而在同一应用的不同部分演化出多样行为而共享同一份数据。
- 想避免全局变量但仍需共享状态:全局变量难以控制访问、难以测试;Monostate 把共享状态收进类内,借助方法访问,规避裸全局变量。
- 需要融入既有系统:已有系统若习惯以"实例"为单位协作而非单个全局对象,Monostate 更易无缝接入,重构为共享状态时过渡更平滑。
- 需要一致的配置或状态管理:当应用各部分要求配置/状态始终同步时,Monostate 可保证全体一致。
典型真实应用场景:① 需要被应用各部分共享访问的配置设置;② 需要跨多个消费者保持状态一致性的资源池(资源连接池、线程池、对象池等,本仓库的 LoadBalancer 服务器池即属此类)。
七、收益与代价
收益(Benefits):
- 不限制实例创建,就能提供全局共享状态;
- 保证所有实例间的数据一致性。
代价(Trade-offs):
- 隐藏依赖:共享状态带来隐式耦合,使系统更难理解和调试——某个实例改状态,其他实例"莫名其妙"受影响;
- 失去实例独立性:无法再为不同实例维护彼此独立的状态,灵活性下降。
八、与相关模式的对比
- Singleton 单例模式:两者都保证"单一共享状态"。Singleton 靠限制实例数量达成,Monostate 靠静态成员共享状态、允许多实例。Singleton 的构造器通常私有、继承受限;Monostate 可正常继承扩展,但状态全局唯一这一点二者一致。
- Flyweight 享元模式:Flyweight 通过共享(部分)内部状态来降低内存占用,与 Monostate 共享状态的思想同源,但 Flyweight 面向"大量细粒度对象的内存优化",Monostate 面向"多实例行为一致性"。
从 UML 类图(monostate/etc/MonoState.ucls、monostate/etc/monostate.urm.puml)看,LoadBalancer的属性被标记为静态(带S),与Server是一对多关联,与Request是依赖关系,与上文源码完全对应。
九、使用建议与注意事项
- 优先考虑 Singleton 还是 Monostate?若只需"一个全局实例"且无需继承,Singleton 更直观、显式;若代码库大量以实例方式协作、且需要子类化扩展共享行为,Monostate 更合适。
- 务必处理并发:静态状态是进程级共享临界区,写入操作应加锁(仓库中
synchronized即示范),否则多线程下轮询游标、服务器列表会被竞争破坏。 - 谨慎引入隐藏依赖:Monostate 的共享是隐式的,规模变大后"某个实例改了状态影响全局"的副作用难以追踪;可用显式命名(如
LoadBalancer这类角色化类名)降低迷惑性。 - 测试策略:参考仓库测试——用"通过实例 A 修改、断言实例 B 观察到一致结果"的用例直接验证共享语义。
Monostate 与 Singleton 殊途同归,都是"类级别唯一状态"的载体;当你需要"多个对象、一份真相"时,它就是那个兼顾透明性与扩展性的选择。
- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
相关推荐
java-design-patterns 中的 Monostate 模式:以类级静态状态实现多实例共享的 Singleton 替代方案
java design patterns 中的 Monostate 模式:以类级静态状态实现多实例共享的 Singleton 替代方案 导读 Monostate
示例工程教程java-design-patterns 中的 Bloc 模式:用 Java 实现响应式状态管理与动态监听器
java design patterns 中的 Bloc 模式:用 Java 实现响应式状态管理与动态监听器 导读 Bloc(Business Logic Co
示例工程教程java-design-patterns 中的奇特递归模板模式(CRTP):用递归类型边界实现编译期静态多态
java design patterns 中的奇特递归模板模式(CRTP):用递归类型边界实现编译期静态多态 本文基于 localization/ar/crtp
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考