GoSuda

Go интерфейсы — это не наследование

By Yunjin Lee
views ...

Обзор

Интерфейсы Go позволяют легко использовать функции с одинаковыми аргументами и возвращаемыми значениями в нескольких структурах, но это отличается от способа расширения и переопределения поведения внутренних функций, как это делается с помощью ключевого слова extends в Java. Правильное понимание композиционного повторного использования кода в Go является необходимым для избегания путаницы с наследованием, однако с самого начала достичь идеального теоретического понимания сложно. Рассмотрим это на примере сценариев, в которых легко допустить ошибки.

Распространенные ошибки

Новички могут совершать следующие ошибки:

 1package main
 2import (
 3	"fmt"
 4	"strings"
 5)
 6
 7type Fruits interface {
 8	GetBrix() float64
 9	GetName() string
10	SetLabel()
11	GetLabel(string) string
12	PrintAll()
13}
14
15type Apple struct {
16	Label string
17	Name  string
18	Brix  float64
19}
20
21type Watermelon struct {
22	Label string
23	Name  string
24	Brix  float64
25}
26
27func (a *Apple) PrintAll() {
28	fmt.Printf("Fruit: %s, Label: %s, Brix: %v\n", a.Name, a.Label, a.Brix)
29}
30
31const (
32	NO_LABEL = "EMPTY LABEL"
33)
34
35func (a *Apple) SetLabel(lbl string) {
36	a.Brix 	= 14.5;
37	a.Name 	= "apple";
38	lbl_lower := strings.ToLower(lbl)
39	if strings.Contains(lbl_lower, a.Name) {
40		fmt.Println("Succeed: Label was ", lbl)
41		a.Label = lbl;
42	} else {
43		fmt.Println("Failed: Label was ", lbl)
44		a.Label = NO_LABEL;
45	}
46}
47
48func (w *Watermelon) SetLabel(lbl string) {
49	w.Brix = 10;
50	w.Name = "watermelon";
51	lbl_lower := strings.ToLower(lbl)
52	if strings.Contains(lbl_lower, w.Name) {
53		w.Label = lbl;
54	} else {
55		w.Label = NO_LABEL;
56	}
57}
58
59func main() {
60	fmt.Println("Inheritance test #1")
61	apple := new(Apple)
62	watermelon := apple
63	apple.SetLabel("Apple_1")
64	fmt.Println("Apple, before copied to Watermelon")
65	apple.PrintAll()
66	watermelon.SetLabel("WaterMelon_2")
67	fmt.Println("Apple, after copied to Watermelon")
68	apple.PrintAll()
69	fmt.Println("Watermelon, which inherited Apple's Method")
70	watermelon.PrintAll()
71}

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

1Inheritance test #1
2Succeed: Label was  Apple_1
3Apple, before copied to Watermelon
4Fruit: apple, Label: Apple_1, Brix: 14.5
5Failed: Label was  WaterMelon_2
6Apple, after copied to Watermelon
7Fruit: apple, Label: EMPTY LABEL, Brix: 14.5
8Watermelon, which inherited Apple's Method
9Fruit: apple, Label: EMPTY LABEL, Brix: 14.5

Здесь поведение Go становится ясным.

1watermelon := apple

Этот код совершенно не преобразует Apple в класс Watermelon. Watermelon является лишь указателем на apple.

Здесь следует еще раз подчеркнуть, что Go не следует традиционной концепции наследования.

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

Каким же тогда должен быть образцовый код?

Подходящий пример в языке Go

 1package main
 2import (
 3	"fmt"
 4	"strings"
 5)
 6
 7type Fruits interface {
 8	GetBrix() float64
 9	GetName() string
10	SetLabel()
11	GetLabel(string) string
12	PrintAll()
13}
14
15type BaseFruit struct {
16	Name  string
17	Brix  float64
18}
19
20type Apple struct {
21	Label string
22	Fruit BaseFruit
23}
24
25type Watermelon struct {
26	Label string
27	Fruit BaseFruit
28
29}
30
31func (b *BaseFruit) PrintAll() {
32	fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
33}
34
35
36const (
37	NO_LABEL = "EMPTY LABEL"
38)
39
40func (a *Apple) SetLabel(lbl string) {
41	a.Fruit.Brix 	= 14.5;
42	a.Fruit.Name 	= "apple";
43	lbl_lower := strings.ToLower(lbl)
44	if strings.Contains(lbl_lower, a.Fruit.Name) {
45		fmt.Println("Succeed: Label was ", lbl)
46		a.Label = lbl;
47	} else {
48		fmt.Println("Failed: Label was ", lbl)
49		a.Label = NO_LABEL;
50	}
51	fmt.Printf("Fruit %s label set to %s\n", a.Fruit.Name, a.Label);
52	a.Fruit.PrintAll()
53}
54
55func (w *Watermelon) SetLabel(lbl string) {
56	w.Fruit.Brix = 10;
57	w.Fruit.Name = "Watermelon";
58	lbl_lower := strings.ToLower(lbl)
59	if strings.Contains(lbl_lower, w.Fruit.Name) {
60		w.Label = lbl;
61	} else {
62		w.Label = NO_LABEL;
63	}
64	fmt.Printf("Fruit %s label set to %s\n", w.Fruit.Name, w.Label);
65	w.Fruit.PrintAll()
66}
67
68func main() {
69	apple := new(Apple)
70	watermelon := new(Watermelon)
71	apple.SetLabel("Apple_1")
72	watermelon.SetLabel("WaterMelon_2")
73}

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

 1package main
 2import (
 3	"fmt"
 4	"strings"
 5)
 6
 7type Fruits interface {
 8	GetBrix() float64
 9	GetName() string
10	SetLabel()
11	GetLabel(string) string
12	PrintAll()
13}
14
15type BaseFruit struct {
16	Name  string
17	Brix  float64
18}
19
20type Apple struct {
21	Label string
22	BaseFruit
23}
24
25type Watermelon struct {
26	Label string
27	BaseFruit
28
29}
30
31func (b *BaseFruit) PrintAll() {
32	fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
33}
34
35
36const (
37	NO_LABEL = "EMPTY LABEL"
38)
39
40func (a *Apple) SetLabel(lbl string) {
41	a.Brix 	= 14.5;
42	a.Name 	= "apple";
43	lbl_lower := strings.ToLower(lbl)
44	if strings.Contains(lbl_lower, a.Name) {
45		fmt.Println("Succeed: Label was ", lbl)
46		a.Label = lbl;
47	} else {
48		fmt.Println("Failed: Label was ", lbl)
49		a.Label = NO_LABEL;
50	}
51	fmt.Printf("Fruit %s label set to %s\n", a.Name, a.Label);
52	a.PrintAll()
53}
54
55func (w *Watermelon) SetLabel(lbl string) {
56	w.Brix = 10;
57	w.Name = "Watermelon";
58	lbl_lower := strings.ToLower(lbl)
59	if strings.Contains(lbl_lower, w.Name) {
60		w.Label = lbl;
61	} else {
62		w.Label = NO_LABEL;
63	}
64	fmt.Printf("Fruit %s label set to %s\n", w.Name, w.Label);
65	w.PrintAll()
66}
67
68func main() {
69	apple := new(Apple)
70	watermelon := new(Watermelon)
71	apple.SetLabel("Apple_1")
72	watermelon.SetLabel("WaterMelon_2")
73}

В данном примере имеются следующие отличия:

 1w.PrintAll() // Вызов автоматического повышения через безымянную структуру, а не w.Friut.PrintAll()
 2В обоих примерах важны следующие моменты:
 3- main должен быть упрощен, функции должны быть разделены по функциональности.
 4- Если это разные структуры, то это должны быть разные объекты.
 5- Если требуется совместное использование, используйте внутренние структуры.
 6
 7Какие преимущества дает такая философия программирования?
 8
 9## Преимущества
10
11- Четкое разграничение методов, требующих совместного использования, и тех, которые не требуют.
12- Разделение ответственности для отдельных структур и методов.
13- Структурно разделенный код в соответствии с требуемой спецификацией функций.
14
15
16Изначально язык Go может показаться непривычным, поскольку он отличается от традиционного ООП, но, освоившись, можно писать более явный код.
17
18## Резюме
19- Изолировать ответственность.
20- Детально разделять по единицам структуры.
21- Методы не следует понимать как абстрактные классы в Java.
22- Писать явный и конкретный код.
23Язык Go должен рассматриваться как более простой и индивидуальный по сравнению с традиционной ООП-моделью. Вместо расширяющегося программирования следует писать код, разделенный на этапы и структуры.
24