GoSuda

Go Interfaces Are Not Inheritance

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()

В обоих примерах важными моментами являются следующие:

  • main должен быть простым, функции — по назначению
  • Если структуры разные, используйте разные объекты
  • Если требуется совместное использование, используйте внутренние структуры

Какие преимущества дает такая философия программирования?

Преимущества

  • Четкое разделение методов, которые требуют совместного использования, и тех, которые не требуют
  • Разделение ответственности между отдельными структурами и методами
  • Структурно разделенный код в соответствии с требуемыми функциональными спецификациями

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

Резюме

  • Изолируйте области ответственности
  • Делите на более мелкие структуры
  • Методы не следует понимать как абстрактные классы в Java
  • Пишите явный и конкретный код Язык Go должен рассматриваться как более простой и индивидуальный, чем традиционная модель ООП. Вместо того чтобы программировать расширяемо, давайте писать код поэтапно и структурно разделяя его.