Go интерфейсы — это не наследование
Обзор
Интерфейсы 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