GoSuda

Das Go Interface ist keine Vererbung

By Yunjin Lee
views ...

Überblick

Go-Schnittstellen ermöglichen es, dass mehrere Strukturen Funktionen mit identischen Argumenten und Rückgabewerten aufweisen können; dies unterscheidet sich jedoch von dem Mechanismus in Java, bei dem das extends-Schlüsselwort die Implementierung interner Funktionen angemessen erweitert und überschreibt. Man wird die Verwechslung mit Vererbung nur vermeiden können, wenn man die kompositorische Wiederverwendung von Code in Go vollständig erfasst hat, doch eine theoretisch vollkommene Einsicht von Beginn an ist anspruchsvoll. Untersuchen wir dies anhand von Szenarien, in denen Fehler leicht entstehen können.

Häufige Fehler

Anfänger neigen möglicherweise zu folgenden Fehlern.

 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}

Ein solcher Code erscheint unproblematisch, wenn man fälschlicherweise annimmt, Go folge der traditionellen Vererbung. Die Ausgabe dieses Codes ist 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

Hierdurch wird das Verhalten von Go lediglich verdeutlicht:

1watermelon := apple

Dieser Code konvertiert apple keineswegs in eine Instanz der Watermelon-Klasse. Vielmehr ist watermelon lediglich ein Pointer auf apple.

Hierbei muss erneut betont werden: Go folgt nicht dem Konzept der traditionellen Vererbung.

Wenn Code unter dieser fehlerhaften Annahme erstellt wird, resultiert dies in fatalen Fehlern wie unnötiger Pointer-Erstellung und unerwarteter Funktionskopie für andere Strukturen.

Wie sieht dann ein vorbildlicher Code aus?

Ein geeignetes Beispiel in der Sprache 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}

Es ist jedoch möglich, in Go etwas zu implementieren, das der Vererbung ähnelt. Ein Beispiel hierfür ist das sogenannte anonyme Embedding. Dies wird realisiert, indem die innere Struktur als namenlose Struktur deklariert wird. In solchen Fällen ist der direkte Zugriff auf die Felder der eingebetteten Struktur ohne explizite Nennung möglich. Dieses Muster, bei dem Felder der eingebetteten Struktur in die umgebende Struktur promoviert werden, kann je nach Anwendungsfall die Lesbarkeit erhöhen. Es wird jedoch davon abgeraten, dieses Muster zu verwenden, wenn die eingebettete Struktur explizit dargestellt werden muss.

 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 ergeben sich folgende Differenzen:

1w.PrintAll() // Aufruf durch automatische Promotion mittels der namenlosen Struktur und nicht w.Fruit.PrintAll()

Beide Beispiele verdeutlichen wesentliche Punkte:

  • Die main-Funktion sollte schlank gehalten werden; Funktionen sollten funktionsspezifisch sein.
  • Bei unterschiedlichen Strukturen sind unterschiedliche Objekte zu verwenden.
  • Bei bestehendem Bedarf an gemeinsamer Nutzung ist die eingebettete Struktur zu verwenden.

Welche Vorteile bietet eine solche Programmierphilosophie?

Vorteile

  • Klare Unterscheidung zwischen Methoden, die geteilt werden müssen, und solchen, die es nicht tun.
  • Trennung der Verantwortlichkeiten auf individuelle Strukturen und Methoden.
  • Strukturell getrennter Code gemäß der Spezifikation der benötigten Funktionalität.

Obwohl die Sprache Go anfangs aufgrund ihrer Abweichung von der traditionellen OOP ungewohnt erscheinen mag, ermöglicht sie bei Vertrautheit eine explizite Programmierung.

Zusammenfassung

  • Die Verantwortlichkeiten sind zu isolieren.
  • Eine detaillierte Unterteilung entsprechend der Struktur ist vorzunehmen.
  • Methoden dürfen nicht wie abstrakte Klassen in Java interpretiert werden.
  • Es ist eine explizite und konkrete Programmierung anzustreben. Die Sprache Go sollte einfacher und individueller behandelt werden als das traditionelle OOP-Modell. Anstatt erweiternd zu programmieren, sollten wir stufenweise und strukturell getrennt vorgehen.