İçeriğe geç
academia.sh

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 reflect dü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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat