GoSuda

Go Arayüzleri ve Yapı Gömme: Sınıf Kalıtımıyla Farklar ve Bileşim Tabanlı Tasarım

By Yunjin Lee views ...

Genel Bakış

Go arayüzleri (interface), aynı argüman ve dönüş değerlerine sahip fonksiyonları birden fazla struct üzerinde kolayca bulundurmayı sağlar; ancak Java'daki extends anahtar kelimesinde olduğu gibi, iç fonksiyonların davranışını uygun şekilde genişletme ve geçersiz kılma (override) yöntemiyle aynı değildir. Go'nun bileşimsel (compositional) kod yeniden kullanımını doğru anlamadan kalıtımla karıştırmamak mümkün değildir; ancak başlangıçtan itibaren teorik olarak tam bir kavrayışa sahip olmak zordur. Hata yapmaya elverişli senaryolarla birlikte inceleyelim.

Sık Yapılan Hatalar

Yeni başlayanlar aşağıdaki gibi hatalar yapabilir.

 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'nun geleneksel kalıtımı izlediğini düşündüğünüzde bu kod sorunsuz görünebilir. Ancak bu kodun çıktısı aşağıdaki gibidir.

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

Burada Go'nun davranışı yalnızca daha açık hale gelir.

1watermelon := apple

Bu kod, Apple'ı hiçbir şekilde Watermelon sınıfına dönüştürmez. Sadece watermelon, apple'a işaret eden bir pointer'dır.

Bunu bir kez daha vurgulamak gerekirse, Go geleneksel kalıtım kavramını izlemez.

Bu yanlış anlama ile kod yazıldığında, anlamsız pointer oluşturma, beklenmeyen diğer struct'lar için fonksiyon kopyalama gibi ciddi hatalar ortaya çıkar.

Peki örnek teşkil eden kod nasıl olmalıdır?

Go Dilinde Uygun Bir Örnek

 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}

Ancak Go'da da kalıtım gibi görünmesini sağlamak mümkündür. Bu, anonim gömme (anonymous embedding) adı verilen bir örnektir. İç struct'ı isimsiz olarak tanımladığınızda bu mümkün olur. Bu durumda alt struct'ın alanlarına açıkça belirtmeden de doğrudan erişilebilir. Alt struct'ın alanlarını üst struct'a yükseltme (promotion) desenini kullanmak, bazı durumlarda okunabilirliği artırabilir. Ancak alt struct'ın açıkça gösterilmesi gereken durumlarda bu yöntemin kullanılmaması önerilir.

 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}

Bu örnekte şu fark vardır.

1w.PrintAll() // w.Friut.PrintAll() yerine, isimsiz struct üzerinden otomatik yükseltilmiş çağrı

Her iki örnek için de önemli noktalar şunlardır:

  • main sade tutulur, fonksiyonlar işlevlerine göre ayrılır
  • Farklı yapılar için farklı nesneler kullanılır
  • Paylaşım gerektiğinde iç struct kullanılır

Bu programlama felsefesinin ne gibi faydaları vardır?

Faydalar

  • Paylaşılması gereken metotlar ile olmayanların ayrımı nettir
  • Sorumluluk, ayrı struct ve metotlara ayrılır
  • Gerekli işlev tanımına göre yapısal olarak ayrılmış kod

Başlangıçta Go dili geleneksel OOP'den farklı olduğu için alışık olunmayabilir; ancak alışıldığında açık ve net bir programlama yapmak mümkündür.

Özet

  • Sorumlulukları izole edelim
  • Struct birimleri bazında ayrıntılı bölelim
  • Metotlar Java'daki soyut sınıflar gibi anlaşılmamalıdır
  • Açık ve somut bir programlama yapalım Go dili, geleneksel OOP modelinden daha sade ve net olup ayrı ayrı ele alınmalıdır. Genişletilebilir şekilde programlamak yerine, adım adım ve yapısal olarak ayrıştırarak yazalım.