Используйте Go для реализации 23 шаблонов проектирования GoF (2)

Go
Используйте Go для реализации 23 шаблонов проектирования GoF (2)

предисловие

предыдущий пост«23 шаблона проектирования с использованием Go для реализации GoF (1)»Представляет 23 шаблона проектирования.творческий режим(Creational Pattern), порождающий шаблон — это тип шаблона проектирования, связанный с созданием объектов.Скрыть особенности создания объекта от пользователя объектаТаким образом, цель развязки. Эта статья в основном посвященаСтруктурный образец(Структурный паттерн), основная идеяСобирайте несколько объектов в более крупные структуры, сохраняя гибкость и эффективность структуры., чтобы решить проблему связи между модулями из структуры программы.

Составной узор

组合模式结构

Кратко

В объектно-ориентированном программировании есть два общеми метода дизайна объектов,комбинацияинаследовать, оба могут решить проблему повторного использования кода, но использование последнего чревато побочными эффектами слишком глубокого наследования и слишком сложных отношений между объектами, что приводит к ухудшению сопровождения кода. Таким образом, классический принцип объектно-ориентированного проектирования таков:Композиция над наследованием.

Мы все знаем, что семантика, представленная комбинацией, - это «имеет-а», то есть взаимосвязь между частью и целым. Самый классический комбинированный режим описывается следующим образом:

Объекты сгруппированы в древовидную структуру для представления иерархии «часть-целое», что позволяет пользователям последовательно использовать отдельные объекты и комбинированные объекты.

Язык Go естественным образом поддерживает режим композиции, а поскольку он не поддерживает отношения наследования, Go также преследуетКомпозиция над наследованиемпринципа и призываем всех использовать комбинированный метод в программировании. Есть два способа реализовать шаблон композиции в Go:прямая комбинация(Прямая композиция) иВстроить комбинацию(встраивание композиции), давайте вместе обсудим эти два различных метода реализации.

Перейти к реализации

Реализация Direct Composition похожа на Java/C++, то есть один объект используется как свойство-член другого объекта.

Типичная реализация, такая как«23 шаблона проектирования с использованием Go для реализации GoF (1)»Например,Messageструктура, поHeaderиBodyсоставлен. ТакMessageпредставляет собой целое, иHeaderиBodyявляется частью сообщения.

type Message struct {
	Header *Header
	Body   *Body
}

Теперь давайте рассмотрим немного более сложный пример, также учитывая систему обработки сообщений в стиле плагинов, описанную в предыдущей статье. Ранее мы использовалиАбстрактный заводской узорРешена проблема с загрузкой плагина.Обычно у каждого плагина есть жизненный цикл.Общими являются стартовое состояние и стоповое состояние.Теперь используемКомбинированный режимдля решения проблемы с запуском и остановкой плагина.

сначала дайPluginИнтерфейс добавляет несколько методов, связанных с жизненным циклом:

package plugin
...
// 插件运行状态
type Status uint8

const (
	Stopped Status = iota
	Started
)

type Plugin interface {
  // 启动插件
	Start()
  // 停止插件
	Stop()
  // 返回插件当前的运行状态
	Status() Status
}
// Input、Filter、Output三类插件接口的定义跟上一篇文章类似
// 这里使用Message结构体替代了原来的string,使得语义更清晰
type Input interface {
	Plugin
	Receive() *msg.Message
}

type Filter interface {
	Plugin
	Process(msg *msg.Message) *msg.Message
}

type Output interface {
	Plugin
	Send(msg *msg.Message)
}

Для подключаемой системы обработки сообщений все является подключаемой программой, поэтому мыPipeineТакже разработан как плагин, который реализуетPluginинтерфейс:

package pipeline
...
// 一个Pipeline由input、filter、output三个Plugin组成
type Pipeline struct {
	status plugin.Status
	input  plugin.Input
	filter plugin.Filter
	output plugin.Output
}

func (p *Pipeline) Exec() {
	msg := p.input.Receive()
	msg = p.filter.Process(msg)
	p.output.Send(msg)
}
// 启动的顺序 output -> filter -> input
func (p *Pipeline) Start() {
	p.output.Start()
	p.filter.Start()
	p.input.Start()
	p.status = plugin.Started
	fmt.Println("Hello input plugin started.")
}
// 停止的顺序 input -> filter -> output
func (p *Pipeline) Stop() {
	p.input.Stop()
	p.filter.Stop()
	p.output.Stop()
	p.status = plugin.Stopped
	fmt.Println("Hello input plugin stopped.")
}

func (p *Pipeline) Status() plugin.Status {
	return p.status
}

ОдинPipelineЗависит отInput,Filter,OutputТри типа подключаемых модулей образуют отношение «часть-целое», и все они реализуютPluginинтерфейс, который является реализацией типичного шаблона композиции. Клиент не должен запускаться и останавливаться явноInput,FilterиOutputплагин, вызовPipelineобъектStartиStopметод,PipelineЭто уже помогло вам выполнить запуск и остановку соответствующих плагинов по порядку.

По сравнению с предыдущей статьей реализация в этой статьеInput,Filter,OutputПри наличии трех типов подключаемых модулей необходимо реализовать еще три метода жизненного цикла. как в предыдущей статьеHelloInput,UpperFilterиConsoleOutputНапример, конкретная реализация выглядит следующим образом:

package plugin
...
type HelloInput struct {
	status Status
}

func (h *HelloInput) Receive() *msg.Message {
  // 如果插件未启动,则返回nil
	if h.status != Started {
		fmt.Println("Hello input plugin is not running, input nothing.")
		return nil
	}
	return msg.Builder().
		WithHeaderItem("content", "text").
		WithBodyItem("Hello World").
		Build()
}

func (h *HelloInput) Start() {
	h.status = Started
	fmt.Println("Hello input plugin started.")
}

func (h *HelloInput) Stop() {
	h.status = Stopped
	fmt.Println("Hello input plugin stopped.")
}

func (h *HelloInput) Status() Status {
	return h.status
}
package plugin
...
type UpperFilter struct {
	status Status
}

func (u *UpperFilter) Process(msg *msg.Message) *msg.Message {
	if u.status != Started {
		fmt.Println("Upper filter plugin is not running, filter nothing.")
		return msg
	}
	for i, val := range msg.Body.Items {
		msg.Body.Items[i] = strings.ToUpper(val)
	}
	return msg
}

func (u *UpperFilter) Start() {
	u.status = Started
	fmt.Println("Upper filter plugin started.")
}

func (u *UpperFilter) Stop() {
	u.status = Stopped
	fmt.Println("Upper filter plugin stopped.")
}

func (u *UpperFilter) Status() Status {
	return u.status
}

package plugin
...
type ConsoleOutput struct {
	status Status
}

func (c *ConsoleOutput) Send(msg *msg.Message) {
	if c.status != Started {
		fmt.Println("Console output is not running, output nothing.")
		return
	}
	fmt.Printf("Output:\n\tHeader:%+v, Body:%+v\n", msg.Header.Items, msg.Body.Items)
}

func (c *ConsoleOutput) Start() {
	c.status = Started
	fmt.Println("Console output plugin started.")
}

func (c *ConsoleOutput) Stop() {
	c.status = Stopped
	fmt.Println("Console output plugin stopped.")
}

func (c *ConsoleOutput) Status() Status {
	return c.status
}

Код теста выглядит следующим образом:

package test
...
func TestPipeline(t *testing.T) {
	p := pipeline.Of(pipeline.DefaultConfig())
	p.Start()
	p.Exec()
	p.Stop()
}
// 运行结果
=== RUN   TestPipeline
Console output plugin started.
Upper filter plugin started.
Hello input plugin started.
Pipeline started.
Output:
    Header:map[content:text], Body:[HELLO WORLD]
Hello input plugin stopped.
Upper filter plugin stopped.
Console output plugin stopped.
Hello input plugin stopped.
--- PASS: TestPipeline (0.00s)
PASS

Другая реализация шаблона композиции, Embedding Composition, на самом деле использует функцию анонимного члена языка Go, которая по существу аналогична прямой композиции.

По-прежнему беря в качестве примера структуру Message, если используется встроенная композиция, она выглядит так:

type Message struct {
	Header
	Body
}
// 使用时,Message可以引用Header和Body的成员属性,例如:
msg := &Message{}
msg.SrcAddr = "192.168.0.1"

Шаблон адаптера

适配器模式结构

Кратко

Шаблон адаптера является одним из наиболее часто используемых структурных шаблонов и позволяет двум объектам работать вместе, которые в противном случае не работали бы вместе из-за несогласованного интерфейса. В реальной жизни режимы адаптера можно увидеть повсюду, например преобразователи вилок питания, которые могут заставить вилки британского стиля работать с розетками китайского типа. Что делает шаблон адаптера?интерфейсAdaptee, через переходникAdapterПреобразование в другой интерфейс, ожидаемый КлиентомTargetИспользовать, принцип реализации тоже очень прост, т.е.Adapterпутем реализацииTargetинтерфейс и вызвать его в соответствующем методеAdapteeреализация интерфейса.

Типичный сценарий применения:Старый интерфейс в системе устарел и будет удален.Однако из-за исторической нагрузки старый интерфейс не может быть немедленно заменен новым интерфейсом.В настоящее время можно добавить адаптер для адаптации старого интерфейса к новому интерфейсу. для использования.. Шаблон адаптера является хорошей практикой принципов объектно-ориентированного проектирования.принцип открыто-закрыто(открытый/закрытый принцип), при добавлении интерфейса не нужно модифицировать старый интерфейс, нужно только добавить дополнительный адаптационный слой.

Перейти к реализации

Продолжая рассматривать пример системы обработки сообщений из предыдущего раздела, до сих пор входные данные для системы были получены изHelloInput, теперь предположим, что в систему необходимо добавить функцию получения данных из очереди сообщений Kafka, где интерфейс потребителя Kafka выглядит следующим образом:

package kafka
...
type Records struct {
	Items []string
}

type Consumer interface {
	Poll() Records
}

Из-за текущегоPipelineЭто разработаноplugin.InputИнтерфейс для приема данных, таким образом,kafka.Consumerи не могут быть напрямую интегрированы в систему.

что делать? Используйте режим адаптера!

чтобы позволитьPipelineможет быть использованkafka.Consumerинтерфейс, нам нужно определить адаптер следующим образом:

package plugin
...
type KafkaInput struct {
	status Status
	consumer kafka.Consumer
}

func (k *KafkaInput) Receive() *msg.Message {
	records := k.consumer.Poll()
	if k.status != Started {
		fmt.Println("Kafka input plugin is not running, input nothing.")
		return nil
	}
	return msg.Builder().
		WithHeaderItem("content", "kafka").
		WithBodyItems(records.Items).
		Build()
}

// 在输入插件映射关系中加入kafka,用于通过反射创建input对象
func init() {
	inputNames["hello"] = reflect.TypeOf(HelloInput{})
	inputNames["kafka"] = reflect.TypeOf(KafkaInput{})
}
...

Поскольку в языке Go нет конструктора, если следовать инструкциям из предыдущей статьиАбстрактный заводской узорсоздаватьKafkaInput, то полученный экземплярconsumerЧлен будет, потому что он не инициализированnil. Поэтому необходимо датьPluginдобавить новый интерфейсInitМетод, который используется для определения некоторых операций инициализации плагина и называется до того, как завод возвращает экземпляр.

package plugin
...
type Plugin interface {
	Start()
	Stop()
	Status() Status
	// 新增初始化方法,在插件工厂返回实例前调用
	Init()
}

// 修改后的插件工厂实现如下
func (i *InputFactory) Create(conf Config) Plugin {
	t, _ := inputNames[conf.Name]
	p := reflect.New(t).Interface().(Plugin)
  // 返回插件实例前调用Init函数,完成相关初始化方法
	p.Init()
	return p
}

// KakkaInput的Init函数实现
func (k *KafkaInput) Init() {
	k.consumer = &kafka.MockConsumer{}
}

в приведенном выше кодеkafka.MockConsumerДля реализации нашего шаблона Kafka Consumer код выглядит следующим образом:

package kafka
...
type MockConsumer struct {}

func (m *MockConsumer) Poll() *Records {
	records := &Records{}
	records.Items = append(records.Items, "i am mock consumer.")
	return records
}

Код теста выглядит следующим образом:

package test
...
func TestKafkaInputPipeline(t *testing.T) {
	config := pipeline.Config{
		Name: "pipeline2",
		Input: plugin.Config{
			PluginType: plugin.InputType,
			Name:       "kafka",
		},
		Filter: plugin.Config{
			PluginType: plugin.FilterType,
			Name:       "upper",
		},
		Output: plugin.Config{
			PluginType: plugin.OutputType,
			Name:       "console",
		},
	}
	p := pipeline.Of(config)
	p.Start()
	p.Exec()
	p.Stop()
}
// 运行结果
=== RUN   TestKafkaInputPipeline
Console output plugin started.
Upper filter plugin started.
Kafka input plugin started.
Pipeline started.
Output:
	Header:map[content:kafka], Body:[I AM MOCK CONSUMER.]
Kafka input plugin stopped.
Upper filter plugin stopped.
Console output plugin stopped.
Pipeline stopped.
--- PASS: TestKafkaInputPipeline (0.00s)
PASS

Шаблон моста

桥接模式结构

Кратко

Мостовой режим в основном используется дляРазделите абстрактную часть и часть реализации, чтобы они могли изменяться в независимых направлениях..它解决了在模块有多种变化方向的情况下,用继承所导致的类爆炸问题。举一个例子,一个产品有形状和颜色两个特征(变化方向),其中形状分为方形和圆形,颜色分为红色和蓝色。如果采用继承的设计方案,那么就需要新增4个产品子类:方形红色、圆形红色、方形蓝色、圆形红色。如果形状总共有m种变化,颜色有n种变化,那么就需要新增m*n个产品子类!现在我们使用桥接模式进行优化,将形状和颜色分别设计为一个抽象接口独立出来,这样需要新增2个形状子类:方形和圆形,以及2个颜色子类:红色和蓝色。同样,如果形状总共有m种变化,颜色有n种变化,总共只需要新增m+n个子类!

桥接模式示例

В приведенном выше примере, абстрагируя форму и цвет как интерфейс, продукт больше не зависит от конкретных деталей формы и цвета, чтобы достичь цели разделения.Режим моста — это, по сути, интерфейсно-ориентированное программирование, которое может обеспечить хорошую гибкость и масштабируемость системы.. Если для объекта существует несколько направлений изменений, и каждое направление изменений необходимо расширить, то для проектирования более целесообразно использовать шаблон моста.

Перейти к реализации

Возвращаясь к примеру системы обработки сообщений,PipelineОбъект в основном состоит изInput,Filter,OutputТри типа плагинов (3 функции), поскольку это подключаемая система, поддержка различныхInput,Filter,Output, и могут быть гибко объединены (Есть несколько направлений изменений). Очевидно,PipelineЭто очень удобно для проектирования в режиме моста, и мы на самом деле тоже так делаем. мы будемInput,Filter,OutputОни разработаны как абстрактный интерфейс и расширяются в соответствующих направлениях.PipelineОн зависит только от этих трех абстрактных интерфейсов и не воспринимает детали конкретной реализации.

使用桥接模式设计的Pipeline

package plugin
...
type Input interface {
	Plugin
	Receive() *msg.Message
}

type Filter interface {
	Plugin
	Process(msg *msg.Message) *msg.Message
}

type Output interface {
	Plugin
	Send(msg *msg.Message)
}
package pipeline
...
// 一个Pipeline由input、filter、output三个Plugin组成
type Pipeline struct {
	status plugin.Status
	input  plugin.Input
	filter plugin.Filter
	output plugin.Output
}
// 通过抽象接口来使用,看不到底层的实现细节
func (p *Pipeline) Exec() {
	msg := p.input.Receive()
	msg = p.filter.Process(msg)
	p.output.Send(msg)
}

Код теста выглядит следующим образом:

package test
...
func TestPipeline(t *testing.T) {
	p := pipeline.Of(pipeline.DefaultConfig())
	p.Start()
	p.Exec()
	p.Stop()
}
// 运行结果
=== RUN   TestPipeline
Console output plugin started.
Upper filter plugin started.
Hello input plugin started.
Pipeline started.
Output:
	Header:map[content:text], Body:[HELLO WORLD]
Hello input plugin stopped.
Upper filter plugin stopped.
Console output plugin stopped.
Pipeline stopped.
--- PASS: TestPipeline (0.00s)
PASS

Суммировать

В этой статье в основном представлены режим композиции, режим адаптера и режим моста в структурном режиме.Комбинированный режимЭто в основном решает проблему повторного использования кода.По сравнению с отношениями наследования, комбинированный режим может избежать проблемы сложного кода, вызванного слишком глубоким наследованием.Поэтому область объектно-ориентированного дизайна популярна.Композиция над наследованиемПринцип и дизайн языка Go также являются хорошей практикой этого принципа;режим адаптераОн можно рассматривать как мост между двумя несовместимыми интерфейсами, которые могут преобразовать один интерфейс в другой интерфейс, ожидаемый клиентом, который решает проблему, которую модули не могут работать вместе из-за несовместимых интерфейсов;режим мостаАбстрактная часть и часть реализации модуля разделены, чтобы они могли расширяться в своем собственном направлении, чтобы достичь цели разделения.

В следующей статье мы продолжим фокусироваться на структурных паттернах и представим оставшиеся 4 паттерна узлов: декоративные паттерны, паттерны фасадов, паттерны легковеса и паттерны прокси.