Java Composition本质:对象职责建模与生命周期管理

📅 2026/6/22 4:01:00
Java Composition本质:对象职责建模与生命周期管理
1. Composition 不是“组合”而是 Java 中被严重低估的建模基石很多人第一次看到 “Composition in Java Example” 这个标题下意识会想“哦就是讲怎么把几个类拼在一起用吧不就是 new 一个对象再 set 进去吗”——这种理解错得非常典型也错得非常危险。它直接导致大量 Java 开发者在真实项目中写出大量难以维护、无法测试、边界模糊的代码。我带过三届校招新人几乎每届都有人把 Composition 和 Inheritance 混为一谈甚至在 Spring Boot 项目里用继承去“复用” Service 层逻辑结果改一个方法五个模块同时报空指针。Composition 的本质不是语法层面的“把 A 类放进 B 类里”而是语义层面的“B 由 A 构成A 是 B 的一部分A 的生命周期依附于 B”。这个定义里藏着三个关键判断锚点has-a 关系是否真实存在A 是否可替换A 的销毁是否由 B 控制比如“汽车 has-a 发动机”成立但“汽车 is-a 发动机”显然荒谬发动机可以被不同厂商的型号替换松耦合而一旦汽车报废发动机通常也跟着进回收站生命周期绑定。这三点才是 Composition 区别于 Association关联、Aggregation聚合甚至 Inheritance继承的分水岭。你在网上搜到的绝大多数“Composition 示例”只停留在class Car { private Engine engine; }这一行代码上然后就跳到engine.start()。这根本不是 Composition这只是“持有引用”。真正的 Composition 必须回答这个 engine 是谁创建的谁负责关闭它如果 engine 初始化失败car 能否进入可用状态它的配置参数从哪来这些细节恰恰是面试官在问“请手写一个 Composition 示例”时真正想听的——他要的不是语法正确性而是你对对象职责边界的认知深度。这也是为什么“desktop composition is disabled”这类系统级错误提示会高频出现在 Java 开发者搜索记录里表面看是 Windows 桌面特效开关问题深层反映的是开发者对“composition”一词在不同上下文中的语义混淆。图形界面里的 desktop composition 指的是像素图层的合成渲染而 Java 编程中的 composition 指的是对象职责的合成建模。两个词同源但领域完全不同。混淆它们就像把“数据库索引”和“图书目录索引”当成一回事——都叫索引但一个靠 B 树一个靠人工编目。所以本文不提供“教科书式”的 Composition 定义而是带你从零开始亲手构建一个真实场景下的 Composition 实现一个支持热插拔协议解析器的网络消息处理器。它会强制你面对初始化顺序、资源释放、异常传播、依赖注入边界等所有 Composition 的核心痛点。你将看到一个看似简单的private final ProtocolParser parser;声明背后藏着至少七层设计决策。2. 为什么不用继承一次真实线上事故的复盘去年我们一个支付网关服务突然出现偶发性超时监控显示 99% 的请求耗时正常但总有 0.3% 的请求卡在MessageProcessor.process()方法里长达 15 秒以上最终触发熔断。排查三天后定位到问题根源竟是一段三年前写的“优雅复用”代码// 当年为了快速支持新协议写了这个继承结构 public class XmlMessageProcessor extends JsonMessageProcessor { Override public Message process(String raw) { // 先调父类解析 JSON再额外处理 XML 特有字段 Message msg super.process(raw); parseXmlSpecificFields(msg, raw); return msg; } }问题出在哪表面上看XmlMessageProcessor确实“复用”了JsonMessageProcessor的解析逻辑。但JsonMessageProcessor内部持有一个JsonParser实例该实例在构造时会加载 Jackson 的ObjectMapper并预热反射缓存。而XmlMessageProcessor的构造函数里又悄悄 new 了一个SAXParser实例。这两个解析器共享同一个线程池和内存缓冲区当高并发 XML 请求涌入时SAXParser的 SAX 事件回调会抢占ObjectMapper的线程资源导致 JSON 解析器内部锁等待进而阻塞整个process()方法。这就是典型的Inheritance 误用引发的隐式耦合。XmlMessageProcessor继承JsonMessageProcessor意味着它必须完全理解并承担父类的所有实现细节、资源消耗模式和线程安全契约。而 Composition 的解法是让两者彻底解耦public class MessageProcessor { private final JsonParser jsonParser; private final XmlParser xmlParser; // 构造时明确声明依赖且类型抽象化 public MessageProcessor(JsonParser jsonParser, XmlParser xmlParser) { this.jsonParser Objects.requireNonNull(jsonParser); this.xmlParser Objects.requireNonNull(xmlParser); } public Message process(String raw) { if (isXml(raw)) { return xmlParser.parse(raw); // 完全独立的资源、线程、生命周期 } else { return jsonParser.parse(raw); } } }这里的关键转变有三点控制权反转不再由MessageProcessor自己决定创建哪个解析器而是由外部比如 Spring 容器或工厂类按需注入。MessageProcessor只关心“能用”不关心“怎么造”。接口隔离JsonParser和XmlParser都实现统一的ParserMessage接口MessageProcessor对具体实现一无所知。新增ProtobufParser时只需实现接口无需修改MessageProcessor任何一行代码。生命周期解耦JsonParser和XmlParser的初始化、销毁、配置都可以独立管理。JsonParser可以用单例模式全局复用XmlParser可以按请求创建新实例避免状态污染。那次事故后我们团队立下铁规任何新功能禁止使用 extends 关键字实现业务逻辑复用。必须先画出对象关系图确认是 is-a 还是 has-a如果是 has-a必须用 final 字段 构造注入并在单元测试中 mock 所有依赖。这条规则看似死板却让我们后续两年的线上故障率下降了 67%。因为 Composition 强制你把“谁负责什么”这件事提前想清楚、写明白、测到位。3. 从零构建一个生产级 Composition 示例ProtocolRouter现在我们动手实现一个真正能上生产环境的 Composition 示例ProtocolRouter。它不是一个玩具类而是一个支撑日均 2 亿请求的消息路由核心。它的职责很明确接收原始字节流识别协议类型HTTP/HTTPS/WebSocket/自定义二进制将数据路由给对应的协议处理器并确保处理器的资源被正确管理。这个例子将覆盖 Composition 的全部核心实践。3.1 核心接口设计用抽象隔离变化Composition 的起点永远是接口。我们不从具体类开始而是先定义协议处理器的行为契约/** * 协议处理器接口所有具体处理器必须实现此接口 * 重点定义了初始化、处理、销毁三个生命周期方法 */ public interface ProtocolHandler { /** * 初始化处理器加载配置、建立连接池等 * throws HandlerInitializationException 初始化失败时抛出 */ void initialize() throws HandlerInitializationException; /** * 处理单条消息返回处理结果 * param message 原始消息字节流 * return 处理后的响应 */ Response handle(byte[] message); /** * 销毁处理器释放所有资源连接、缓存、线程池 */ void destroy(); }这个接口的设计暗含 Composition 的哲学把“做什么”handle和“何时做”initialize/destroy清晰分离。initialize()和destroy()方法的存在就是 Composition 对生命周期管理的硬性要求。没有这两个方法你就无法保证ProtocolRouter在启动时正确初始化所有处理器在关闭时安全释放所有资源。接着我们定义路由策略接口它决定了“哪个消息该交给哪个处理器”/** * 路由策略根据消息特征选择处理器 * Composition 的灵活性正体现在此处——策略可自由替换 */ public interface RoutingStrategy { /** * 根据消息头、长度、魔数等特征返回匹配的处理器 * param message 原始消息 * return 匹配的处理器null 表示无匹配 */ ProtocolHandler selectHandler(byte[] message); }注意RoutingStrategy的selectHandler方法返回的是ProtocolHandler接口而不是某个具体实现类。这意味着ProtocolRouter对具体策略一无所知它可以是基于 HTTP Header 的HeaderBasedStrategy也可以是基于 TLS 握手包的TlsHandshakeStrategy甚至可以是机器学习模型驱动的MLRoutingStrategy。只要它们实现了接口ProtocolRouter就能无缝集成。3.2 ProtocolRouter 的 Composition 结构final 字段与构造注入ProtocolRouter类本身就是 Composition 的典范。它不继承任何东西只通过final字段持有其组成部分的引用并在构造时强制注入public class ProtocolRouter { // 所有依赖都声明为 final确保不可变性与线程安全基础 private final ListProtocolHandler handlers; private final RoutingStrategy routingStrategy; private final Logger logger; private final AtomicBoolean isRunning new AtomicBoolean(false); /** * 构造函数强制注入所有依赖体现 Composition 的“组装”思想 * param handlers 所有可用的协议处理器列表 * param routingStrategy 路由策略 * param logger 日志器也是 Composition 的一部分 */ public ProtocolRouter(ListProtocolHandler handlers, RoutingStrategy routingStrategy, Logger logger) { // 防御性检查确保 Composition 的根基稳固 this.handlers Objects.requireNonNull(handlers, handlers must not be null); this.routingStrategy Objects.requireNonNull(routingStrategy, routingStrategy must not be null); this.logger Objects.requireNonNull(logger, logger must not be null); // 验证所有处理器必须已初始化否则 Router 无法工作 for (ProtocolHandler handler : handlers) { if (!(handler instanceof InitializedHandler)) { throw new IllegalArgumentException( All handlers must implement InitializedHandler or be pre-initialized); } } } /** * 启动 Router依次初始化所有处理器 * 这是 Composition 生命周期管理的关键一步 */ public void start() throws RouterStartException { if (!isRunning.compareAndSet(false, true)) { return; // 已启动避免重复初始化 } logger.info(Starting ProtocolRouter with {} handlers, handlers.size()); for (int i 0; i handlers.size(); i) { try { handlers.get(i).initialize(); logger.debug(Handler {} initialized successfully, i); } catch (HandlerInitializationException e) { // 关键设计单个处理器初始化失败应停止整个 Router 启动 // 这体现了 Composition 的整体性部件失效系统不可用 logger.error(Failed to initialize handler {}, i, e); stop(); // 立即清理已初始化的部分 throw new RouterStartException(Failed to start router: e.getMessage(), e); } } logger.info(ProtocolRouter started successfully); } /** * 处理消息Composition 的核心行为 * param message 原始字节流 * return 处理结果 */ public Response route(byte[] message) { if (!isRunning.get()) { throw new IllegalStateException(Router is not running. Call start() first.); } ProtocolHandler handler routingStrategy.selectHandler(message); if (handler null) { logger.warn(No handler found for message of length {}, message.length); return Response.error(Unsupported protocol); } try { return handler.handle(message); } catch (Exception e) { logger.error(Error handling message with handler {}, handler.getClass().getSimpleName(), e); return Response.error(Internal processing error); } } /** * 停止 Router依次销毁所有处理器确保资源释放 * Composition 的闭环管理 */ public void stop() { if (!isRunning.compareAndSet(true, false)) { return; // 已停止 } logger.info(Stopping ProtocolRouter...); // 倒序销毁通常后初始化的先销毁避免依赖冲突 for (int i handlers.size() - 1; i 0; i--) { try { handlers.get(i).destroy(); logger.debug(Handler {} destroyed, i); } catch (Exception e) { logger.error(Error destroying handler {}, i, e); // 销毁失败不中断整体流程继续销毁其他处理器 } } logger.info(ProtocolRouter stopped); } }这段代码里ProtocolRouter的 Composition 结构一目了然handlers一组ProtocolHandler代表其“能力集合”。它们是 Router 的“器官”。routingStrategy决定如何调度这些“器官”的“大脑”。logger负责记录所有关键事件的“神经系统”。所有字段都是final确保一旦组装完成其组成结构就不可更改。构造函数的参数列表就是这个对象的“装配说明书”。任何试图绕过构造函数直接 new 一个ProtocolRouter的做法都会因缺少必要依赖而编译失败——这是编译器强制你遵守 Composition 规则的第一道防线。提示AtomicBoolean isRunning的存在是 Composition 在并发场景下的必然要求。它不是一个“功能”而是一个状态协调器用于同步start()和stop()的执行确保route()方法不会在 Router 未启动或已停止的状态下被调用。这个小细节正是 Composition 思维深入骨髓的体现每一个状态都必须有明确的归属和管理责任。3.3 具体处理器实现HttpHandler 的完整生命周期现在我们来实现一个具体的ProtocolHandlerHttpHandler。它将展示 Composition 如何将一个复杂的功能HTTP 协议解析分解为更小的、可独立管理的部件。public class HttpHandler implements ProtocolHandler { // HttpHandler 的 Composition 结构它自己也是一个“组装体” private final HttpRequestParser requestParser; // 解析 HTTP 请求行和头 private final HttpResponseBuilder responseBuilder; // 构建 HTTP 响应 private final ConnectionPool connectionPool; // 管理底层 TCP 连接 private final RateLimiter rateLimiter; // 限流器防止过载 // 所有部件都通过构造注入且均为 final public HttpHandler(HttpRequestParser requestParser, HttpResponseBuilder responseBuilder, ConnectionPool connectionPool, RateLimiter rateLimiter) { this.requestParser Objects.requireNonNull(requestParser); this.responseBuilder Objects.requireNonNull(responseBuilder); this.connectionPool Objects.requireNonNull(connectionPool); this.rateLimiter Objects.requireNonNull(rateLimiter); } Override public void initialize() throws HandlerInitializationException { try { // 初始化所有子部件 requestParser.initialize(); responseBuilder.initialize(); connectionPool.initialize(); rateLimiter.initialize(); // 额外的初始化逻辑预热连接池 connectionPool.preheat(10); // 预热 10 个连接 } catch (Exception e) { throw new HandlerInitializationException(Failed to initialize HttpHandler, e); } } Override public Response handle(byte[] message) { // 1. 限流检查 if (!rateLimiter.tryAcquire()) { return Response.error(Too many requests); } // 2. 解析请求 HttpRequest request; try { request requestParser.parse(message); } catch (ParseException e) { return Response.error(Invalid HTTP request: e.getMessage()); } // 3. 从连接池获取连接 Connection conn; try { conn connectionPool.acquire(); } catch (ConnectionPoolException e) { return Response.error(Service unavailable); } // 4. 处理业务逻辑此处简化为模拟 String responseBody processBusinessLogic(request); // 5. 构建响应 byte[] responseBytes responseBuilder.build( 200, OK, Map.of(Content-Type, text/plain), responseBody.getBytes(StandardCharsets.UTF_8) ); // 6. 归还连接 connectionPool.release(conn); return Response.success(responseBytes); } Override public void destroy() { // 销毁所有子部件顺序与初始化相反 connectionPool.destroy(); rateLimiter.destroy(); responseBuilder.destroy(); requestParser.destroy(); } private String processBusinessLogic(HttpRequest request) { // 真实业务逻辑此处省略 return Hello from HttpHandler; } }HttpHandler本身就是一个 Composition 的嵌套实例。它不自己实现 HTTP 解析、连接管理、限流等任何功能而是将这些职责委托给更小、更专注的部件HttpRequestParser,ConnectionPool等。每个部件都拥有自己的initialize()和destroy()方法HttpHandler的initialize()和destroy()方法就是对这些子部件生命周期的编排。这种层层嵌套的 Composition构建出了一个健壮、可测试、易扩展的系统。你可以单独为HttpRequestParser写单元测试mock 掉ConnectionPool你可以为ConnectionPool写压力测试验证其在 10000 并发下的表现你甚至可以将HttpHandler的ConnectionPool替换为一个内存版的MockConnectionPool用于快速集成测试。这一切都源于 Composition 对“关注点分离”和“生命周期显式化”的坚持。4. Composition 的陷阱与避坑指南那些文档里不会写的实战经验Composition 理念虽好但在真实项目落地时会遇到一堆文档里绝不会提及的“灰色地带”问题。这些问题往往不会导致编译失败却会让代码在生产环境里悄无声息地腐烂。以下是我在多个大型 Java 项目中踩过的坑以及总结出的硬核避坑指南。4.1 陷阱一循环依赖——不是架构问题是设计信号最经典的循环依赖场景是UserService和EmailService// UserService.java public class UserService { private final EmailService emailService; public UserService(EmailService emailService) { this.emailService emailService; } public void createUser(User user) { // ... 创建用户逻辑 emailService.sendWelcomeEmail(user); // 依赖 EmailService } } // EmailService.java public class EmailService { private final UserService userService; public EmailService(UserService userService) { this.userService userService; } public void sendWelcomeEmail(User user) { // ... 发送邮件逻辑 userService.updateUserStatus(user.getId(), EMAIL_SENT); // 反向依赖 UserService } }Spring 容器可能会用三级缓存勉强解决这个循环依赖但这是饮鸩止渴。循环依赖从来不是 Spring 的 bug而是你领域模型设计错误的强烈信号。它表明UserService和EmailService的职责边界已经模糊不清它们本应是两个独立的、松耦合的组件却被强行绑在了一起。避坑方案引入事件总线Event Bus将“发送欢迎邮件”这个动作从一个同步的、强耦合的方法调用改为一个异步的、松耦合的事件发布// 定义事件 public class UserCreatedEvent { private final User user; public UserCreatedEvent(User user) { this.user user; } // getter... } // UserService 不再依赖 EmailService public class UserService { private final EventPublisher eventPublisher; // 事件发布器一个轻量级依赖 public UserService(EventPublisher eventPublisher) { this.eventPublisher eventPublisher; } public void createUser(User user) { // ... 创建用户逻辑 eventPublisher.publish(new UserCreatedEvent(user)); // 发布事件不关心谁消费 } } // EmailService 订阅事件 public class EmailService implements EventHandlerUserCreatedEvent { Override public void handle(UserCreatedEvent event) { // ... 发送邮件逻辑 // 此处可以安全地调用 UserService 的查询方法但绝不能调用更新方法 // 因为事件处理是异步的且发生在用户创建之后 } }这个改动将UserService和EmailService之间的强依赖UserService - EmailService - UserService打破变成了UserService - EventPublisher和EmailService - EventPublisher的两个单向依赖。EventPublisher是一个极简的、无状态的基础设施组件它不包含任何业务逻辑因此不会引发新的循环依赖。这才是 Composition 应有的样子部件之间通过清晰、单向的契约进行协作。4.2 陷阱二过度设计的“可插拔”——让简单问题复杂化很多开发者受“设计模式八股文”影响一上来就想搞“高度可插拔”。于是一个本可以用if-else判断的协议识别逻辑被硬生生拆成ProtocolDetector接口、HttpDetector、WebSocketDetector、CustomBinaryDetector三个实现类再加一个DetectorFactory和DetectorRegistry。代码量翻了五倍可读性暴跌而实际业务需求可能未来五年都不会增加第四种协议。避坑方案YAGNIYou Arent Gonna Need It原则 渐进式重构我的经验是先用最简单、最直接的方式实现第一个需求然后在第二个需求出现时再提取公共部分。比如最初只有 HTTPpublic class SimpleProtocolRouter { public Response route(byte[] message) { if (isHttp(message)) { return httpHandler.handle(message); } throw new UnsupportedOperationException(Unknown protocol); } }当 WebSocket 需求来了先写一个isWebSocket(message)判断加到if-else里。当第三个协议来了if-else变得臃肿时再考虑提取ProtocolDetector接口。此时你已经有了三个真实的、经过验证的实现类提取接口的风险极低且能精准抓住共性。注意这里的“简单”不是指代码少而是指心智负担小、修改成本低。一个 20 行的if-else远比一个需要理解 5 个新类、3 个新接口、1 个工厂模式的“优雅”设计更容易维护。Composition 的终极目标是降低复杂度而不是制造新的复杂度。4.3 陷阱三资源泄漏——finalize() 是个陷阱不是救星很多开发者认为只要在destroy()方法里释放了资源就万事大吉。但他们忽略了 JVM 的 GC 机制destroy()是手动调用的而finalize()方法是 JVM 在对象被 GC 前自动调用的且JVM 不保证finalize()一定会被调用也不保证调用时机。这意味着如果你只依赖finalize()来释放文件句柄、数据库连接等稀缺资源你的应用迟早会因“Too many open files”而崩溃。避坑方案使用 try-with-resources 显式 destroy()Java 7 引入的try-with-resources语句是 Composition 资源管理的黄金搭档。它要求资源类实现AutoCloseable接口而AutoCloseable.close()方法就是destroy()的标准命名// 让 ProtocolHandler 实现 AutoCloseable public interface ProtocolHandler extends AutoCloseable { void initialize() throws HandlerInitializationException; Response handle(byte[] message); Override void close(); // 标准的 destroy() 方法名 } // 在 Router 的 stop() 方法中使用 try-with-resources 的语义 public void stop() { if (!isRunning.compareAndSet(true, false)) return; // 使用 Collections.synchronizedList 或 CopyOnWriteArrayList 确保线程安全 for (ProtocolHandler handler : new ArrayList(handlers)) { try { handler.close(); // 显式调用 } catch (Exception e) { logger.error(Error closing handler, e); } } }更重要的是在业务代码中也要养成try-with-resources的习惯public Response processWithSafety(byte[] message) { // 创建一个临时的、轻量级的处理器实例 try (ProtocolHandler tempHandler new TempHttpHandler()) { tempHandler.initialize(); return tempHandler.handle(message); } catch (Exception e) { logger.error(Error in temporary handler, e); return Response.error(Processing failed); } }try-with-resources保证了无论handle()方法是正常返回还是抛出异常tempHandler.close()都会被执行。这比任何finally块都更可靠也比寄希望于finalize()更务实。Composition 的资源管理必须是确定性的、可预测的、可测试的。4.4 陷阱四测试困境——如何 Mock 一个“组合体”当你想为ProtocolRouter写单元测试时会发现它依赖ListProtocolHandler和RoutingStrategy。如果直接 new 一个真实的HttpHandler测试就会变得又慢又脆弱因为它会尝试连接真实的网络、数据库。你需要 Mock 它们。避坑方案使用 Mockito 的Mock和InjectMocks但要理解其原理RunWith(MockitoJUnitRunner.class) public class ProtocolRouterTest { Mock private ProtocolHandler httpHandler; Mock private ProtocolHandler wsHandler; Mock private RoutingStrategy routingStrategy; Mock private Logger logger; InjectMocks private ProtocolRouter router; // Mockito 会自动将上面的 Mock 注入到这里 Before public void setUp() { // 初始化 Router传入 Mock 对象列表 ListProtocolHandler handlers Arrays.asList(httpHandler, wsHandler); this.router new ProtocolRouter(handlers, routingStrategy, logger); } Test public void testRouteToHttpHandler() { byte[] httpMessage GET / HTTP/1.1\r\nHost: example.com\r\n\r\n.getBytes(); // 配置 Mock当 routingStrategy.selectHandler 被调用时返回 httpHandler when(routingStrategy.selectHandler(httpMessage)).thenReturn(httpHandler); when(httpHandler.handle(httpMessage)).thenReturn(Response.success(new byte[0])); // 执行测试 Response response router.route(httpMessage); // 验证结果和交互 assertThat(response.isSuccess()).isTrue(); verify(httpHandler).handle(httpMessage); // 验证 httpHandler.handle 被调用 verifyNoMoreInteractions(wsHandler); // 验证 wsHandler 没有被调用 } }关键点在于InjectMocks。它不是魔法而是 Mockito 在运行时通过反射找到router对象中所有private final字段如handlers,routingStrategy,logger然后将Mock标记的对象按照类型匹配注入进去。这完美契合了 Composition 的构造注入模式。如果你的ProtocolRouter是用setter方法注入依赖的InjectMocks就无法工作你必须手动调用setXXX()方法。所以坚持使用构造注入不仅是设计原则更是为了测试便利性。5. Composition 与 Java 生态Spring、Lombok 和现代开发实践Composition 不是孤立存在的理念它深深植根于 Java 的现代开发生态中。理解它与主流框架、工具的协同关系才能真正将其融入日常开发。5.1 Spring FrameworkComposition 的最佳实践平台Spring 的核心思想就是 Composition。Component,Service,Repository这些注解本质上就是在告诉 Spring 容器“请帮我创建这个对象并把它作为 Composition 的一个部件。” 而Autowired就是 Spring 为你自动完成的构造注入。Service public class OrderService { private final PaymentService paymentService; // final 字段 private final InventoryService inventoryService; // 构造注入Spring 5 推荐方式 public OrderService(PaymentService paymentService, InventoryService inventoryService) { this.paymentService paymentService; this.inventoryService inventoryService; } public Order createOrder(OrderRequest request) { // ... 业务逻辑 paymentService.charge(request.getPaymentInfo()); // 依赖注入的部件 inventoryService.reserve(request.getItems()); // 依赖注入的部件 return new Order(); } }Spring 容器在启动时会扫描所有Component类根据它们的构造函数参数递归地创建并注入所有依赖。这整个过程就是 Composition 的自动化装配。你不需要手动 new 一堆对象再塞进去Spring 会帮你搞定。这极大地降低了 Composition 的使用门槛但也带来一个风险过度依赖 Spring 的自动装配会让你忘记 Composition 的本质。你应该始终能清晰地说出OrderService由哪些部件构成每个部件的职责是什么它们的生命周期如何管理如果某天你离开 Spring换用 Micronaut 或 Quarkus这些知识依然有效。5.2 Lombok减少样板代码聚焦 Composition 本质Lombok 的RequiredArgsConstructor和AllArgsConstructor注解是 Composition 的强力加速器。它们自动生成构造函数让你可以专注于定义final字段而不必为每个字段手写this.field field。Service RequiredArgsConstructor // 自动生成只包含 final 字段的构造函数 public class UserService { private final UserRepository userRepository; // final必须注入 private final EmailService emailService; // final必须注入 private final Logger logger; // final必须注入 // Lombok 自动生成 // public UserService(UserRepository userRepository, // EmailService emailService, // Logger logger) { // this.userRepository userRepository; // this.emailService emailService; // this.logger logger; // } public User createUser(User user) { User saved userRepository.save(user); emailService.sendWelcomeEmail(saved); return saved; } }RequiredArgsConstructor只为final和NonNull字段生成参数这完美契合了 Composition 的要求所有必需的部件都必须在构造时提供。它用一行注解就强制团队遵守了 Composition 的最佳实践。当然Lombok 也有陷阱比如Data会生成toString()、equals()等方法如果类里有敏感字段如密码就必须手动重写toString()来排除。但这属于另一个话题了。5.3 Java 14 Record为不可变数据建模的 Composition 新范式Java 14 引入的Record是 Composition 在数据建模领域的革命。它天生就是不可变的、透明的、基于值的。一个Record天然就是一个 Composition 的“数据部件”。// 一个纯粹的数据载体没有行为只有结构 public record HttpRequest( HttpMethod method, String path, MapString, String headers, byte[] body ) { // 可以添加静态工厂方法增强可读性 public static HttpRequest ofGet(String path) { return new HttpRequest(HttpMethod.GET, path, Map.of(), new byte[0]); } } // 在 ProtocolHandler 中它就是一个完美的输入/输出部件 public interface ProtocolHandler { // 输入是 HttpRequest Record输出是 HttpResponse Record HttpResponse handle(HttpRequest request); }Record的不可变性与 Composition 的final字段精神高度一致。它消除了数据在传递过程中被意外修改的风险让ProtocolRouter的各个部件之间可以放心地共享数据而不用担心状态污染。这是 Composition 在数据流层面的自然延伸。6. Composition 的终极价值不是代码技巧而是思维范式写完这个ProtocolRouter示例你可能会觉得“哦原来 Composition 就是多写几个接口多用几个final字段再把new换成Autowired。” 如果你只看到这一层那这篇文章就失败了。Composition 的终极价值远不止于此。它是一种对抗软件熵增的思维范式。软件系统天生趋向混乱需求变更、人员流动、技术迭代都会像热力学第二定律一样让代码库的“熵”不断增加。而 Composition就是一套行之有效的“负熵”机制。它通过强制你回答以下问题来持续对抗混乱这个类的职责到底是什么回答不了就说明它违反了单一职责原则应该被拆分它由哪些更小的、更专注的部件构成回答不了就说明它还不够“组合”还太“原子”这些部件的生命周期是由谁来管理的回答不了就说明资源管理是模糊的迟早泄漏如果我要替换掉其中某一个部件需要修改多少其他代码修改越多耦合越紧Composition 越失败每一次你认真思考并回答这些问题你都在为系统的长期健康投票。你写的不是一段代码而是一份关于“这个系统应该如何被理解和演进”的契约。这份契约比任何框架、任何语法糖都重要。所以下次当你看到一个“Java Composition Example”的搜索请求不要急着去复制粘贴一个class A { private B b; }的例子。停下来问问自己在这个例子的场景里A和B之间是否存在真实的“has-a”关系B的创建、配置、销毁是否都由A明确掌控如果B初始化失败A是否还能作为一个可用的整体存在如果答案是否定的那么你看到的很可能只是一个披着 Composition 外衣的、脆弱的、不可维护的代码片段。Composition 不是终点而是一个起点。它始于一个final字段的声明成于无数次对“职责”与“边界”的审慎思考终于一个在风雨中依然稳健运行的系统。这条路没有捷径但每一步都算数。