Menu
CoddyTech

装饰器模式

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 93 课,共 107 课。

装饰器模式允许你通过用其他对象包装对象,动态地为对象添加新行为。适配器会更改接口以匹配另一个接口,而装饰器保留相同的接口,但通过在原始对象外层逐层包装来增强功能。

在 Go 中,我们通过让基础类型和装饰器都实现同一个接口来实现这一点:

type Notifier interface {
    Send(message string) string
}

type BasicNotifier struct{}

func (b BasicNotifier) Send(message string) string {
    return "Sending: " + message
}

装饰器会包装另一个 Notifier,并在委托给它之前或之后添加行为:

type TimestampDecorator struct {
    wrapped Notifier
}

func (t TimestampDecorator) Send(message string) string {
    timestamped := "[2024-01-15] " + message
    return t.wrapped.Send(timestamped)
}

type UppercaseDecorator struct {
    wrapped Notifier
}

func (u UppercaseDecorator) Send(message string) string {
    return strings.ToUpper(u.wrapped.Send(message))
}

装饰器的强大之处在于可以叠加多个包装器来组合不同的行为:

notifier := BasicNotifier{}
withTimestamp := TimestampDecorator{wrapped: notifier}
withBoth := UppercaseDecorator{wrapped: withTimestamp}

fmt.Println(notifier.Send("Hello"))
// Sending: Hello

fmt.Println(withBoth.Send("Hello"))
// SENDING: [2024-01-15] HELLO

当你需要在不修改对象代码的情况下为对象添加职责时,装饰器模式非常合适,尤其是在需要组合不同功能时。它常用于日志记录、缓存、身份验证和压缩层。

challenge icon

挑战

简单

让我们使用装饰器模式构建一个咖啡店点单系统!你将创建一种基础咖啡饮品,以及可以添加牛奶、糖和鲜奶油等配料的装饰器。每个装饰器都会包装前一个饮品,逐步完善描述和价格,这是装饰器层层叠加功能的绝佳现实示例。

你需要将代码组织在三个文件中:

  • beverage.go:定义核心接口和基础咖啡类型。

    创建一个包含两个方法的 Beverage 接口:

    • Description() string:返回饮品的名称
    • Cost() float64:返回价格

    将 Coffee 结构体实现为基础饮品。它的描述应为 Coffee,价格应为 2.00。

  • decorators.go:创建可以为任意饮品添加配料的装饰器类型。

    每个装饰器都会包装一个 Beverage,并实现相同的接口。创建三个装饰器:

    • MilkDecorator:在描述中添加 , Milk,在价格上增加 0.50
    • SugarDecorator:在描述中添加 , Sugar,在价格上增加 0.25
    • WhipDecorator:在描述中添加 , Whip,在价格上增加 0.75

    每个装饰器都应调用被包装饮品的方法,并增强返回结果。

  • main.go:通过叠加装饰器来制作定制饮品。

    读取要添加的配料数量。然后逐个读取配料类型(milk、sugar 或 whip),并使用相应的装饰器包装当前饮品。

    添加完所有配料后,分两行输出最终饮品的描述和价格。价格格式为 $X.XX,并且必须恰好保留两位小数。

输入格式如下:

  • 第 1 行:要添加的配料数量
  • 后续各行:每行一种配料类型(milk、sugar 或 whip)

例如,给定:

2
milk
sugar

输出应为:

Coffee, Milk, Sugar
$2.75

再例如,给定:

3
whip
milk
milk

输出应为:

Coffee, Whip, Milk, Milk
$3.75

再例如,给定:

0

输出应为:

Coffee
$2.00

注意,每个装饰器都会包装前一个饮品,逐步累加描述和价格。你可以多次添加同一种配料(例如双份牛奶),装饰器的顺序也会决定描述中的顺序。基础咖啡不需要了解任何配料,每个装饰器只需增强它所包装的内容!

亲手试试

package main

import (
	"fmt"
)

func main() {
	// 读取配料的数量
	var count int
	fmt.Scanln(&count)

	// 先创建一杯基础咖啡
	var drink Beverage = &Coffee{}

	// 读取每种配料,并用对应的装饰器包装饮品
	for i := 0; i < count; i++ {
		var extra string
		fmt.Scanln(&extra)

		// TODO:根据配料类型,用对应的装饰器包装 drink
		// 为 "milk" 使用 MilkDecorator
		// 为 "sugar" 使用 SugarDecorator
		// 为 "whip" 使用 WhipDecorator
	}

	// TODO:输出最终的描述和价格
	// 将价格格式化为 $X.XX,保留两位小数
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器