GoSuda

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

By Yunjin Lee views ...

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.