Go Interfaces Are Not Inheritance
Обзор
Интерфейсы 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()
В обоих примерах важными моментами являются следующие:
mainдолжен быть простым, функции — по назначению- Если структуры разные, используйте разные объекты
- Если требуется совместное использование, используйте внутренние структуры
Какие преимущества дает такая философия программирования?
Преимущества
- Четкое разделение методов, которые требуют совместного использования, и тех, которые не требуют
- Разделение ответственности между отдельными структурами и методами
- Структурно разделенный код в соответствии с требуемыми функциональными спецификациями
Поначалу Go может показаться непривычным, поскольку он отличается от традиционного ООП, но, освоившись, можно писать более явный код.
Резюме
- Изолируйте области ответственности
- Делите на более мелкие структуры
- Методы не следует понимать как абстрактные классы в Java
- Пишите явный и конкретный код Язык Go должен рассматриваться как более простой и индивидуальный, чем традиционная модель ООП. Вместо того чтобы программировать расширяемо, давайте писать код поэтапно и структурно разделяя его.