[Golang] Практика шаблонов проектирования: Композитный

Go

Об этой серии

Эта серия статей впервые посвящена языковой практике го. На самом деле, я уже некоторое время использую язык Go в проекте.Опыт показывает, что неважно, официальные это документы, книги или сетевые материалы, вводных сведений об идиомах языка Go относительно немного. в исходниках стандартной библиотеки. , так что особенно хочется иметь какие-то подборки и сводки по этому поводу.

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

Конкретные темы этой серии будут более гибкими, и план в основном включает темы в следующих областях:

  1. Идиомы языка Go.
  2. Реализация шаблонов проектирования. Особенно изменения, внесенные введением языковых функций, таких как замыкания, сопрограммы и DuckType.
  3. Обсуждение идеи шаблонов проектирования. Будут какие-то ругательства.

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

У меня возражение против этого предложения.Давайте сначала продадим.Начнем с практических примеров.

Мы видели много примеров комбинированного режима, таких как файловая система (файл/папка), окно GUI (фрейм/элемент управления), меню (меню/элемент меню) и т. д. Здесь я также приведу пример меню, но это это не операция Меню в системе настоящее меню, KFC's...

Давайте думать о еде в KFC как菜单项, пакет есть菜单. Меню и пункты меню имеют некоторые общедоступные свойства: имя, описание, цена, возможность покупки и т. д., поэтому, как говорит GoF, нам нужно использовать их последовательно. Их иерархическая структура отражена в меню, содержащем несколько пунктов меню или в меню, цена которого является суммой всех подпунктов. Что ж, этот пример на самом деле не очень уместен, и он не может хорошо отразить ситуацию, когда меню содержит меню, поэтому я определил меню «экономного обеда», которое включает в себя несколько комплексных меню.

Подводя итог коду, наш окончательный код вызова выглядит следующим образом:

func main() {
	menu1 := NewMenu("培根鸡腿燕麦堡套餐", "供应时间:09:15--22:44")
	menu1.Add(NewMenuItem("主食", "培根鸡腿燕麦堡1个", 11.5))
	menu1.Add(NewMenuItem("小吃", "玉米沙拉1份", 5.0))
	menu1.Add(NewMenuItem("饮料", "九珍果汁饮料1杯", 6.5))

	menu2 := NewMenu("奥尔良烤鸡腿饭套餐", "供应时间:09:15--22:44")
	menu2.Add(NewMenuItem("主食", "新奥尔良烤鸡腿饭1份", 15.0))
	menu2.Add(NewMenuItem("小吃", "新奥尔良烤翅2块", 11.0))
	menu2.Add(NewMenuItem("饮料", "芙蓉荟蔬汤1份", 4.5))

	all := NewMenu("超值午餐", "周一至周五有售")
	all.Add(menu1)
	all.Add(menu2)

	all.Print()
}

В результате получается следующее:

超值午餐, 周一至周五有售, ¥53.50
------------------------
培根鸡腿燕麦堡套餐, 供应时间:09:15--22:44, ¥23.00
------------------------
  主食, ¥11.50
    -- 培根鸡腿燕麦堡1个
  小吃, ¥5.00
    -- 玉米沙拉1份
  饮料, ¥6.50
    -- 九珍果汁饮料1杯

奥尔良烤鸡腿饭套餐, 供应时间:09:15--22:44, ¥30.50
------------------------
  主食, ¥15.00
    -- 新奥尔良烤鸡腿饭1份
  小吃, ¥11.00
    -- 新奥尔良烤翅2块
  饮料, ¥4.50
    -- 芙蓉荟蔬汤1份

объектно-ориентированная реализация

Сначала позвольте мне объяснить: Go не является объектно-ориентированным языком, на самом деле в нем есть только структуры, а не классы или объекты. Но для удобства пояснения воспользуюсь им позжеЭтот термин используется для обозначения определения структуры с использованием对象Этот термин используется для обозначения экземпляра структуры.

По соглашению сначала используется классический объектно-ориентированный анализ. Прежде всего, нам нужно определить абстрактный базовый класс для меню и пунктов меню, чтобы пользователи могли полагаться только на интерфейс, чтобы добиться согласованности в использовании.

В языке Go нет наследования, поэтому мы определяем абстрактный базовый класс как интерфейс, а меню и пункты меню позже будут реализовывать определенные функции:

type MenuComponent interface {
	Name() string
	Description() string
	Price() float32
	Print()

	Add(MenuComponent)
	Remove(int)
	Child(int) MenuComponent
}

Реализация пункта меню:

type MenuItem struct {
	name        string
	description string
	price       float32
}

func NewMenuItem(name, description string, price float32) MenuComponent {
	return &MenuItem{
		name:        name,
		description: description,
		price:       price,
	}
}

func (m *MenuItem) Name() string {
	return m.name
}

func (m *MenuItem) Description() string {
	return m.description
}

func (m *MenuItem) Price() float32 {
	return m.price
}

func (m *MenuItem) Print() {
	fmt.Printf("  %s, ¥%.2f\n", m.name, m.price)
	fmt.Printf("    -- %s\n", m.description)
}

func (m *MenuItem) Add(MenuComponent) {
	panic("not implement")
}

func (m *MenuItem) Remove(int) {
	panic("not implement")
}

func (m *MenuItem) Child(int) MenuComponent {
	panic("not implement")
}

Обратите внимание на два момента.

  1. NewMenuItem() создает MenuItem, но возвращает абстрактный интерфейс MenuComponent. (Полиморфизм в объектной ориентации)
  2. Поскольку MenuItem является конечным узлом, реализация трех методов Add() Remove() Child() не может быть обеспечена, поэтому при вызове произойдет паника.

Вот реализация меню:

type Menu struct {
	name        string
	description string
	children    []MenuComponent
}

func NewMenu(name, description string) MenuComponent {
	return &Menu{
		name:        name,
		description: description,
	}
}

func (m *Menu) Name() string {
	return m.name
}

func (m *Menu) Description() string {
	return m.description
}

func (m *Menu) Price() (price float32) {
	for _, v := range m.children {
		price += v.Price()
	}
	return
}

func (m *Menu) Print() {
	fmt.Printf("%s, %s, ¥%.2f\n", m.name, m.description, m.Price())
	fmt.Println("------------------------")
	for _, v := range m.children {
		v.Print()
	}
	fmt.Println()
}

func (m *Menu) Add(c MenuComponent) {
	m.children = append(m.children, c)
}

func (m *Menu) Remove(idx int) {
	m.children = append(m.children[:idx], m.children[idx+1:]...)
}

func (m *Menu) Child(idx int) MenuComponent {
	return m.children[idx]
}

вPrice()Статистика по всем детямPriceпочтовая сумма,Print()После вывода собственной информации выводить информацию всех подэлементов по очереди. Также обратите вниманиеRemove()реализация (удаляет элемент из среза).

Хорошо, теперь рассмотрим следующие 3 вопроса для этой реализации.

  1. MenuItemа такжеMenuВ нем два атрибута и метода имени и описания, и писать его дважды явно избыточно. При использовании любого другого объектно-ориентированного языка и свойства, и методы должны быть перемещены в базовый класс для реализации. Но в Go нет наследования, что действительно глупо.
  2. Здесь мы действительно достигаемПоследовательный доступ пользователейВсе же? Очевидно, что нет, когда пользователь получаетMenuComponentПосле этого все равно необходимо знать его тип, прежде чем его можно будет правильно использовать.MenuItemиспользоватьAdd()Подождите, пока нереализованные методы запаникуют. Точно так же мы можем абстрагировать папки/файлы в «узлы файловой системы», которые могут считывать имена и вычислять занимаемое пространство, но как только мы захотим добавить дочерние узлы в «узлы файловой системы», нам все равно придется решить, является ли это папкой в конец.
  3. Продолжайте думать над пунктом 2: произвести какой-либопоследовательный доступЧто является существенной причиной явления? Точка зрения:Menuа такжеMenuItemчто-то, что по существу (является) одним и тем же (MenuComponent), чтобы к ним можно было обращаться последовательно; другой вид:Menuа такжеMenuItemэто две разные вещи, которые просто имеют одни и те же свойства, поэтому к ним можно обращаться последовательно.

Используйте композицию вместо наследования

Как упоминалось ранее, язык Go не имеет наследования, а имя и описание, которые изначально принадлежали базовому классу, не могут быть реализованы в базовом классе. На самом деле, если вы измените свое мышление, эту проблему очень легко решить путем комбинирования. если мы думаемMenuа такжеMenuItemПо сути, это две разные вещи, но они обладают некоторыми одинаковыми свойствами, поэтому, если одни и те же свойства будут извлечены, а затем объединены в две, проблема будет решена.

Сначала посмотрите на извлеченные свойства:

type MenuDesc struct {
	name        string
	description string
}

func (m *MenuDesc) Name() string {
	return m.name
}

func (m *MenuDesc) Description() string {
	return m.description
}

переписатьMenuItem:

type MenuItem struct {
	MenuDesc
	price float32
}

func NewMenuItem(name, description string, price float32) MenuComponent {
	return &MenuItem{
		MenuDesc: MenuDesc{
			name:        name,
			description: description,
		},
		price: price,
	}
}

// ... 方法略 ...

переписатьMenu:

type Menu struct {
	MenuDesc
	children []MenuComponent
}

func NewMenu(name, description string) MenuComponent {
	return &Menu{
		MenuDesc: MenuDesc{
			name:        name,
			description: description,
		},
	}
}

// ... 方法略 ...

Правильное использование композиции в Go помогает выразить назначение структур данных.. Особенно, когда более сложный объект обрабатывает несколько вещей одновременно, будет очень ясно и элегантно разделить объект на несколько независимых частей, а затем объединить их вместе. Например, вышеMenuItemописание + цена,MenuЭто описание + подменю.

На самом деле дляMenu, лучше положитьchildrenа такжеAdd() Remove() Child()Также извлеките пакет, а затем объедините его, чтобыMenuфункция понятна с первого взгляда.

type MenuGroup struct {
	children []MenuComponent
}

func (m *Menu) Add(c MenuComponent) {
	m.children = append(m.children, c)
}

func (m *Menu) Remove(idx int) {
	m.children = append(m.children[:idx], m.children[idx+1:]...)
}

func (m *Menu) Child(idx int) MenuComponent {
	return m.children[idx]
}

type Menu struct {
	MenuDesc
	MenuGroup
}

func NewMenu(name, description string) MenuComponent {
	return &Menu{
		MenuDesc: MenuDesc{
			name:        name,
			description: description,
		},
	}
}

Образ мышления на языке Go

В центре внимания этой статьи находится следующее. Используя язык Go для разработки проектов более 2 месяцев, самое большое ощущение: изучение языка Go должно изменить способ мышления, успех изменений бесконечен, и если вы не сможете измениться вовремя, вы обнаружите, что попали везде стена.

Давайте воспользуемся настоящим способом Go для реализации меню KFC. Прежде всего, пожалуйста, повторите три раза: нет наследования, нет наследования, нет наследования; нет базового класса, нет базового класса, нет базового класса; интерфейс — это просто набор сигнатур функций, интерфейс — это просто набор сигнатур функций, интерфейс — это просто набор сигнатур функций; структура не зависит от интерфейса, структура не зависит от интерфейса, структура не зависит от интерфейса.

Ну, в отличие от ранее, теперь мы не определяем сначала интерфейс, а затем реализуем его конкретно, потому что структура не зависит от интерфейса, поэтому мы напрямую реализуем конкретную функцию. первыйMenuDescа такжеMenuItem, обратите внимание сейчасNewMenuItemТип возвращаемого значения:*MenuItem.

type MenuDesc struct {
	name        string
	description string
}

func (m *MenuDesc) Name() string {
	return m.name
}

func (m *MenuDesc) Description() string {
	return m.description
}

type MenuItem struct {
	MenuDesc
	price float32
}

func NewMenuItem(name, description string, price float32) *MenuItem {
	return &MenuItem{
		MenuDesc: MenuDesc{
			name:        name,
			description: description,
		},
		price: price,
	}
}

func (m *MenuItem) Price() float32 {
	return m.price
}

func (m *MenuItem) Print() {
	fmt.Printf("  %s, ¥%.2f\n", m.name, m.price)
	fmt.Printf("    -- %s\n", m.description)
}

ДалееMenuGroup. мы знаемMenuGroupпредставляет собой набор меню/пунктов меню,childrenТип не определен, поэтому мы знаем, что здесь нужно определить интерфейс. и потому, чтоMenuGroupлогика правильнаяchildrenДобавить, удалить, прочитать операции,childrenНет никаких ограничений или требований к свойствам , поэтому мы временно определяем интерфейс как пустой интерфейс.interface{}.

type MenuComponent interface {
}

type MenuGroup struct {
	children []MenuComponent
}

func (m *Menu) Add(c MenuComponent) {
	m.children = append(m.children, c)
}

func (m *Menu) Remove(idx int) {
	m.children = append(m.children[:idx], m.children[idx+1:]...)
}

func (m *Menu) Child(idx int) MenuComponent {
	return m.children[idx]
}

Ну наконец тоMenuРеализация:

type Menu struct {
	MenuDesc
	MenuGroup
}

func NewMenu(name, description string) *Menu {
	return &Menu{
		MenuDesc: MenuDesc{
			name:        name,
			description: description,
		},
	}
}

func (m *Menu) Price() (price float32) {
	for _, v := range m.children {
		price += v.Price()
	}
	return
}

func (m *Menu) Print() {
	fmt.Printf("%s, %s, ¥%.2f\n", m.name, m.description, m.Price())
	fmt.Println("------------------------")
	for _, v := range m.children {
		v.Print()
	}
	fmt.Println()
}

в реализацииMenuВ процессе мы обнаружили, чтоMenuк этомуchildrenНа самом деле есть два ограничения: должно бытьPrice()Методы иPrint()метод. Так даMenuComponentмодифицировать:

type MenuComponent interface {
	Price() float32
	Print()
}

последнее наблюдениеMenuItemа такжеMenu, они все встречаютсяMenuComponent, так что оба могут бытьMenuизchildren, комбинированный режим готов!

Сравните и подумайте

Разница между двумя кодами до и после на самом деле очень мала:

  1. Второй реализованный интерфейс проще, всего две функции.
  2. Тип возвращаемого значения функции New отличается.

С точки зрения мышления, разница большая, но также немного тонкая:

  1. Интерфейс в первой реализации представляет собой шаблон, который представляет собой чертеж структуры, а его атрибуты исходят из всестороннего анализа и предварительного введения компонентов системы; интерфейс во второй реализации представляет собой оператор ограничения, а его атрибуты исходят из восприятие пользователем пользователя.
  2. Первое осознание думаетchildrenсерединаMenuComponentЭто конкретный объект, у этого объекта есть ряд методов, которые можно вызвать, но функция его методов будет другой из-за охвата подкласса; вторая реализация считает, чтоchildrenсерединаMenuComponentМожет быть любым несвязанным объектом, единственное требование состоит в том, чтобы они "просто случились" для реализации ограничений, заданных интерфейсом.

Обратите внимание, что в первой реализацииMenuComponentимеютAdd(),Remove(),Child()Есть три метода, но они не обязательно доступны, и возможность их использования зависит от типа конкретного объекта; во второй реализации этих небезопасных методов нет, потому что функция New возвращает конкретный тип, поэтому методы что можно назвать безопасными.

Кроме того, изMenuВыньте ребенка из , доступные методы толькоPrice()а такжеPrint(), его можно назвать совершенно смело. если вы хотитеMenuComponentдаMenuВ случае добавления к нему подпунктов? Это просто:

if m, ok := all.Child(1).(*Menu); ok {
	m.Add(NewMenuItem("玩具", "Hello Kitty", 5.0))
}

Ясно и ясно, если ребенокMenu, то мы можем выполнитьAdd()работать.

Идем дальше, здесь наши требования к типу не так уж сильны, и нам не нужно, чтобы он былMenu, просто нужно предоставить комбинациюMenuComponentфункция, поэтому вы можете извлечь такой интерфейс:

type Group interface {
	Add(c MenuComponent)
	Remove(idx int)
	Child(idx int) MenuComponent
}

Предыдущий код для добавления дочерних элементов изменен на этот:

if m, ok := all.Child(1).(Group); ok {
	m.Add(NewMenuItem("玩具", "Hello Kitty", 5.0))
}

Рассмотрим снова операцию «покупка».В объектно-ориентированной реализации тип покупки:MenuComponent, поэтому операцию покупки можно применить и кMenuа такжеMenuItem. Если вы посмотрите на это с точки зрения языка Go, единственное требование к покупаемому объекту — это наличиеPrice(), поэтому параметр операции покупки представляет собой такой интерфейс:

type Product interface {
    Price() float32
}

Таким образом, операцию покупки можно применять не только кMenuа такжеMenuItem, а также для любого объекта, который предоставляет цену. Мы можем добавить любой товар, будь то игрушки или абонементы или купоны, лишь бы былиPrice()метод можно купить.

Суммировать

Наконец, чтобы обобщить мои мысли, добро пожаловать на обсуждение или критику:

  1. В режиме композиции согласованный доступ является псевдотребованием. Непротиворечивый доступ — это не требование, которому мы должны соответствовать во время разработки, а естественный эффект, когда разные объекты имеют одинаковые свойства. В приведенном выше примере мы создали два разных типа, меню и MenuItem, но, поскольку они имеют одинаковые свойства, мы можем взять цену, взять описание и добавить меню в качестве дочернего элемента таким же образом.
  2. Полиморфизм в языке Go проявляется не на этапе создания объекта, а на этапе использования объекта, разумное использование «маленьких интерфейсов» может значительно снизить степень связанности системы.

PS Я разместил три полных кода, задействованных в этой статье, на play.golang.org: (требуется FQ)