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

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.