Interfejsy Go nie są dziedziczeniem
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.