Skip to content

组合的力量:用 Go 构建灵活的软件结构

与依赖继承的传统面向对象语言不同,Go 语言将"组合优于继承"作为其核心设计哲学之一。这一思想,结合小而美的接口,使得结构型设计模式在 Go 中的实现显得尤为自然和强大。我们不是在"套用"模式,而是在"践行"语言的最佳实践。

1. 接口:Go 结构模式的基石

在 Go 中,结构型模式的一切都围绕着接口(Interface)。接口定义了"需要什么行为",而不是"你是谁"。这种鸭子类型(Duck Typing)的特性是实现解耦和灵活组合的关键。

"Don't design with interfaces, discover them." - Rob Pike

我们不应该预先设计庞大的接口,而应该在使用方发现需要某种行为时,才定义出最小化的接口。

2. 适配器模式 (Adapter Pattern): 隐式与显式的艺术

适配器模式的目的是让两个不兼容的接口能够协同工作。在 Go 中,这通常以一种非常隐蔽和自然的方式发生。

2.1. 隐式适配器

假设我们有一个 DataProcessor 接口,它需要一个 Process 方法。而我们有一个第三方库提供的 LegacySystem,它有一个功能类似但签名不同的 ProcessData 方法。

go
type DataProcessor interface {
    Process() string
}

// 第三方库的类型,我们不能修改它
type LegacySystem struct {
    Data string
}

func (l *LegacySystem) ProcessData() string {
    return fmt.Sprintf("Legacy processing: %s", l.Data)
}

// 适配器
type LegacySystemAdapter struct {
    Legacy *LegacySystem
}

// Process 方法使 LegacySystemAdapter 满足了 DataProcessor 接口
func (a *LegacySystemAdapter) Process() string {
    return a.Legacy.ProcessData()
}

// --- 使用 ---
legacy := &LegacySystem{Data: "important data"}
adapter := &LegacySystemAdapter{Legacy: legacy}

// 现在我们可以将适配过的对象传递给需要 DataProcessor 的函数
func Execute(p DataProcessor) {
    fmt.Println(p.Process())
}

Execute(adapter) // 输出: Legacy processing: important data

这里的 LegacySystemAdapter 就是一个显式的适配器。它包裹了 LegacySystem,并提供了一个符合 DataProcessor 接口的 Process 方法。

然而,如果一个类型恰好实现了接口所需的所有方法,它就自动满足了该接口,无需任何显式声明。这就是 Go 接口的强大之处,很多时候适配是"免费"的。

3. 装饰器模式 (Decorator Pattern): 用嵌入来增强功能

装饰器模式允许在不修改对象自身的情况下,动态地为其添加新的行为。在 Go 中,实现装饰器的最地道方式是结构体嵌入(Struct Embedding)

一个经典的例子是装饰 http.Handler

go
// 基础的 Handler
type GreeterHandler struct{}

func (h *GreeterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello, Gopher!")
}

// 日志装饰器
type LoggingMiddleware struct {
    Next http.Handler // 嵌入 http.Handler 接口
}

// LoggingMiddleware "装饰"了 ServeHTTP 方法
func (m *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    log.Printf("Started request to %s", r.URL.Path)

    m.Next.ServeHTTP(w, r) // 调用被装饰的 Handler 的原始方法

    log.Printf("Finished request in %s", time.Since(start))
}

// --- 使用 ---
greeter := &GreeterHandler{}
loggedGreeter := &LoggingMiddleware{Next: greeter}

// http.Handle("/", loggedGreeter)

LoggingMiddleware 嵌入了 http.Handler 接口,这意味着它本身也满足了这个接口。然后,它实现了自己的 ServeHTTP 方法,在其中添加了日志记录的逻辑,并调用了内部 Next handler 的 ServeHTTP 方法。

这种方式可以像套娃一样,一层一层地将功能"装饰"上去,每一层都只关心自己的职责,完美体现了单一职责原则。

4. 代理模式 (Proxy Pattern): 通过组合控制访问

代理模式为另一个对象提供一个替身或占位符,以控制对这个对象的访问。这在 Go 中同样通过组合来实现。

假设我们有一个昂贵的数据库查询操作,我们希望为其添加缓存功能。

go
// "主题"接口
type ReportGenerator interface {
    GenerateReport(id string) (string, error)
}

// "真实主题":执行实际、昂贵的操作
type DatabaseReportGenerator struct{}

func (g *DatabaseReportGenerator) GenerateReport(id string) (string, error) {
    log.Printf("Querying database for report %s...", id)
    time.Sleep(2 * time.Second) // 模拟昂贵的操作
    return fmt.Sprintf("Report data for ID %s", id), nil
}

// "代理":控制对真实主题的访问
type CachingReportGeneratorProxy struct {
    RealGenerator ReportGenerator
    Cache         map[string]string
}

func (p *CachingReportGeneratorProxy) GenerateReport(id string) (string, error) {
    // 先检查缓存
    if report, found := p.Cache[id]; found {
        log.Printf("Returning report %s from cache.", id)
        return report, nil
    }

    // 如果缓存未命中,则调用真实对象的方法
    report, err := p.RealGenerator.GenerateReport(id)
    if err != nil {
        return "", err
    }

    // 将结果存入缓存
    log.Printf("Caching report %s.", id)
    p.Cache[id] = report

    return report, nil
}

// --- 使用 ---
realGen := &DatabaseReportGenerator{}
proxy := &CachingReportGeneratorProxy{
    RealGenerator: realGen,
    Cache:         make(map[string]string),
}

// 第一次调用,会查询数据库
proxy.GenerateReport("user123") 
// 第二次调用,会直接从缓存返回
proxy.GenerateReport("user123")

这里的 CachingReportGeneratorProxy 持有一个 ReportGenerator 接口的实例。它首先检查缓存,只有在缓存未命中时,才将调用委托给"真实"的 DatabaseReportGenerator。这便是代理模式的精髓:在不改变原始对象的情况下,通过一个中间层来增加额外的控制逻辑。

结论

在 Go 的世界里,适配器、装饰器和代理模式并非需要刻意记忆的僵硬模板。它们是遵循"组合优于继承"和面向接口编程这两个核心原则时,自然而然浮现出的优雅结构。通过灵活运用接口和结构体嵌入,我们可以构建出松耦合、高内聚、易于扩展和维护的系统。