AIGC标识 建造者模式与原型模式 — 创建型设计模式续篇

建造者模式与原型模式 — 创建型设计模式续篇


一、建造者模式(Builder Pattern)

问题场景

假设你在开发一个 HTTP 请求配置对象,它有十几个可选参数:URL(必填)、Method、Headers、Timeout、Proxy、RetryCount、Body、ContentType、Auth、CachePolicy……如果用传统的结构体初始化,要么写一个参数巨多的构造函数,要么创建一个全是零值的结构体再逐个赋值——前者难读难记,后者容易漏设关键参数。

核心思想

建造者模式把"一步步搭建复杂对象"这件事单独拎出来,交给一个专门的 Builder 来做。Builder 提供一组链式方法,每个方法设置一个属性然后返回 Builder 本身,最后调用 Build() 拿到成品对象。

这个过程就像点外卖选套餐:先选主菜,再选饮品,再选甜点,最后确认下单——你不需要一次性在脑子里记住所有选项,一步一步来就行。

Go 实现:HTTP 请求配置

package main

import "fmt"

// RequestConfig 是我们要构建的复杂对象
type RequestConfig struct {
    URL         string
    Method      string
    Headers     map[string]string
    Timeout     int    // 秒
    RetryCount  int
    ContentType string
    AuthToken   string
}

// RequestBuilder 负责逐步构建 RequestConfig
type RequestBuilder struct {
    config RequestConfig
}

// NewRequestBuilder 创建 Builder,URL 是必填参数
func NewRequestBuilder(url string) *RequestBuilder {
    return &RequestBuilder{
        config: RequestConfig{
            URL:     url,
            Method:  "GET", // 默认值
            Headers: map[string]string{},
            Timeout: 30,    // 默认 30 秒
        },
    }
}

// Method 设置请求方法,返回 Builder 便于链式调用
func (b *RequestBuilder) Method(method string) *RequestBuilder {
    b.config.Method = method
    return b
}

// Timeout 设置超时时间
func (b *RequestBuilder) Timeout(seconds int) *RequestBuilder {
    b.config.Timeout = seconds
    return b
}

// Retry 设置重试次数
func (b *RequestBuilder) Retry(count int) *RequestBuilder {
    b.config.RetryCount = count
    return b
}

// ContentType 设置内容类型
func (b *RequestBuilder) ContentType(ct string) *RequestBuilder {
    b.config.ContentType = ct
    return b
}

// Auth 设置认证令牌
func (b *RequestBuilder) Auth(token string) *RequestBuilder {
    b.config.AuthToken = token
    return b
}

// Header 添加一个请求头
func (b *RequestBuilder) Header(key, value string) *RequestBuilder {
    b.config.Headers[key] = value
    return b
}

// Build 返回最终的 RequestConfig
func (b *RequestBuilder) Build() *RequestConfig {
    return &b.config
}

func main() {
    // 链式调用,清晰易读
    config := NewRequestBuilder("https://api.example.com/users").
        Method("POST").
        ContentType("application/json").
        Auth("Bearer abc123").
        Header("X-Request-Id", "req-001").
        Timeout(60).
        Retry(3).
        Build()

    fmt.Printf("请求配置:\n")
    fmt.Printf("  URL:         %s\n", config.URL)
    fmt.Printf("  Method:      %s\n", config.Method)
    fmt.Printf("  ContentType: %s\n", config.ContentType)
    fmt.Printf("  Auth:        %s\n", config.AuthToken)
    fmt.Printf("  Timeout:     %d 秒\n", config.Timeout)
    fmt.Printf("  Retry:       %d 次\n", config.RetryCount)
    fmt.Printf("  Headers:     %v\n", config.Headers)
}

运行输出(预期):

请求配置:
  URL:         https://api.example.com/users
  Method:      POST
  ContentType: application/json
  Auth:        Bearer abc123
  Timeout:     60 秒
  Retry:       3 次
  Headers:     map[X-Request-Id:req-001]

带指挥者(Director)的变体

有些场景中,构建步骤的顺序是固定的。比如组装一台电脑总是"CPU → RAM → GPU → 存储"这个流程,这时候可以引入一个 Director 来编排构建步骤:

// Director 封装固定的构建流程
type ComputerDirector struct{}

func (d *ComputerDirector) BuildHighEnd(b *ComputerBuilder) *Computer {
    return b.SetCPU("Intel i9").
        SetRAM("64GB DDR5").
        SetGPU("RTX 4090").
        SetStorage("2TB NVMe SSD").
        Build()
}

func (d *ComputerDirector) BuildBudget(b *ComputerBuilder) *Computer {
    return b.SetCPU("Intel i3").
        SetRAM("8GB DDR4").
        SetGPU("Integrated").
        SetStorage("256GB SSD").
        Build()
}

Director 的好处是:预设方案可以复用,调用方只需说"我要高端配置"或"我要经济配置",不需要逐个参数指定。

什么时候用建造者模式

适用 不适用
对象有很多可选参数(>4 个) 对象只有 2-3 个属性,直接用结构体就行
参数之间有约束关系(比如设了 Auth 就必须设 ContentType) 参数都是必填的,用普通构造函数更简单
需要多种预设配置方案 不需要链式调用的场景

Go 建造者模式 vs Java 建造者模式

Go 没有类的构造函数重载,也没有 private Builder 内部类。Go 的建造者通常就是:

  • 一个结构体(Builder)持有产品字段
  • 方法返回 *Builder 实现链式调用
  • Build() 返回最终产品

这比 Java 的写法简洁得多——不需要写 inner static class,不需要 Lombok 注解,几个方法就搞定了。


二、原型模式(Prototype Pattern)

问题场景

你需要创建一批结构相同的配置对象,比如 10 个服务器节点配置,它们的主机名、IP 前缀、端口都差不多,只是编号不同。如果一个一个手写,既繁琐又容易出错。

核心思想

原型模式就是先造一个模板对象(原型),然后通过克隆它来批量创建新对象,再在克隆体上微调差异部分

就像复印文件:你不会重新打字写一份一模一样的合同,而是复印一份然后在复印件上修改细节。

Go 实现:深拷贝与浅拷贝

Go 里克隆对象有两种方式:

  1. 浅拷贝:只复制值类型字段,引用类型(slice、map、指针)共享底层数据
  2. 深拷贝:所有字段都独立复制,新对象和老对象完全脱耦
package main

import "fmt"

// ServerConfig 服务器配置(原型)
type ServerConfig struct {
    Hostname string
    IP       string
    Port     int
    Tags     []string   // 引用类型,浅拷贝会共享
    Metadata map[string]string // 引用类型,浅拷贝会共享
}

// Clone 浅拷贝:值类型独立,引用类型共享
func (s *ServerConfig) Clone() *ServerConfig {
    copy := *s // Go 的 * 操作做浅拷贝
    return &copy
}

// DeepClone 深拷贝:所有字段完全独立
func (s *ServerConfig) DeepClone() *ServerConfig {
    newTags := make([]string, len(s.Tags))
    copy(newTags, s.Tags) // 复制切片

    newMeta := make(map[string]string)
    for k, v := range s.Metadata {
        newMeta[k] = v // 复制 map
    }

    return &ServerConfig{
        Hostname: s.Hostname,
        IP:       s.IP,
        Port:     s.Port,
        Tags:     newTags,
        Metadata: newMeta,
    }
}

func main() {
    // 创建原型
    prototype := &ServerConfig{
        Hostname: "node-template",
        IP:       "10.0.0.1",
        Port:     8080,
        Tags:     []string{"web", "backend"},
        Metadata: map[string]string{"env": "prod", "region": "us-west"},
    }

    // 浅拷贝克隆 — 修改 Tags 会影响原型!
    shallowClone := prototype.Clone()
    shallowClone.Hostname = "node-01"  // 值类型:不影响原型
    shallowClone.Tags[0] = "frontend"  // 引用类型:会影响原型!

    fmt.Println("=== 浅拷贝后 ===")
    fmt.Printf("原型 Tags:   %v\n", prototype.Tags)     // 被修改了!
    fmt.Printf("克隆 Tags:   %v\n", shallowClone.Tags)  // 和原型共享

    // 深拷贝克隆 — 完全独立,互不影响
    deepClone := prototype.DeepClone()
    deepClone.Hostname = "node-02"
    deepClone.Tags[0] = "database"  // 不影响原型

    fmt.Println("=== 深拷贝后 ===")
    fmt.Printf("原型 Tags:   %v\n", prototype.Tags)     // 不受影响
    fmt.Printf("深克隆 Tags: %v\n", deepClone.Tags)     // 完全独立
}

运行输出(预期):

=== 浅拷贝后 ===
原型 Tags:   [frontend backend]   ← 被修改了!
克隆 Tags:   [frontend backend]

=== 深拷贝后 ===
原型 Tags:   [frontend backend]   ← 不受影响
深克隆 Tags: [database backend]

教训:如果原型对象包含引用类型字段(slice、map、嵌套指针),必须用深拷贝。浅拷贝虽然快,但共享底层数据可能导致意外的副作用。

原型模式的批量使用

package main

import "fmt"

type ServerNode struct {
    Hostname string
    IP       string
    Port     int
    Role     string
}

// Clone 深拷贝(这里只有值类型,浅拷贝够用)
func (n *ServerNode) Clone() *ServerNode {
    copy := *n
    return &copy
}

func main() {
    // 创建原型模板
    template := &ServerNode{
        IP:   "10.0.1.",
        Port: 8080,
        Role: "worker",
    }

    // 用原型克隆 + 微调,批量创建 5 个节点
    nodes := make([]*ServerNode, 5)
    for i := 0; i < 5; i++ {
        node := template.Clone()
        node.Hostname = fmt.Sprintf("worker-%d", i+1)
        node.IP = fmt.Sprintf("%s%d", template.IP, 100+i)
        nodes[i] = node
    }

    for _, n := range nodes {
        fmt.Printf("%s @ %s:%d (%s)\n", n.Hostname, n.IP, n.Port, n.Role)
    }
}

运行输出(预期):

worker-1 @ 10.0.1.100:8080 (worker)
worker-2 @ 10.0.1.101:8080 (worker)
worker-3 @ 10.0.1.102:8080 (worker)
worker-4 @ 10.0.1.103:8080 (worker)
worker-5 @ 10.0.1.104:8080 (worker)

什么时候用原型模式

适用 不适用
需要批量创建结构相似的对象 对象间完全不同,没有共性
对象初始化代价大(如读数据库、网络请求) 对象初始化很快,直接新建即可
避免重复设置相同的默认值 需要保证每个对象完全独立(但用深拷贝就行)

Go 原型模式 vs 其他语言

Go 没有内置的 clone() 方法或 Cloneable 接口。你需要自己实现 Clone() 方法:

  • 如果只有值类型字段:copy := *original(浅拷贝即可)
  • 如果有引用类型字段:手动 make + copy(深拷贝)
  • 更复杂的情况:可以用 encoding/json 序列化再反序列化(通用深拷贝方案)
// JSON 深拷贝 — 万能方案,但性能较差
func DeepCloneJSON[T any](original T) T {
    data, _ := json.Marshal(original)
    var clone T
    json.Unmarshal(data, &clone)
    return clone
}

这种方案通用但慢,适合不频繁克隆的场景。高性能场景还是手写深拷贝。


三、创建型模式总结

至此,创建型设计模式的五种经典模式都学完了:

模式 核心思想 Go 实现特点
简单工厂 一个函数根据参数返回不同类型 返回 interface{} 或具体接口类型
工厂方法 每种产品有自己的工厂 用接口+多实现替代继承
抽象工厂 创建一组相关产品的家族 接口组合,返回多个产品
建造者 逐步构建复杂对象 链式方法 + Build()
原型 克隆已有对象 手写 Clone()/DeepClone() 或 JSON 序列化

Go 和 Java/C++ 的实现差异:

  • Go 没有继承,工厂和抽象工厂用接口组合代替
  • Go 没有构造函数重载,建造者模式更显价值
  • Go 没有内置 clone,原型模式需要自己实现深拷贝
posted @ 2026-08-03 09:21  FfHUCisI  阅读(3)  评论(0)    收藏  举报