建造者模式:从“参数地狱”到优雅构建

深夜,一条紧急告警刺穿寂静:核心报表服务因NullPointerException全线崩溃。排查根源,罪魁祸首竟是一个拥有10多个参数的“上帝构造函数”。本文将从这个灾难现场出发,引入“链式建造者模式”进行重构,并深入Spring AIOkHttp及电商物流、支付网关等真实场景,剖析其Builder是如何优雅地构建复杂契约的。你将彻底掌握这一构建复杂、不可变对象的终极武器,并看透它在现代框架设计中的核心地位。

一场由null引发的生产瘫痪

那是一个发布新功能的夜晚,我们为数据中台的报表导出功能增加了一个新的筛选条件。看似简单的改动,上线后却触发了大规模的NullPointerException,导致所有异步报表任务失败。

经过紧急回滚和复盘,问题定位在一个平平无奇的ReportRequest对象的创建上。

image-20250811091027330

“上帝构造函数”的“原罪”

为了创建一个报表请求,开发者需要实例化一个ReportRequest对象,它的构造函数长这样:

public class ReportRequest {private String reportName;  // 必填private long startDate;     // 必填private long endDate;       // 必填private String filterByUser; // 可选private String filterByDept; // 可选// ... 可能还有10个其他可选参数// “伸缩构造器”反模式:为了应对可选参数,写了一堆重载构造函数public ReportRequest(String reportName, long startDate, long endDate) {this(reportName, startDate, endDate, null, null, ...);}// ... 还有更多构造函数
}// 调用方的噩梦
ReportRequest request = new ReportRequest("MonthlySalesReport", start, end, null, null, "EU", 0, true, "PDF");

“罪状”分析:

  1. 可读性极差:当参数超过5个,尤其是类型相同时,你很难分清哪个null对应哪个参数。这次事故,就是因为一位同事在调用时,将两个null的位置搞反了。
  2. 维护地狱:每增加一个可选参数,你就得新增一个构造函数,或者修改一长串现有的构造函数链。
  3. 无法保证一致性:对象在构造函数执行完毕之前,可能处于一种“半成品”状态。

链式建造者的降维打击

重构

要解决这个地狱,建造者模式登场了。我们采用在现代开源框架中更流行的链式建造者(Fluent Builder)

import com.google.common.base.Preconditions;public class ReportRequest {private final String reportName;  // 必填,设为finalprivate final long startDate;     // 必填,设为finalprivate final long endDate;       // 必填,设为finalprivate final String filterByDept;private final String exportFormat;// ... 其他属性均为final// 构造函数变为private,只能通过Builder创建private ReportRequest(Builder builder) {this.reportName = builder.reportName;this.startDate = builder.startDate;this.endDate = builder.endDate;this.filterByDept = builder.filterByDept;this.exportFormat = builder.exportFormat;}// 静态内部类Builderpublic static class Builder {// 必填参数在Builder的构造函数中强制传入private final String reportName;private final long startDate;private final long endDate;// 可选参数提供默认值private String filterByDept = null;private String exportFormat = "CSV";public Builder(String reportName, long startDate, long endDate) {this.reportName = reportName;this.startDate = startDate;this.endDate = endDate;}// 每一个setter方法都返回Builder自身,实现链式调用public Builder byDept(String filterByDept) {this.filterByDept = filterByDept;return this;}public Builder format(String exportFormat) {this.exportFormat = exportFormat;return this;}// build()方法负责创建最终的、不可变的对象public ReportRequest build() {// 可以在这里进行复杂的校验逻辑Preconditions.checkNotNull(reportName, "Report name cannot be null");Preconditions.checkArgument(startDate < endDate, "Start date must be before end date");return new ReportRequest(this);}}
}// 调用方的春天
ReportRequest request = new ReportRequest.Builder("MonthlySalesReport", start, end).byDept("Sales-EU").format("PDF").build();

降维打击在哪?

  1. 可读性.byDept("...") .format("..."),代码即文档,清晰明了。
  2. 安全性:必填参数在构造时强制传入,可选参数通过具名方法设置,彻底告别null的顺序混淆。
  3. 不可变性ReportRequest对象的所有字段都是final的,并在build()方法中一次性完成构建。一旦创建,状态就无法被修改,是线程安全的。

看看大师们的源码棋谱

建造者模式的威力,远不止于此。在企业级架构中,它是一种构建复杂“契约”的核心思想。让我们直接深入源码和真实业务,看看大师们是如何下这盘棋的。

实战一:电商统一物流下单

  • 场景:在一个电商平台,当一个订单需要发货时,系统需要调用一个统一的物流服务。这个服务需要整合多家物流公司(顺丰、圆通等)的API。创建一个物流下单请求(ShipmentOrder)非常复杂,包含收发件人信息、包裹详情、保价、代收货款、签收回执等大量可选参数。

  • 建造者应用:设计一个ShipmentOrder.Builder,将复杂的下单流程变得清晰可控。

    // 伪代码
    ShipmentOrder order = new ShipmentOrder.Builder("SF", "order123", sender, recipient).withInsurance(new BigDecimal("5000.00")) // 申请保价.withCod(order.getTotalAmount()) // 代收货款.requireSignature() // 要求签收回执.withDeliveryNotes("易碎品,请轻放").build();
    // builder的build()方法内部可以进行组合校验,
    // 例如“代收货款金额不能超过保价金额”
    

实战二:对接银联支付网关

  • 场景:对接传统的金融机构如银联(UnionPay)的支付网关时,其API请求报文通常是固定格式(如XML),且包含大量字段,如商户号、终端号、交易类型、后台通知地址、风控信息等。

  • 建造者应用:设计一个UnionPayRequest.Builder,不仅负责参数设置,还可以在build()方法中封装生成最终报文的复杂逻辑

    // 伪代码
    UnionPayRequest request = new UnionPayRequest.Builder("898310000000001", "order456", amount).withTerminalId("00000001").withNotifyUrl("https://api.my-shop.com/notify/unionpay").withRiskInfo(riskInfoObject) // 传入复杂的风控对象.build(); // build()方法内部负责将所有参数转换为XML格式并签名
    

实战三:Spring AI与大模型的复杂契约

  • 场景:与AI大模型交互时,请求参数极其复杂且多变。如果用构造函数,那将是史诗级的灾难。Spring AIOllamaApi.ChatRequest.Builder为我们展示了完美的应对之道。

    // Spring AI 调用伪代码
    OllamaChatRequest request = new OllamaChatRequest.Builder("llama3").withMessage(new Message("user", "你好")).withTemperature(0.8f).withFormat("json").build();
    

其设计精髓在于,将一个复杂的AI请求分解为模型、消息、参数等多个可独立配置的部分,通过链式调用清晰地构建出一个完整的、经过校验的请求契约。

探究OkHttp与Spring的实现

不可变HTTP请求的教科书——OkHttp的Request.Builder

public class Request {final HttpUrl url;final String method;final Headers headers;final RequestBody body;private Request(Builder builder) { /* ... */ }public static class Builder {HttpUrl url;String method;Headers.Builder headers;RequestBody body;public Builder() {this.method = "GET";this.headers = new Headers.Builder();}public Builder url(String url) { /* ... */ return this; }public Builder header(String name, String value) { /* ... */ return this; }public Builder post(RequestBody body) { return method("POST", body); }public Request build() {if (url == null) throw new IllegalStateException("url == null");return new Request(this);}}
}

设计巧思

  • 组合建造者Request.Builder内部还组合了Headers.Builder,将复杂性进一步分解。
  • 默认值与便捷方法:提供了method的默认值GET,以及.post()等便捷方法,提升了API的易用性。
  • 最终校验:在build()方法中对必填项进行最终校验。

安全优雅的URL构建——Spring的UriComponentsBuilder

public class UriComponentsBuilder implements UriBuilder, Cloneable {private String scheme;private String host;private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();public UriComponentsBuilder scheme(String scheme) { this.scheme = scheme; return this; }public UriComponentsBuilder host(String host) { this.host = host; return this; }public UriComponentsBuilder queryParam(String name, Object... values) { /* ... */ return this; }public UriComponents build() {// 在这里执行所有组件的组装和编码逻辑return new UriComponents(scheme, ..., queryParams, ...);}
}

设计巧思

  • 关注点分离:将一个URL拆分为scheme, host, queryParams等多个独立部分。
  • 自动编码:在build()方法内部负责处理所有参数的URL编码,将开发者从繁琐且易错的工作中解放出来。
  • 可变与不可变分离UriComponentsBuilder自身是可变的,但它最终build()出的UriComponents对象是不可变的。

用构建过程的确定性,对抗对象状态的不确定性

  1. 告别“上帝构造函数”:当一个类的构造函数参数超过4个,特别是含有多个可选参数时,就应该立刻启动重构,引入建造者模式。
  2. 链式调用是最佳实践:采用静态内部类实现的链式建造者,是目前最主流、可读性最强的实现方式。
  3. 建造者赋能不可变性:将目标对象的构造函数设为private,所有字段设为final,仅通过Builderbuild()方法创建实例。这是构建线程安全对象的关键一步。
  4. 应对复杂契约的利器:当需要构建的对象的参数列表复杂、易变时(如API请求、AI模型参数、电商订单),建造者模式是保证代码可维护性的不二之选。

好的代码会说话,而建造者模式,就是对象创建时最雄辩的演说家。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/93068.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/93068.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

jenkins在windows配置sshpass

我的服务器里jenkins是通过docker安装的&#xff0c;jenkins与项目都部署在同一台服务器上还好&#xff0c;但是当需要通过jenkins构建&#xff0c;再通过scp远程推送到别的服务器上&#xff0c;就出问题了&#xff0c;毕竟不是手动执行scp命令&#xff0c;可以手动输入密码&am…

Linux操作系统从入门到实战(十八)在Linux里面怎么查看进程

Linux操作系统从入门到实战&#xff08;十八&#xff09;在Linux里面怎么查看进程前言一、如何识别一个进程&#xff1f;—— PID二、怎么查看进程的信息&#xff1f;方式1&#xff1a;通过/proc目录方式2&#xff1a;用ps命令三、父进程是什么&#xff1f;—— PPID四、bash是…

[TryHackMe](知识学习)---基于堆栈得到缓冲区溢出

1.了解缓冲区溢出WINDOWS程序动态调试工具immunity debuggerhttps://www.immunityinc.com/products/debugger/2.Mona脚本#!/usr/bin/env python3import socket, time, sysip "10.201.99.37"port 1337 timeout 5 prefix "OVERFLOW1 "string prefix &q…

LRU算法与LFU算法

知识点&#xff1a; LRU是Least Recently Used的缩写&#xff0c;意思是最近最少使用&#xff0c;它是一种Cache替换算法 Cache的容量有限&#xff0c;因此当Cache的容量用完后&#xff0c;而又有新的内容需要添加进来时&#xff0c; 就需要挑选 并舍弃原有的部分内容&#xf…

目标检测公开数据集全解析:从经典到前沿

目标检测公开数据集全解析&#xff1a;从经典到前沿 一、引言 目标检测&#xff08;Object Detection&#xff09;是计算机视觉领域的核心任务之一&#xff0c;旨在在图像或视频中识别并定位感兴趣的物体。与图像分类不同&#xff0c;目标检测不仅需要判断物体的类别&#xf…

数据备份与进程管理

一、数据备份1.Linux服务器中需要备份的数据&#xff08;1&#xff09;Linux系统重要数据&#xff1a;/root/目录&#xff0c;/home/目录&#xff0c;/etc/目录&#xff08;2&#xff09;安装服务的数据&#xff1a;Apache&#xff08;配置文件&#xff0c;网页主目录&#xff…

docker volume卷入门教程

1. 基础概念 Docker卷是专门用于持久化容器数据的存储方案&#xff0c;独立于容器生命周期。其核心优势包括&#xff1a; 数据持久化&#xff1a;容器删除后数据仍保留跨容器共享&#xff1a;多个容器可访问同一卷备份与迁移&#xff1a;支持直接复制卷数据驱动支持&#xff1a…

计算机网络——协议

1. 计算机网络分层1.1 OSI 7层模型应用层表示层会话层传输层网络层数据链路层物理层1.2 TCP/IP 4 层模型应用层运输层网际层网络接口层1.3 5层体系机构应用层传输层网络层数据链路层物理层2. 应用层协议2.1 HTTP协议2.1.1 基本介绍HTTP&#xff08;HyperText Transfer Protocol…

【React】hooks 中的闭包陷阱

在 React Hooks 中的 闭包陷阱&#xff08;Closure Trap&#xff09;在 useEffect、事件回调、定时器等场景里很常见。1. 闭包陷阱是什么 当你在函数组件里定义一个回调&#xff08;比如事件处理函数&#xff09;&#xff0c;这个回调会捕获当时渲染时的变量值。如果后面状态更…

校园快递小程序(腾讯地图API、二维码识别、Echarts图形化分析)

&#x1f388;系统亮点&#xff1a;腾讯地图API、二维码识别、Echarts图形化分析&#xff1b;一.系统开发工具与环境搭建1.系统设计开发工具后端使用Java编程语言的Spring boot框架 项目架构&#xff1a;B/S架构 运行环境&#xff1a;win10/win11、jdk17小程序&#xff1a; 技术…

Python网络爬虫(二) - 解析静态网页

文章目录一、网页解析技术介绍二、Beautiful Soup库1. Beautiful Soup库介绍2. Beautiful Soup库几种解析器比较3. 安装Beautiful Soup库3.1 安装 Beautiful Soup 43.2 安装解析器4. Beautiful Soup使用步骤4.1 创建Beautiful Soup对象4.2 获取标签4.2.1 通过标签名获取4.2.2 通…

【Linux基础知识系列】第九十四篇 - 如何使用traceroute命令追踪路由

在网络环境中&#xff0c;了解数据包从源主机到目标主机的路径是非常重要的。这不仅可以帮助我们分析网络连接问题&#xff0c;还可以用于诊断网络延迟、丢包等问题。traceroute命令是一个强大的工具&#xff0c;它能够追踪数据包在网络中的路径&#xff0c;显示每一跳的延迟和…

达梦数据闪回查询-快速恢复表

Time:2025/08/12Author:skatexg一、环境说明DM数据库&#xff1a;DM8.0及以上版本二、适用场景研发在误操作或变更数据后&#xff0c;想马上恢复表到某个时间点&#xff0c;可以通过闪回查询功能快速实现&#xff08;通过全量备份恢复时间长&#xff0c;成本高&#xff09;三、…

力扣(LeetCode) ——225 用队列实现栈(C语言)

题目&#xff1a;用队列实现栈示例1&#xff1a; 输入&#xff1a; [“MyStack”, “push”, “push”, “top”, “pop”, “empty”] [[], [1], [2], [], [], []] 输出&#xff1a; [null, null, null, 2, 2, false] 解释&#xff1a; MyStack myStack new MyStack(); mySta…

微软推出AI恶意软件检测智能体 Project Ire

开篇 在8月5号&#xff0c;微软研究院发布了一篇博客文章&#xff0c;在该篇博客中推出了一款名为Project Ire的AI Agent。该Agent可以在无需人类协助的情况下&#xff0c;自主分析和分类二进制文件。它可以在无需了解二进制文件来源或用途的情况下&#xff0c;对文件进行完全的…

哪些对会交由SpringBoot容器管理?

在 Spring Boot 中,交由容器管理的对象通常称为“Spring Bean”,这些对象的创建、依赖注入、生命周期等由 Spring 容器统一管控。以下是常见的会被 Spring Boot 容器管理的对象类型及识别方式: 一、通过注解声明的组件(最常见) Spring Boot 通过类级别的注解自动扫描并注…

Android POS应用在android运行常见问题及解决方案

概述 本文档记录了在Android POS应用开发过程中遇到的两个关键问题及其解决方案&#xff1a; UnsatisfiedLinkError: couldnt find "libnative.so" 错误ActivityNotFoundException 错误商户信息一致性检查绕过 问题1&#xff1a;UnsatisfiedLinkError - libnative.so…

基于SpringBoot的旅游网站系统

1. 项目简介 旅游线路管理系统是一个基于Spring Boot的在线旅游服务平台&#xff0c;提供旅游线路展示、分类、预订、订单管理等功能。系统包含前台用户界面和后台管理模块&#xff0c;支持用户注册登录、线路浏览、收藏、下单支付、客服咨询等核心功能。管理员可管理线路信息、…

CVPR 2025 | 机器人操控 | RoboGround:用“掩码”中介表示,让机器人跨场景泛化更聪明

点击关注gongzhonghao【计算机sci论文精选】1.导读1.1论文基本信息论文标题&#xff1a;ROBOGROUND: Robotic Manipulation with Grounded Vision-Language Priors作者&#xff1a;Haifeng Huang, Xinyi Chen, Hao Li&#xff0c; Xiaoshen Han, Yilun Chen, Tai Wang, Zehan W…

构建Node.js单可执行应用(SEA)的方法

如果为了降低部署复杂度&#xff0c;可以考虑使用vercel/ncc。除非有特别理由&#xff0c;不建议使用SEA。1. 环境准备1.1. 基础要求Node.js: > 19.0.0 (推荐最新LTS版本)1.2. 安装依赖npm install postject typescript1.3. 验证环境node -v # 确认版本 > 19 ts…