Ders 04 / 13
Arayüz Gömme
Gömme, iki arayüzü kaynakta tek bir bileşik sözleşmede birleştiriyor ve bu birleşim hiçbir yeni yöntem eklemiyor: gömme ile kurulan ve elle yeniden yazılan aynı sözleşme, aynı iki yöntemi istiyor. Bileşik arayüzü karşılamak için bir tipin yazması gereken yöntem sayısı, gömülen arayüzlerin toplam yöntem sayısına eşit, ne fazla ne eksik. Sınırlayıcı ölçüm: gömme yalnız sözleşmeyi birleştiriyor, gerçekleştirimi birleştirmiyor — iki ayrı tip iki ayrı sözleşmeyi ayrı ayrı karşılayabiliyor ama bileşik sözleşmeyi hiçbiri karşılamıyor.
İçindekiler
Önceki ders bir tipin birden çok sözleşmeyi aynı anda karşılayabildiğini gördü, ama her sözleşme kaynakta ayrı ayrı duruyordu. Bir tip hem depolayabilmeli hem kapatılabilmeli gerektiğinde, bu iki isteği tek bir adla anmanın bir yolu gerekiyor. Gömme bu yolu sağlıyor: bir arayüz, başka arayüzleri kendi gövdesine yalnızca adlarını yazarak alabiliyor. Bu, önceki üç dersteki bağların hiçbirine benzemiyor — kaynakta açıkça yazılı, dolayısıyla bildirim sınıfının en açık örneği.
İlk dersteki on altı bağın yalnız dördü bildirimdeydi ve ikisi zaten gömmeyle ilgiliydi:
Gomulunun kendisi ve onu karşıladığını doğrulayan derleme zamanı kanıtı. Bu ders o iki
satırı büyütüp gömmenin kendi kurallarını ölçüyor — ne birleştirdiğini, ne kadarını
birleştirdiğini, ve nerede durduğunu. Üç ölçüm bu sırayla ilerliyor: önce gömmenin gerçekten
yeni bir yöntem eklemediği doğrulanıyor, sonra bileşik sözleşmeyi karşılamanın en az kaç
yöntem gerektirdiği bulunuyor, son olarak bu birleşmenin nerede durduğu — sözleşmede mi,
gerçekleştirimde mi — sınanıyor.
Sözleşmeleri Kaynakta Birleştirmek
Bir arayüz gömmesi, bir arayüz gövdesinin içine başka bir arayüzün adını yazmaktan ibaret.
Sonuç, gömülen arayüzlerin yöntemlerinin tamamını isteyen yeni bir arayüz. Yazım kısa:
gömülen arayüzün önüne bir alan adı konmuyor, yalnız kendi adı bir satır olarak yazılıyor —
bu, gömmenin yapı gömmesinden ödünç aldığı aynı yazım kuralı, adlandırılmamış bir alanmış
gibi görünen ama bir tip adı olan bir satır. Aşağıdaki ölçüm aynı sözleşmeyi iki yoldan
kuruyor — biri gömerek, biri elle yeniden yazarak — ve ikisinin de istediği yöntem sayısını
reflect ile sayıyor.
// esdegerlik.go — gomme ile kurulan sozlesme, elle yazilanla ayni yontem sayisini istiyor
package main
import (
"fmt"
"reflect"
)
type Okuyucu interface{ Oku() string }
type Yazici interface{ Yaz(m string) }
// Gomme: iki arayuz adi, gomulen arayuzun govdesine yaziliyor.
type OkuyupYazanGomulu interface {
Okuyucu
Yazici
}
// Ayni sozlesme, elle yeniden yazilmis.
type OkuyupYazanElle interface {
Oku() string
Yaz(m string)
}
func main() {
a := reflect.TypeOf((*OkuyupYazanGomulu)(nil)).Elem()
b := reflect.TypeOf((*OkuyupYazanElle)(nil)).Elem()
fmt.Println("gomme ile istenen yontem sayisi :", a.NumMethod())
fmt.Println("elle yazilan istenen yontem sayisi:", b.NumMethod())
for i := 0; i < a.NumMethod(); i++ {
fmt.Println(" yontem:", a.Method(i).Name)
}
}
gomme ile istenen yontem sayisi : 2 elle yazilan istenen yontem sayisi: 2 yontem: Oku yontem: Yaz
OkuyupYazanGomulu kaynakta yalnızca iki adı (Okuyucu, Yazici) taşıyor, hiçbir yöntem
imzası yazmıyor. Ama reflect ona sorulduğunda, tıpkı iki yöntemi elle yazan
OkuyupYazanElle gibi, tam iki yöntem istediğini söylüyor: Oku ve Yaz. Gömme bir
kısaltma — kaynakta daha az yazmayı sağlıyor — ama ürettiği sözleşme, elle yazılanla
birebir aynı. Bu, Go Temelleri kursunun Yapılar dersindeki gömme kavramının bir
uzantısı: orada bir yapı başka bir yapının alan ve yöntem adlarını yazılmadan taşıyordu,
burada bir arayüz başka bir arayüzün yöntem adlarını yazılmadan taşıyor. İkisi de aynı
sözcükle (gömme) anılıyor çünkü ikisi de aynı düzeneği kullanıyor: bir adı yazmak,
o adın taşıdığı her şeyi getiriyor.
AR10. reflect.TypeOf((*T)(nil)).Elem() yazımı, bir arayüz tipinin kendisini (bir arayüz
değerini değil) reflect.Type olarak elde etmenin standart yolu — (*T)(nil) bir işaretçi
değeri üretiyor, .Elem() bu işaretçinin gösterdiği arayüz tipine iniyor. Bu ölçüm boyunca
hiçbir gerçek T değeri oluşturulmuyor, yalnız tipin kendisi sorgulanıyor.
Gömme burada iki arayüzle sınırlı değil; gömülen bir arayüz kendisi de başka bir arayüz
gömebiliyor, ve zincir birkaç kademe uzayabiliyor. Aşağıdaki ölçüm OkuyupYazanGomuluyu
üçüncü bir arayüzle (Kapatan) daha gömerek bir kademe derinleştiriyor ve toplam yöntem
sayısının hâlâ doğru toplandığını sınıyor.
// zincir.go — iki kademeli gomme, toplam yontem sayisini yine dogru topluyor
package main
import (
"fmt"
"reflect"
)
type Okuyucu interface{ Oku() string }
type Yazici interface{ Yaz(m string) }
type Kapatan interface{ Kapat() error }
type OkuyupYazan interface {
Okuyucu
Yazici
}
type TamKaynak interface {
OkuyupYazan
Kapatan
}
func main() {
t := reflect.TypeOf((*TamKaynak)(nil)).Elem()
fmt.Println("iki kademeli gomme, toplam yontem sayisi:", t.NumMethod())
for i := 0; i < t.NumMethod(); i++ {
fmt.Println(" yontem:", t.Method(i).Name)
}
}
iki kademeli gomme, toplam yontem sayisi: 3 yontem: Kapat yontem: Oku yontem: Yaz
TamKaynak, OkuyupYazanı gömüyor, OkuyupYazan da kendi içinde Okuyucu ile Yaziciyi
gömüyor — iki kademe. Toplam yöntem sayısı yine üç, tek tek gömülen üç yöntemin
(Oku, Yaz, Kapat) toplamı. Kademe sayısı arttıkça toplama kuralı değişmiyor: her
kademe kendi altındaki bütün adları yukarı taşıyor, hiçbir ad kaybolmuyor ve hiçbir ad
iki kez sayılmıyor.
Bu, TamKaynakı okuyan birinin görmesi gereken şeyi netleştiriyor: kaynakta yalnız iki satır
(OkuyupYazan, Kapatan) var, ama sözleşme üç yöntem istiyor. Ara kademeyi (OkuyupYazan)
açmadan bu üç sayıyı kaynaktan çıkarmak mümkün değil — okuyucunun zincirin tamamını takip
etmesi gerekiyor. Gömme kaynağı kısaltıyor ama okuma yükünü aynı oranda azaltmıyor; kısa
kaynak, uzun bir sözleşmeyi hâlâ taşıyabiliyor.
Kaç Yöntem Yazmak Gerekiyor
Gömme yöntem eklemediğine göre, bileşik bir arayüzü karşılamak için bir tipin yazması gereken
en az yöntem sayısı, gömülen arayüzlerin yöntemlerinin toplamı olmalı — ne bir fazla ne bir
eksik. reflect bunu bir arayüz tipine sorarak saydı; aşağıdaki ölçüm aynı soruyu bir
arayüz tanımına karşı sınanan bir tipe sorarak, derleyicinin kendisinden alıyor. İkisi
aynı sayıya varmalı: reflectin söylediği sayı, derleyicinin kabul ettiği en küçük sayı.
// arac.go — bir kaynak parcasinin derlenip derlenmedigini programin icinden sinar
package main
import (
"os"
"os/exec"
"path/filepath"
)
func derlenirMi(kaynak string) bool {
dizin, err := os.MkdirTemp("", "sinama")
if err != nil {
return false
}
defer os.RemoveAll(dizin)
os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module sinama\n\ngo 1.24\n"), 0o644)
os.WriteFile(filepath.Join(dizin, "kod.go"), []byte(kaynak), 0o644)
cmd := exec.Command("go", "build", "./...")
cmd.Dir = dizin
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
return cmd.Run() == nil
}
// kac-yontem.go — bilesik arayuzu karsilamak icin gereken en az yontem sayisi olculuyor
package main
import "fmt"
func main() {
ortak := `package sinama
type Okuyucu interface{ Oku() string }
type Yazici interface{ Yaz(m string) }
type OkuyupYazan interface {
Okuyucu
Yazici
}
`
sifirYontem := ortak + `type Aday struct{}
var _ OkuyupYazan = Aday{}
`
birYontem := ortak + `type Aday struct{}
func (Aday) Oku() string { return "" }
var _ OkuyupYazan = Aday{}
`
ikiYontem := ortak + `type Aday struct{}
func (Aday) Oku() string { return "" }
func (Aday) Yaz(m string) {}
var _ OkuyupYazan = Aday{}
`
fmt.Println("sifir yontemle derleniyor mu:", derlenirMi(sifirYontem))
fmt.Println("bir yontemle derleniyor mu :", derlenirMi(birYontem))
fmt.Println("iki yontemle derleniyor mu :", derlenirMi(ikiYontem))
}
sifir yontemle derleniyor mu: false bir yontemle derleniyor mu : false iki yontemle derleniyor mu : true
Sıfır yöntemli aday derlenmiyor, çünkü ikisi de eksik. Bir yöntemli aday (yalnız Oku) da
derlenmiyor, çünkü Yaz hâlâ eksik — Okuyucuyu karşılamak OkuyupYazanı karşılamak
anlamına gelmiyor, gömme bir arayüzü diğerinin yerine geçirmiyor, ikisini birlikte istiyor.
İki yöntemli aday derleniyor ve bu, tam olarak reflectin bir önceki bölümde saydığı sayı.
Üç aday, tek bir eşiği (iki yöntem) iki yönden de sınıyor: bir eksik yetmiyor, tam sayı
yetiyor.
Bu eşik, üçüncü dersteki küçük arayüz ölçümüyle aynı ekseni izliyor: bir arayüzün istediği
yöntem sayısı ne kadar büyükse, onu karşılayan tipin yükü o kadar ağır. Gömme burada
yöntem sayısını azaltmıyor; yalnız iki ayrı sözleşmeyi tek bir adla anmayı
kolaylaştırıyor. OkuyupYazanı karşılamanın maliyeti, Okuyucuyu ve Yaziciyi ayrı ayrı
karşılamanın maliyetinin toplamı — gömme bu toplamı düşürecek bir kısayol sunmuyor, yalnız
adlandırmayı tek bir noktada topluyor.
AR11. Üç aday da aynı Aday adını taşıyor ve aynı OkuyupYazana karşı sınanıyor;
tek değişen, Adayın kendi yöntem kümesi. Bu, üç ayrı ölçümü karşılaştırılabilir kılıyor —
fark yalnız bir değişkende, geri kalan her şey sabit. Aynı disiplin dördüncü bir adayla da
sınanabilirdi — üç yöntem yazan, ikisi OkuyupYazanın istediği isimlerle eşleşen, üçüncüsü
fazladan bir yöntem olan bir tip. Böyle bir tip yine derlenirdi, çünkü arayüz karşılaması bir
alt küme ilişkisidir: tipin yöntem kümesi arayüzün istediğini kapsıyorsa, fazlası bir
sorun oluşturmuyor.
Sınırlayıcı Ölçüm: Sözleşme Birleşir, Gerçekleştirim Birleşmez
Gömme kaynakta iki sözleşmeyi birleştiriyor, ama bu birleşme bir gerçekleştirim garantisi vermiyor. Bileşik arayüzü karşılayan bir tip, gömülen iki arayüzü de ayrı ayrı karşılıyor — bu yön kolay. Ama tersi doğru değil: iki ayrı tip, iki ayrı sözleşmeyi ayrı ayrı karşılayabiliyor, bileşik sözleşmeyi ise hiçbiri karşılamıyor. Önceki bölümdeki eşik ölçümü “kaç yöntem yeter” sorusunu bir tek tipin üzerinden sordu; bu ölçüm aynı soruyu birden çok tip üzerinden sorup, tipler arasında hiçbir paylaşımın olmadığını gösteriyor.
// iki-yon.go — bilesik sozlesmeyi karsilamanin iki yonu ayri ayri sinaniyor
package main
import "fmt"
type Okuyucu interface{ Oku() string }
type Yazici interface{ Yaz(m string) }
type OkuyupYazan interface {
Okuyucu
Yazici
}
type Dosya struct{}
func (Dosya) Oku() string { return "icerik" }
func (Dosya) Yaz(m string) {}
type SadeceOkuyan struct{}
func (SadeceOkuyan) Oku() string { return "a" }
type SadeceYazan struct{}
func (SadeceYazan) Yaz(m string) {}
func main() {
var d any = Dosya{}
_, okurMu := d.(Okuyucu)
_, yazarMi := d.(Yazici)
_, bilesikMi := d.(OkuyupYazan)
fmt.Println("ileri yon, Dosya Okuyucu mu :", okurMu)
fmt.Println("ileri yon, Dosya Yazici mi :", yazarMi)
fmt.Println("ileri yon, Dosya OkuyupYazan mi :", bilesikMi)
var okur any = SadeceOkuyan{}
var yazar any = SadeceYazan{}
_, okurTamam := okur.(Okuyucu)
_, yazarTamam := yazar.(Yazici)
_, okurBilesikMi := okur.(OkuyupYazan)
_, yazarBilesikMi := yazar.(OkuyupYazan)
fmt.Println("geri yon, SadeceOkuyan Okuyucu mu :", okurTamam)
fmt.Println("geri yon, SadeceYazan Yazici mi :", yazarTamam)
fmt.Println("geri yon, SadeceOkuyan OkuyupYazan mi:", okurBilesikMi)
fmt.Println("geri yon, SadeceYazan OkuyupYazan mi :", yazarBilesikMi)
}
ileri yon, Dosya Okuyucu mu : true ileri yon, Dosya Yazici mi : true ileri yon, Dosya OkuyupYazan mi : true geri yon, SadeceOkuyan Okuyucu mu : true geri yon, SadeceYazan Yazici mi : true geri yon, SadeceOkuyan OkuyupYazan mi: false geri yon, SadeceYazan OkuyupYazan mi : false
İleri yönde tek bir tip (Dosya) her üç arayüzü de karşılıyor — bileşik sözleşmeyi
karşılayan bir tip, parçalarını da otomatik olarak karşılıyor, çünkü bileşik sözleşmenin
yöntem kümesi parçaların üst kümesi. Dosyanın Oku ve Yaz yöntemlerini yazmış olması
tek başına yeterli; Okuyucuyu, Yaziciyi ve OkuyupYazanı ayrı ayrı karşılamak için üç
kez farklı kod yazmasına gerek kalmıyor.
Geri yönde durum farklı: SadeceOkuyan yalnız Okuyucuyu, SadeceYazan yalnız Yaziciyi
karşılıyor ve ikisi de doğru. Ama OkuyupYazanı karşılayan hiçbiri yok — ne
SadeceOkuyan, ne SadeceYazan. İki sözleşme ayrı ayrı karşılanmış olması, bileşik
sözleşmenin de karşılandığı anlamına gelmiyor; bunun için tek bir tipin her iki yöntemi
birden yazması gerekiyor. Programda SadeceOkuyan ve SadeceYazanın ikisi birden var olsa
bile, OkuyupYazan isteyen bir yer bu ikisinden hiçbirini kabul etmiyor — ikisinin
“toplamı” diye bir şey tip sisteminde yok.
AR12. SadeceOkuyan ile SadeceYazanın birlikte “bir yerlerde” Oku ve Yaz
yöntemlerinin ikisini de karşılıyor olması, programın hiçbir yerinde bir anlam taşımıyor —
Go’nun tip sistemi iki ayrı tipin yöntemlerini otomatik olarak birleştiren bir düzenek
sunmuyor. Gömme yalnız arayüz tanımını birleştiriyor; iki farklı somut tipin
davranışlarını birleştirmek isteyen bir üçüncü tip, kendi yöntemlerini kendisi yazmak ya da
K01’in Yapılar dersindeki gömme yoluyla iki somut tipi kendi içine almak zorunda.
Bu ayrım, yapı gömmesiyle arayüz gömmesi arasındaki temel farkı açıklıyor. Bir yapı başka bir yapıyı gömdüğünde, gömülü tipin gerçek bir değeri — kendi alanları ve yöntemleriyle birlikte — dış yapının içinde somut olarak duruyor; K01’in Yapılar dersinde gömülü yöntemlerin dış tipten yazılmadan çağrılabilmesinin nedeni buydu. Bir arayüz başka bir arayüzü gömdüğünde ise ortada hiçbir değer yok, yalnızca bir imza listesi birleşiyor. Arayüz gömmesi bir değer taşımıyor, yalnızca bir istek taşıyor; bu isteği kimin karşılayacağı ayrı bir soru, ve bu sorunun yanıtı yalnızca tek bir somut tipin kendi yöntemleriyle (ya da o tipin kendi yapı gömmeleriyle) verilebiliyor. İki gömme de aynı sözcüğü paylaşıyor çünkü ikisi de bir adı yazıp onun taşıdığı her şeyi kazanma fikrine dayanıyor; ama biri bir değeri taşırken öbürü yalnızca bir sözleşmeyi taşıyor.
Özet
- Arayüz gömme, bir arayüzün gövdesine başka arayüzlerin adlarını yazmaktan ibaret; bağ kaynakta açıkça duruyor, bu kursta bildirim sınıfının en açık örneği.
- Gömme ile kurulan bir sözleşme, aynı yöntemleri elle yeniden yazan bir sözleşmeyle
reflectdüzeyinde birebir aynı yöntem sayısını istiyor — gömme hiçbir yeni yöntem eklemiyor. - Bileşik bir arayüzü karşılamak için bir tipin yazması gereken en az yöntem sayısı, gömülen arayüzlerin toplamına eşit: bir eksik yetmiyor, tam sayı yetiyor.
- Bileşik arayüzü karşılayan bir tip gömülen arayüzlerin her birini de karşılıyor, ama tersi doğru değil: iki ayrı tip iki sözleşmeyi ayrı ayrı karşılayıp bileşik sözleşmeyi hiçbiri karşılamayabiliyor.
- Arayüz gömmesi bir değer taşımıyor, yalnızca bir sözleşme taşıyor; yapı gömmesi (K01’in Yapılar dersi) ise gerçek bir değeri taşıyor — aynı sözcük, iki ayrı düzenek.
Sonraki Adım
Bu derste sözleşmeyi tanımlayan taraf, sözleşmeyi karşılayacak tipten önce davranmıştı:
Okuyucu ve Yazici, Dosya yazılmadan önce zaten oradaydı. Ama üçüncü dersteki
Depolayici de, birinci dersteki Kisa da, karşıladıkları tipin kaynağında hiç
geçmiyordu — sözleşmeyi tanımlayan taraf, kullanan taraftı. Bu iki kalıp arasındaki fark bir
tesadüf değil, bir tercih: bazen sözleşmeyi üreten taraf yazar (bu ders), bazen tüketen
taraf (birinci ve üçüncü ders). Sıradaki ders bu tercihi bir tasarım kararına dönüştürüyor:
bir arayüzü kim tanımlamalı, üreten paket mi, tüketen taraf mı, ve bunun ölçülebilir bir
karşılığı var mı?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.