装饰设计模式(Decorator degin pattern)

装饰模式非常简单,但使用频率非常高。在实际使用中,非常适合和旧的代码一起工作
装饰模式,又译为装饰器、装饰者模式

参考书籍

go design patterns for real world project

描述

装饰模式可以让你不修改旧的代码而给他们添加更多的功能。它是若何工作的?装饰模式的工作就像俄罗斯套娃。你有一个小的娃娃,把它套在一个大娃娃里面,然后再把他们套在一个更大的娃娃里面,依次类推。
Decorator type实现与其所装饰的类型的相同的接口(都是娃娃),Decorator type会将其所装饰的类型存储在其成员中(大娃娃里面保存有小娃娃)

目标

扩展功能:当你想要扩展旧有的代码,而不想冒险破坏原来的方法,你可以首先考虑装饰者模式,针对这类问题它真的非常有效。
装饰器非常强大的另一个领域可能没有那么明显,尽管他在基于用户输入、偏好或类似输入创建具有大量功能的类型时会显露出来。就像一把瑞士军刀,你有一个基本类型(刀的框架),从哪里你可以展开他的功能。
准确的说,我们在什么情况下使用装饰者模式呢?

  • 当你需要添加一功能,而不想触碰旧代码的时候。因为你想避免潜在的修改影响(open/close原则)
  • 对象的功能在创建的时候是变化的,而且功能的数目也是未知的。

例子

在我们的示例中,我们准备了一种Pizza类型,其中核心是pizza,配料是修饰类型。我们的披萨有洋葱和肉两种配料

接受的标准

装饰器模式接受标准是要有一个接口和一个核心类型,即所有层都要建立的类型
🐷 我们必须要有一个所有装饰器都会实现的主接口,其名字为IngredientAdd,他将会有一个AddIngredient() stirng 方法
🐷我们须有要有一个核心的PizzaDecorator typeI(修饰器),我们将向其添加配料
🐷 我们必须要有一个配料onion(洋葱)实现相同的IngredientAdd接口,它会将opion字符添加在返回的pizza中
🐷 我们必须要有一个配料meat(肉)实现相同的IngredientAdd接口,它会将meat字符添加在返回的pizza中
🐷 当我们在顶部对象上调用AddIngredient 方法的时候,必须返回一个装饰完整的Pizza文本:Pizza with the following ingredients:meat,opion(翻译:pizz中含有如下成分:肉,洋葱)

单元测试

为了启动单元测试,我们必须首先更具验收标准创建所描述的基本结构。首先,所有装饰类必须实现的接口描述如下

type IngredientAdd interface {
	AddIngredient() (string, error)
}

下面的代码定义了PizzaDecorator类型,其中必须包含IngredientAdd,并且实现了IngredientAdd接口

type PizzaDecorator struct {
	Ingredient IngredientAdd
}

func (p *PizzaDecorator) AddIngredient() (string, error) {
	return "", errors.New("not implemented yet")
}

Meat 结构的定义将于PizzaDecorator结构非常类似

type Meat struct {
	Ingredient IngredientAdd
}

func (m *Meat) AddIngredient() (string, error) {
	return "", errors.New("not implemented yet")
}

现在我们用类似的方式定义洋葱结构

type Onion struct {
	Ingredient IngredientAdd
}

func (o *Onion) AddIngredient() (string, error) {
	return "", errors.New("not implemented yet")
}

这足以实现第一个单元测试,并允许编译器在没有任何编译错误的情况下运行它们

func TestPizzaDecorator_AddIngredient(t *testing.T) {
	pizza := PizzaDecorator{}
	pizzaResult, err := pizza.AddIngredient()
	if err != nil {
		t.Log(err)
	}
	expectedText := "Pizza with the following ingredients:"
	if !strings.Contains(pizzaResult, expectedText) {
		t.Errorf("调用pizza装饰器添加配料的时候,返回的文本是 %s "+
			"而不是我们期待的文本 '%s'", pizzaResult, expectedText)
	}
}
$ go test -v -run=TestPizzaDecorator_AddIngredient
=== RUN   TestPizzaDecorator_AddIngredient
    decorator_design_pattern_test.go:59: not implemented yet
    decorator_design_pattern_test.go:63: 调用pizza装饰器添加配料的时候,返回的文本是  而不是我们期待的文本 'Pizza with the following ingredients:'
--- FAIL: TestPizzaDecorator_AddIngredient (0.00s)
FAIL
exit status 1
FAIL

我们的第一个测试已经完成,我们可以看到PizzaDecorator结构体没有返回任何内容,这就是为什么测试失败的原因。我们现在可以转到洋葱类型,它的测试与Pizza装饰器的测试非常类似,但是我们必须确保我们将配料添加到了IigredientAdd方法,而不是空指针:

func TestOnion_AddIngredient(t *testing.T) {
//代码的前半部分检查当IngredientAdd方法没有传递给Onion结构体初始化的时候返回错误,也就是没有pizza可以添加配料的时候必须返回错误
	onion := &Onion{&PizzaDecorator{}}
	onionResult, err := onion.AddIngredient()
	if err == nil {//注意这里是==不是!=
		t.Errorf("当我们调用Onion装饰器的AddIngredient时,如果没有Ingredient字段,那么必须返回错误"+
			"而不是字符串:'%s'", onionResult)
	}

//最后检查,返回的字符串中是否包含了opion字符,确保披萨中加入了洋葱
	onion = &Onion{&PizzaDecorator{}}
	onionResult, err = onion.AddIngredient()
	if err != nil {
		t.Errorf(err.Error())
	}
	if !strings.Contains(onionResult, "onion") {
		t.Errorf("当我们调用Onion装饰的AddIngredient时"+
			"返回的文本中必须包含单词'onion',而不是 '%s'", onionResult)
	}
}

```bash
% go test -v -run=TestOnion_AddIngredient
=== RUN   TestOnion_AddIngredient
    decorator_design_pattern_test.go:76: not implemented yet
    decorator_design_pattern_test.go:79: 当我们调用Onion装饰的AddIngredient时返回的文本中必须包含单词'onion',而不是 ''
--- FAIL: TestOnion_AddIngredient (0.00s)
FAIL
exit status 1
FAIL 

添加肉的配料完全一样,但我们把类型改成meat而不是onion

func TestMeat_AddIngredient(t *testing.T) {
	meat := &Meat{}
	meatResult, err := meat.AddIngredient()
	if err == nil {
		t.Errorf("当我们调用Meat装饰器的AddIngredient是,如果没有Ingredient字段,那么必须返回错误"+
			"而不是字符串:'%s'", meatResult)
	}

	meat = &Meat{&PizzaDecorator{}}
	meatResult, err = meat.AddIngredient()
	if err != nil {
		t.Errorf(err.Error())
	}
	if !strings.Contains(meatResult, "meat") {
		t.Errorf("当我们调用Meat装饰的AddIngredient时"+
			"返回的文本中必须包含单词'meat',而不是 '%s'", meatResult)
	}
}

测试结果也是类似的

$ test -v -run=TestMeat_AddIngredient
=== RUN   TestMeat_AddIngredient
    decorator_design_pattern_test.go:95: not implemented yet
    decorator_design_pattern_test.go:98: 当我们调用Meat装饰的AddIngredient时返回的文本中必须包含单词'meat',而不是 ''
--- FAIL: TestMeat_AddIngredient (0.00s)
FAIL
exit status 1
FAIL

最后,我们检查所有的配料合在一起的测试,用洋葱和肉制作披萨。返回结果中必须要有以下成分的披萨文本:Pizza with the following ingredients: meat, onion

$ go test -v -run=TestPizzaDecorator_FullStack
=== RUN   TestPizzaDecorator_FullStack
    decorator_design_pattern_test.go:107: not implemented yet
    decorator_design_pattern_test.go:112: 当一份披萨中包含洋葱和肉的时候返回的字符串内容必须是'Pizza with the following ingredients: meat, onion',而不是''
    decorator_design_pattern_test.go:115: 
--- FAIL: TestPizzaDecorator_FullStack (0.00s)
FAIL
exit status 1
FAIL 

从上面输出中,我们可以看到我们的装饰器类型返回空的字符串。这是合理的,因为我们还没有实现它。这是最后一个测试了,检查我们的所有的装饰器是否都实现了。接着让我们仔细看看它们的实现。

实现

type PizzaDecorator struct {
	Ingredient IngredientAdd
}

func (p *PizzaDecorator) AddIngredient() (string, error) {
	return "Pizza with the following ingredients:", nil
}

AddIngredient返回的单行更改足以通过测试

$ go test -v -run=TestPizzaDecorator_AddIngredient
=== RUN   TestPizzaDecorator_AddIngredient
--- PASS: TestPizzaDecorator_AddIngredient (0.00s)
PASS
ok 

转到Onion结构实现,它的返回结果是以AddIngredient()返回结果开头,然后在尾部添加单词onion。以获得添加了洋葱原料的pizza

type Onion struct {
	Ingredient IngredientAdd
}

func (o *Onion) AddIngredient() (string, error) {
	if o.Ingredient == nil { //注意这里是==
		return "", errors.New("Onion的IngredientAdd方法需要一个Ingredient字段")
	}
	s, err := o.Ingredient.AddIngredient()
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%s %s,", s, "onion"), nil
}

首先检测我们确实有一个指向IngredientAdd的指针。我们使用内部的IngredientAdd接口,并检查它是否返回了错误。如果错误没有发生,我们将收到一个新的字符传,由返回加过,空格,单词onon和逗号组成。看起来很好,可以运行测试


func TestOnion_AddIngredient(t *testing.T) {
	onion := &Onion{}
	onionResult, err := onion.AddIngredient()

	if err == nil {
		t.Errorf("当我们调用Onion装饰器的AddIngredient时,如果没有Ingredient字段,那么必须返回错误"+
			"而不是字符串:'%s'", onionResult)
	}

	onion = &Onion{&PizzaDecorator{}}
	onionResult, err = onion.AddIngredient()
	if err != nil {
		t.Errorf(err.Error())
	}
	if !strings.Contains(onionResult, "onion") {
		t.Errorf("当我们调用Onion装饰的AddIngredient时"+
			"返回的文本中必须包含单词'onion',而不是 '%s'", onionResult)
	}
	t.Log(onionResult)
$ go test -v -run=TestOnion_AddIngredient
=== RUN   TestOnion_AddIngredient
    decorator_design_pattern_test.go:85: Pizza with the following ingredients: onion,
--- PASS: TestOnion_AddIngredient (0.00s)
PASS
ok  

实现Meat struct非常类似

type Meat struct {
	Ingredient IngredientAdd
}

func (m *Meat) AddIngredient() (string, error) {
	if m.Ingredient == nil {//注意这里是==
		return "", errors.New("Meat的IngredientAdd方法需要一个Ingredient字段")
	}
	s, err := m.Ingredient.AddIngredient()
	if err != nil {
		return "", err
	}
	return s, nil
}

测试结果如下

func TestMeat_AddIngredient(t *testing.T) {
	meat := &Meat{}
	meatResult, err := meat.AddIngredient()
	if err == nil {
		t.Errorf("当我们调用Meat装饰器的AddIngredient是,如果没有Ingredient字段,那么必须返回错误"+
			"而不是字符串:'%s'", meatResult)
	}

	meat = &Meat{&PizzaDecorator{}}
	meatResult, err = meat.AddIngredient()
	if err != nil {
		t.Errorf(err.Error())
	}
	if !strings.Contains(meatResult, "meat") {
		t.Errorf("当我们调用Meat装饰的AddIngredient时"+
			"返回的文本中必须包含单词'meat',而不是 '%s'", meatResult)
	}
	t.Log(meatResult)
}
$ go test -v -run=TestMeat_AddIngredient
=== RUN   TestMeat_AddIngredient
    decorator_design_pattern_test.go:104: Pizza with the following ingredients: meat,
--- PASS: TestMeat_AddIngredient (0.00s)
PASS
ok 

前面的所有测试都是分开的,如果他们能够成功,那么我们的full stacked测试也能顺利通过

func TestPizzaDecorator_FullStack(t *testing.T) {
	pizza := &Onion{&Meat{&PizzaDecorator{}}}
	pizzaResult, err := pizza.AddIngredient()
	if err != nil {
		t.Errorf(err.Error())
	}

	expectedText := "Pizza with the following ingredients: meat, onion"
	if !strings.Contains(pizzaResult, expectedText) {
		t.Errorf("当一份披萨中包含洋葱和肉的时候"+
			"返回的字符串内容必须是'%s',而不是'%s'", expectedText, pizzaResult)
	}
	t.Log(pizzaResult)
}

$ go test -v -run=TestPizzaDecorator_FullStack
=== RUN   TestPizzaDecorator_FullStack
    decorator_design_pattern_test.go:119: Pizza with the following ingredients: meat, onion,
--- PASS: TestPizzaDecorator_FullStack (0.00s)
PASS
ok  

令人惊叹的Decorator模式,我们可以继续堆叠IngredientAdds,他们调用内部指针来为PizzaDecorator添加功能,我们没有触碰核心类型,也没有修改或实现任何新的东西,所有的新的特性都由额外的类型实现。

Pizzer装饰的核心代码附在末尾

//filename:decorator_design_pattern
package main_test

import (
	"errors"
	"fmt"
	"strings"
	"testing"
)

type IngredientAdd interface {
	AddIngredient() (string, error)
}

type PizzaDecorator struct {
	Ingredient IngredientAdd
}

func (p *PizzaDecorator) AddIngredient() (string, error) {
	return "Pizza with the following ingredients:", nil
}

type Meat struct {
	Ingredient IngredientAdd
}

func (m *Meat) AddIngredient() (string, error) {
	if m.Ingredient == nil { //注意这里是==
		return "", errors.New("Meat的IngredientAdd方法需要一个Ingredient字段")
	}
	s, err := m.Ingredient.AddIngredient()
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%s %s,", s, "meat"), nil
}

type Onion struct {
	Ingredient IngredientAdd
}

func (o *Onion) AddIngredient() (string, error) {
	if o.Ingredient == nil { //注意这里是==
		return "", errors.New("Onion的IngredientAdd方法需要一个Ingredient字段")
	}
	s, err := o.Ingredient.AddIngredient()
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%s %s,", s, "onion"), nil
}

func TestPizzaDecorator_AddIngredient(t *testing.T) {
	pizza := PizzaDecorator{}
	pizzaResult, err := pizza.AddIngredient()
	if err != nil {
		t.Log(err)
	}
	expectedText := "Pizza with the following ingredients:"
	if !strings.Contains(pizzaResult, expectedText) {
		t.Errorf("调用pizza装饰器添加配料的时候,返回的文本是 %s "+
			"而不是我们期待的文本 '%s'", pizzaResult, expectedText)
	}
}

func TestOnion_AddIngredient(t *testing.T) {
	onion := &Onion{}
	onionResult, err := onion.AddIngredient()

	if err == nil {
		t.Errorf("当我们调用Onion装饰器的AddIngredient时,如果没有Ingredient字段,那么必须返回错误"+
			"而不是字符串:'%s'", onionResult)
	}

	onion = &Onion{&PizzaDecorator{}}
	onionResult, err = onion.AddIngredient()
	if err != nil {
		t.Errorf(err.Error())
	}
	if !strings.Contains(onionResult, "onion") {
		t.Errorf("当我们调用Onion装饰的AddIngredient时"+
			"返回的文本中必须包含单词'onion',而不是 '%s'", onionResult)
	}
	t.Log(onionResult)
}

func TestMeat_AddIngredient(t *testing.T) {
	meat := &Meat{}
	meatResult, err := meat.AddIngredient()
	if err == nil {
		t.Errorf("当我们调用Meat装饰器的AddIngredient是,如果没有Ingredient字段,那么必须返回错误"+
			"而不是字符串:'%s'", meatResult)
	}

	meat = &Meat{&PizzaDecorator{}}
	meatResult, err = meat.AddIngredient()
	if err != nil {
		t.Errorf(err.Error())
	}
	if !strings.Contains(meatResult, "meat") {
		t.Errorf("当我们调用Meat装饰的AddIngredient时"+
			"返回的文本中必须包含单词'meat',而不是 '%s'", meatResult)
	}
	t.Log(meatResult)
}

func TestPizzaDecorator_FullStack(t *testing.T) {
	pizza := &Onion{&Meat{&PizzaDecorator{}}}
	pizzaResult, err := pizza.AddIngredient()
	if err != nil {
		t.Errorf(err.Error())
	}

	expectedText := "Pizza with the following ingredients: meat, onion"
	if !strings.Contains(pizzaResult, expectedText) {
		t.Errorf("当一份披萨中包含洋葱和肉的时候"+
			"返回的字符串内容必须是'%s',而不是'%s'", expectedText, pizzaResult)
	}
	t.Log(pizzaResult)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值