Antarmuka dan embedding struktur dalam Go: perbedaannya dengan pewarisan kelas dan desain komposisi

Ikhtisar
Antarmuka (interface) di Go memudahkan sebuah fungsi dengan argumen dan nilai kembalian yang sama untuk dimiliki oleh banyak struct, tetapi cara ini berbeda dengan cara kata kunci extends di Java yang memperluas serta menimpa (override) perilaku fungsi di dalamnya secara tepat. Pemahaman yang benar tentang penggunaan ulang kode komposisional di Go diperlukan agar tidak tertukar dengan pewarisan (inheritance), tetapi memahaminya secara teoretis dan sempurna sejak awal memang sulit. Mari kita pelajari bersama dengan skenario yang mudah menimbulkan kesalahan.
Kesalahan yang Sering Dilakukan
Pemula mungkin melakukan kesalahan-kesalahan berikut.
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}
Kode seperti ini tampak tidak bermasalah jika kita keliru menganggap bahwa Go mengikuti pewarisan tradisional. Namun, keluarannya adalah sebagai berikut.
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
Di sini perilaku Go menjadi jelas hanya melalui baris berikut.
1watermelon := apple
Kode ini sama sekali tidak mengubah Apple menjadi kelas Watermelon secara langsung. Melainkan, watermelon hanyalah pointer yang menunjuk ke apple.
Sekali lagi ditegaskan di sini, Go tidak mengikuti konsep pewarisan tradisional.
Jika kode ditulis dengan kesalahpahaman seperti ini, akan muncul kesalahan fatal seperti pembuatan pointer yang tidak berarti dan penyalinan fungsi untuk struct lain yang tidak terduga.
Lalu, seperti apakah kode yang ideal?
Contoh yang Tepat dalam Bahasa 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}
Namun, di Go tetap dimungkinkan untuk membuat sesuatu tampak seperti pewarisan. Contohnya adalah embedding anonim. Hal ini dapat dilakukan dengan mendeklarasikan struct internal sebagai struct tanpa nama. Dalam kasus ini, field dari struct bawahan dapat diakses secara langsung tanpa perlu menyebutkannya secara eksplisit. Dengan menggunakan pola promosi field struct bawahan ke struct atas, keterbacaan kode dapat meningkat dalam kondisi tertentu. Akan tetapi, jika struct bawahan perlu ditampilkan secara eksplisit, pola ini tidak disarankan.
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}
Pada contoh ini terdapat perbedaan sebagai berikut.
1w.PrintAll() // Pemanggilan otomatis melalui promosi struct tanpa nama, bukan w.Friut.PrintAll()
Kedua contoh memiliki poin penting sebagai berikut.
- main dibuat sederhana, fungsi dikelompokkan menurut fungsinya
- untuk struct yang berbeda, gunakan objek yang berbeda
- jika diperlukan berbagi, gunakan struct internal
Apa keuntungan dari filosofi pemrograman seperti ini?
Keuntungan
- Pemisahan yang jelas antara method yang perlu dibagikan dan yang tidak
- Pemisahan tanggung jawab pada masing-masing struct dan method
- Kode yang terpisah secara struktural sesuai spesifikasi fungsi yang dibutuhkan
Pada awalnya bahasa Go mungkin terasa asing karena berbeda dari OOP tradisional, tetapi setelah terbiasa, pemrograman yang eksplisit menjadi mungkin.
Ringkasan
- Isolasikan tanggung jawab
- Pisahkan secara terperinci per unit struct
- Method tidak boleh dipahami seperti abstract class di Java
- Lakukan pemrograman yang eksplisit dan konkret Bahasa Go harus diperlakukan secara lebih sederhana, jelas, dan terpisah dibandingkan model OOP tradisional. Alih-alih memprogram secara ekstensif, mari tulis kode dengan memisahkannya secara bertahap dan terstruktur.