构建者设计模式
构建器设计模式是一种创建性设计模式,可让您逐步构造复杂对象,将构造逻辑与最终表示分开。
它在以下情况下特别有用:
一个对象需要许多可选字段,并且并非每次都需要所有字段。
您希望避免伸缩构造函数或具有多个参数的大型构造函数。
对象构造过程涉及 需要按特定顺序发生的多个步骤。
在构建此类对象时,开发人员通常依赖于具有许多参数的构造函数或为每个字段公开 setter。例如,类 可能具有 、 、 、 和 等字段,导致构造函数重载或对象状态不一致。Usernameemailphoneaddresspreferences
但随着字段数量的增加,这种方法变得难以管理、容易出错,并且违反了单一责任原则——将构造逻辑与业务逻辑混合在一起。
构建器模式通过引入一个单独的构建器类来处理对象创建过程来解决这个问题。客户端使用此构建器逐步构建对象,同时保持最终对象不可变、一致且易于创建。
让我们通过一个真实世界的示例来了解如何应用构建器模式来使复杂的对象创建更干净、更安全、更易于维护。
问题:构建复杂 对象HttpRequest
想象一下,您正在构建一个需要配置和创建 HTTP 请求的系统。每个 字段都可以包含必填字段和可选字段的组合,具体取决于用例。HttpRequest
以下是典型的 HTTP 请求可能包括的内容:
URL(必填)
HTTP 方法(例如 GET、POST、PUT – 默认为 GET)
标头(可选,多个键值对)
查询参数(可选,多个键值对)
请求正文(可选,通常用于 POST/PUT)
超时(可选,默认为 30 秒)
乍一看,这似乎是可以控制的。但随着可选字段数量的增加,对象构造的复杂性也随之增加。
朴素的方法:伸缩式构造函数
一种常见的方法是使用构造函数重载(通常称为伸缩构造函数反模式),其中定义多个参数数量不断增加的构造函数:
import java.util.HashMap;
import java.util.Map;
public class HttpRequestTelescoping {
private String url; // Required
private String method; // Optional, default GET
private Map<String, String> headers; // Optional
private Map<String, String> queryParams; // Optional
private String body; // Optional
private int timeout; // Optional, default 30s
public HttpRequestTelescoping(String url) {
this(url, "GET");
}
public HttpRequestTelescoping(String url, String method) {
this(url, method, null);
}
public HttpRequestTelescoping(String url, String method, Map<String, String> headers) {
this(url, method, headers, null);
}
public HttpRequestTelescoping(String url, String method, Map<String, String> headers, Map<String, String> queryParams) {
this(url, method, headers, queryParams, null);
}
public HttpRequestTelescoping(String url, String method, Map<String, String> headers, Map<String, String> queryParams, String body) {
this(url, method, headers, queryParams, body, 30000);
}
public HttpRequestTelescoping(String url, String method, Map<String, String> headers,
Map<String, String> queryParams, String body, int timeout) {
this.url = url;
this.method = method;
this.headers = headers == null ? new HashMap<>() : headers;
this.queryParams = queryParams == null ? new HashMap<>() : queryParams;
this.body = body;
this.timeout = timeout;
System.out.println("HttpRequest Created: URL=" + url + ", Method=" + method +
", Headers=" + this.headers.size() + ", Params=" + this.queryParams.size() +
", Body=" + (body != null) + ", Timeout=" + timeout);
}
// ... getters ...
}
客户端代码示例
public class HttpAppTelescoping {
public static void main(String[] args) {
HttpRequestTelescoping req1 = new HttpRequestTelescoping("https://api.example.com/data"); // GET, defaults
HttpRequestTelescoping req2 = new HttpRequestTelescoping("https://api.example.com/submit", "POST", null, null, "{\"key\":\"value\"}"); // POST with body
HttpRequestTelescoping req3 = new HttpRequestTelescoping("https://api.example.com/config", "PUT", Map.of("X-API-Key", "secret"), null, "config_data", 5000);
}
}
这种方法有什么问题?
虽然它在功能上有效,但随着 对象变得更加复杂,这种设计很快就会变得笨拙且容易出错。
- 1. 难以读写
相同类型的多个参数(例如,,)很容易意外交换参数。StringMap
代码很难一目了然地理解——尤其是当大多数参数是 .null
- 2. 容易出错
客户端必须传递他们不想设置的可选参数,这增加了错误的风险。null
构造函数内部的防御性编程对于避免 s 变得必要。NullPointerException
- 3. 不灵活且脆弱
如果要设置参数 5 而不是 3 和 4,则必须传递 3 和 4。null
您必须遵循确切的参数顺序,这会损害可读性和可用性。
- 4. 可扩展性差
添加新的可选参数需要添加或更改构造函数,这可能会破坏现有代码或强制对客户端进行不必要的更新。
测试和文档变得越来越难以维护。
我们需要什么
我们需要一种更灵活、可读和可维护的方式来构造 对象——尤其是当涉及许多可选值并且需要不同的组合时。HttpRequest
这正是构建器设计模式的用武之地。
构建器模式
Builder 模式将复杂对象的构造与其表示形式分开。
在构建器模式中:
构造逻辑封装在 Builder 中。
最终对象(“产品”)是通过调用方法创建 的。build()
对象本身通常具有私有或包私有构造函数,强制通过构建器进行构造。
这导致了可读、流畅的代码,易于扩展和修改,而不会破坏现有客户端。
类图
构建器(例如HttpRequestBuilder)
定义配置或设置产品的方法。
通常 从每个方法返回以支持流畅的接口。this
通常作为产品类中的静态嵌套类实现。
ConcreteBuilder(例如StandardHttpRequestBuilder)
实现 接口或直接定义流畅的方法。Builder
维护正在构建的产品的每个部分的状态。
实现返回最终产品实例的方法。build()
产品(例如HttpRequest)
正在构造的最终对象。
可以不可变,只能通过构建器构建。
具有接收构建器内部状态的私有构造函数。
导演(可选)(例如HttpRequestDirector)
使用构建器编排构建过程。
当您想要封装标准配置或可重用的构造序列时很有用。
在现代用法中(尤其是在具有流畅构建器的 Java 中),Director 经常被省略,客户端通过链接方法来承担这个角色。
实现构建器
- 1. 创建产品类 (HttpRequest)
我们首先创建 类—— 我们想要构建的产品。它有多个字段(一些是必需的,一些是可选的),它的构造函数将是私有的,强制客户端通过构建器构造它。HttpRequest
构建器类将被定义为 中的静态嵌套类,构造函数将接受该构建器的实例来初始化字段。HttpRequest
import java.util.HashMap;
import java.util.Map;
import java.util.Collections;
public class HttpRequest {
private final String url; // Required
private final String method; // Optional, default GET
private final Map<String, String> headers; // Optional
private final Map<String, String> queryParams; // Optional
private final String body; // Optional
private final int timeout; // Optional, default 30s
// Private constructor, only accessible by the Builder
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers)); // Defensive copy
this.queryParams = Collections.unmodifiableMap(new HashMap<>(builder.queryParams)); // Defensive copy
this.body = builder.body;
this.timeout = builder.timeout;
}
// Getters (no setters to ensure immutability)
public String getUrl() { return url; }
public String getMethod() { return method; }
public Map<String, String> getHeaders() { return headers; }
public Map<String, String> getQueryParams() { return queryParams; }
public String getBody() { return body; }
public int getTimeout() { return timeout; }
@Override
public String toString() {
return "HttpRequest{" +
"url='" + url + '\'' +
", method='" + method + '\'' +
", headers=" + headers +
", queryParams=" + queryParams +
", body='" + (body != null ? body.substring(0, Math.min(10, body.length()))+"..." : "null") + '\'' +
", timeout=" + timeout +
'}';
}
// --- Static Nested Builder Class ---
public static class Builder {
// Required parameter
private final String url;
// Optional parameters - initialized to default values
private String method = "GET";
private Map<String, String> headers = new HashMap<>();
private Map<String, String> queryParams = new HashMap<>();
private String body = null;
private int timeout = 30000; // 30 seconds default
// Builder constructor for required fields
public Builder(String url) {
if (url == null || url.trim().isEmpty()) {
throw new IllegalArgumentException("URL cannot be null or empty.");
}
this.url = url;
}
// Setter-like methods for optional fields, returning the Builder for fluency
public Builder method(String method) {
this.method = (method == null || method.trim().isEmpty()) ? "GET" : method.toUpperCase();
return this;
}
public Builder header(String key, String value) {
if (key != null && value != null) {
this.headers.put(key, value);
}
return this;
}
public Builder queryParam(String key, String value) {
if (key != null && value != null) {
this.queryParams.put(key, value);
}
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeout(int timeoutMillis) {
if (timeoutMillis > 0) {
this.timeout = timeoutMillis;
}
return this;
}
// The final build method that creates the HttpRequest object
public HttpRequest build() {
// Optionally, add validation logic here before creating the object
// For example, ensure body is present for POST/PUT if required by your design
if (("POST".equals(method) || "PUT".equals(method)) && (body == null || body.isEmpty())) {
System.out.println("Warning: Building " + method + " request without a body for URL: " + url);
}
return new HttpRequest(this);
}
}
}
- 2. 添加静态嵌套 类Builder
此构建器类允许客户端通过流畅的界面设置请求的每个部分。
import java.util.HashMap;
import java.util.Map;
import java.util.Collections;
public class HttpRequest {
private final String url; // Required
private final String method; // Optional, default GET
private final Map<String, String> headers; // Optional
private final Map<String, String> queryParams; // Optional
private final String body; // Optional
private final int timeout; // Optional, default 30s
// Private constructor, only accessible by the Builder
private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = Collections.unmodifiableMap(new HashMap<>(builder.headers)); // Defensive copy
this.queryParams = Collections.unmodifiableMap(new HashMap<>(builder.queryParams)); // Defensive copy
this.body = builder.body;
this.timeout = builder.timeout;
}
// Getters (no setters to ensure immutability)
public String getUrl() { return url; }
public String getMethod() { return method; }
public Map<String, String> getHeaders() { return headers; }
public Map<String, String> getQueryParams() { return queryParams; }
public String getBody() { return body; }
public int getTimeout() { return timeout; }
@Override
public String toString() {
return "HttpRequest{" +
"url='" + url + '\'' +
", method='" + method + '\'' +
", headers=" + headers +
", queryParams=" + queryParams +
", body='" + (body != null ? body.substring(0, Math.min(10, body.length()))+"..." : "null") + '\'' +
", timeout=" + timeout +
'}';
}
// --- Static Nested Builder Class ---
public static class Builder {
// Required parameter
private final String url;
// Optional parameters - initialized to default values
private String method = "GET";
private Map<String, String> headers = new HashMap<>();
private Map<String, String> queryParams = new HashMap<>();
private String body = null;
private int timeout = 30000; // 30 seconds default
// Builder constructor for required fields
public Builder(String url) {
if (url == null || url.trim().isEmpty()) {
throw new IllegalArgumentException("URL cannot be null or empty.");
}
this.url = url;
}
// Setter-like methods for optional fields, returning the Builder for fluency
public Builder method(String method) {
this.method = (method == null || method.trim().isEmpty()) ? "GET" : method.toUpperCase();
return this;
}
public Builder header(String key, String value) {
if (key != null && value != null) {
this.headers.put(key, value);
}
return this;
}
public Builder queryParam(String key, String value) {
if (key != null && value != null) {
this.queryParams.put(key, value);
}
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeout(int timeoutMillis) {
if (timeoutMillis > 0) {
this.timeout = timeoutMillis;
}
return this;
}
// The final build method that creates the HttpRequest object
public HttpRequest build() {
// Optionally, add validation logic here before creating the object
// For example, ensure body is present for POST/PUT if required by your design
if (("POST".equals(method) || "PUT".equals(method)) && (body == null || body.isEmpty())) {
System.out.println("Warning: Building " + method + " request without a body for URL: " + url);
}
return new HttpRequest(this);
}
}
}
- 3. 使用客户端代码中的构建器
让我们看看使用构建器构建一个是多么容易和可读:HttpRequest
public class HttpAppBuilder {
public static void main(String[] args) {
// Example 1: Simple GET request
HttpRequest getRequest = new HttpRequest.Builder("https://api.example.com/users")
.method("GET")
.header("Accept", "application/json")
.timeout(5000)
.build();
System.out.println("GET Request: " + getRequest);
// Example 2: POST request with body and custom headers
HttpRequest postRequest = new HttpRequest.Builder("https://api.example.com/posts")
.method("POST")
.header("Content-Type", "application/json")
.header("X-Auth-Token", "some_secret_token")
.body("{\"title\":\"New Post\",\"content\":\"Hello Builder!\"}")
.queryParam("userId", "123")
.build();
System.out.println("POST Request: " + postRequest);
// Example 3: Request with only required URL (defaults for others)
HttpRequest defaultRequest = new HttpRequest.Builder("https://api.example.com/status").build();
System.out.println("Default Request: " + defaultRequest);
// Example 4: Illustrating potential warning from builder
HttpRequest putNoBodyRequest = new HttpRequest.Builder("https://api.example.com/resource/1")
.method("PUT")
// .body("updated data") // Body intentionally omitted
.build();
System.out.println("PUT Request (no body): " + putNoBodyRequest);
// Example of trying to build with invalid required parameter
try {
HttpRequest invalidRequest = new HttpRequest.Builder(null).build();
} catch (IllegalArgumentException e) {
System.err.println("Error creating request: " + e.getMessage());
}
}
}
我们取得了什么成就
不需要长构造函数或 参数。null
可选值名称清晰且易于设置。
最终对象是不可变的,并且完全初始化。
可读且流畅的客户端代码。
易于扩展 — 想要添加新的可选字段?只需向构建器添加一个新方法即可。
https://pan.baidu.com/s/1c1oQItiA7nZxz8Rnl3STpw?pwd=yftc
https://pan.quark.cn/s/dec9e4868381