GoSuda

Interfejsy Go nie są dziedziczeniem

By Yunjin Lee
views ...

Przegląd

Interfejsy w Go umożliwiają łatwe definiowanie funkcji o tych samych argumentach i wartościach zwracanych w wielu strukturach, jednak różni się to od sposobu rozszerzania i nadpisywania wewnętrznego działania tych funkcji, jaki oferuje słowo kluczowe extends w Javie. Prawidłowe zrozumienie kompozycyjnego ponownego użycia kodu w Go jest niezbędne, aby uniknąć pomyłek z dziedziczeniem, ale początkowe, teoretycznie doskonałe zrozumienie jest trudne. Przyjrzyjmy się scenariuszom, w których łatwo o błędy.

Częste błędy

Początkujący użytkownicy mogą popełniać następujące błędy:

 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}

Ten kod wydaje się być bezproblemowy, jeśli błędnie zakłada się, że Go stosuje tradycyjne dziedziczenie. Jednakże jego wynik jest następujący:

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

W tym miejscu działanie Go staje się jasne.

1watermelon := apple

Ten kod wcale nie konwertuje Apple bezpośrednio na klasę Watermelon. Zamiast tego, watermelon jest jedynie wskaźnikiem do apple.

Warto ponownie podkreślić, że Go nie stosuje tradycyjnej koncepcji dziedziczenia.

Tworzenie kodu z takim błędnym zrozumieniem prowadzi do krytycznych błędów, takich jak generowanie bezsensownych wskaźników czy nieoczekiwane kopiowanie funkcji dla innych struktur.

Jaki zatem jest przykład wzorcowego kodu?

Odpowiedni przykład w języku 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}

Jednakże, w Go możliwe jest stworzenie czegoś, co przypomina dziedziczenie. Przykładem jest anonimowe osadzanie, które jest możliwe poprzez zadeklarowanie wewnętrznej struktury bez nazwy. W takich przypadkach pola podrzędnej struktury mogą być używane bez wyraźnego określania i są dostępne. Wykorzystanie tego wzorca, który promuje pola podrzędnej struktury do struktury nadrzędnej, może w niektórych przypadkach poprawić czytelność. Jednakże, jeśli podrzędna struktura musi być wyraźnie widoczna, zaleca się unikanie tego rozwiązania.

 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}

W tym przykładzie występują następujące różnice:

1w.PrintAll() // w.Friut.PrintAll() nie jest wywoływane, lecz automatyczne wywołanie promocji poprzez anonimową strukturę

Oba przykłady podkreślają następujące kluczowe punkty:

  • Upraszczanie funkcji main, a funkcje według ich przeznaczenia.
  • Różne struktury powinny odpowiadać różnym obiektom.
  • W przypadku potrzeby współdzielenia należy używać wewnętrznych struktur.

Jakie są zalety tej filozofii programowania?

Zalety

  • Wyraźne rozróżnienie między metodami wymagającymi współdzielenia a tymi, które tego nie wymagają.
  • Rozdzielenie odpowiedzialności na poszczególne struktury i metody.
  • Strukturalnie rozdzielony kod zgodnie z wymaganiami funkcjonalnymi.

Początkowo język Go może wydawać się nieznany ze względu na różnice w stosunku do tradycyjnego OOP, ale z czasem pozwala na bardziej deklaratywne programowanie.

Podsumowanie

  • Izoluj obszary odpowiedzialności.
  • Dziel na szczegółowe jednostki strukturalne.
  • Metod nie należy rozumieć jako abstrakcyjnych klas w Javie.
  • Stosuj programowanie deklaratywne i konkretne. Język Go powinien być traktowany w sposób prostszy i bardziej indywidualny niż tradycyjny model OOP. Zamiast programować w sposób rozszerzalny, dążmy do stopniowego i strukturalnego podziału kodu.