GoSuda

Go-Schnittstellen und Struktureinbettung: Unterschiede zur Klassenvererbung und kompositionsbasiertes Design

By Yunjin Lee views ...

Überblick

Go-Interfaces erleichtern es, dass mehrere Structs Funktionen mit denselben Argumenten und Rückgabewerten besitzen, unterscheiden sich jedoch von der Art, wie Java mit dem Schlüsselwort extends das Verhalten dieser Methoden erweitert und überschreibt. Um die kompositionelle Code-Wiederverwendung in Go richtig zu verstehen, sollte man sie nicht mit Vererbung verwechseln; doch ein theoretisch vollständiges Verständnis von Grund auf ist schwer zu erlangen. Lassen Sie uns anhand von Szenarien, in denen man leicht Fehler macht, darüber sprechen.

Häufige Fehler

Anfänger können Fehler wie die folgenden machen.

 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}

Wer glaubt, dass Go traditionelle Vererbung kennt, dem scheint dieser Code unproblematisch. Die Ausgabe dieses Codes lautet jedoch wie folgt.

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

Hier wird das Verhalten von Go lediglich deutlicher.

1watermelon := apple

Dieser Code wandelt Apple keineswegs in die Struct Watermelon um. Vielmehr ist watermelon lediglich ein Zeiger auf apple.

Nochmals betont: Go folgt nicht dem traditionellen Konzept der Vererbung.

Wer mit diesem Missverständnis Code schreibt, begeht gravierende Fehler wie die Erzeugung sinnloser Zeiger oder das unbeabsichtigte Kopieren von Funktionen für andere Structs.

Wie sieht also ein musterhafter Code aus?

Angemessenes Beispiel in 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}

Dennoch ist es in Go möglich, Vererbung ähnlich erscheinen zu lassen. Ein Beispiel dafür ist die anonyme Einbettung. Sie ist möglich, wenn man die innere Struct als namenlose Struct deklariert. In diesem Fall kann man auch ohne explizite Angabe auf die Felder der untergeordneten Struct direkt zugreifen. Mit diesem Muster, bei dem die Felder der untergeordneten Struct in die übergeordnete Struct hochgestuft werden, kann die Lesbarkeit je nach Situation verbessert werden. Wenn die untergeordnete Struct jedoch explizit sichtbar gemacht werden muss, wird von der Verwendung abgeraten.

 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}

In diesem Beispiel besteht folgender Unterschied.

1w.PrintAll() // Automatischer Aufstieg über die namenlose Struct-Einbettung, statt w.Friut.PrintAll()

Beide Beispiele haben folgende wichtige Gesichtspunkte gemeinsam:

  • main schlank halten, Funktionen nach Funktionalität aufteilen
  • Für unterschiedliche Structs jeweils eigene Objekte verwenden
  • Bei Bedarf gemeinsamer Nutzung die innere Struct verwenden

Welche Vorteile hat diese Programmierphilosophie?

Vorteile

  • Klare Unterscheidung zwischen Methoden, die gemeinsam genutzt werden müssen, und solchen, die es nicht müssen
  • Trennung der Verantwortlichkeiten auf einzelne Structs und Methoden
  • Entsprechend der benötigten Funktionsspezifikation strukturell getrennter Code

Zunächst mag Go ungewohnt sein, da es sich von der traditionellen OOP unterscheidet; mit zunehmender Vertrautheit wird jedoch eine explizite Programmierung möglich.

Zusammenfassung

  • Verantwortlichkeiten isolieren
  • Nach Structs im Einzelnen aufteilen
  • Methoden dürfen nicht wie abstrakte Klassen in Java verstanden werden
  • Explizit und konkret programmieren Go ist einfacher und übersichtlicher als das traditionelle OOP-Modell und sollte einzeln behandelt werden. Anstatt erweiterbar zu programmieren, schreiben Sie den Code besser schrittweise und strukturiert getrennt.