GoSuda

Goのインターフェースは継承ではない

By Yunjin Lee
views ...

概要

Goのインターフェースは、同一の引数と戻り値を持つ関数を複数の構造体に容易に持たせることを可能にしますが、Javaのextendsキーワードのように、その内部関数の動作まで適切に拡張し、オーバーライドする方式とは異なります。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言語が伝統的なOOPとは異なり慣れないかもしれませんが、慣れれば明示的なプログラミングが可能になります。

要約

  • 責任の所在を孤立させること
  • 構造体単位で細かく分割すること
  • メソッドをJavaの抽象クラスのように理解してはならない
  • 明示的かつ具体的なプログラミングをすること Go言語は伝統的なOOPモデルよりも簡潔明瞭であり、個別に扱われるべきです。拡張的にプログラミングするのではなく、段階的かつ構造的に分離して記述するようにしましょう。