Ders 05 / 13
Arayüz Tasarımı
Tüketici tarafında tanımlama ölçülüyor: dört yöntemli bir tip hiçbir arayüze atıf yapmadan yazılıyor, dört ayrı tüketici birbirinden habersiz kendi tek yöntemli arayüzünü tanımlıyor ve aynı tip dördünü de karşılıyor — üreten tarafın kaynağında bu dört adın hiçbirine atıf yok. Sınırlayıcı ölçüm: aynı sözleşme iki tüketicide iki ayrı adla tanımlandığında birbirinin yerine geçemez görünüyor, ama tip sistemi ikisini de aynı değere doğrudan kabul ediyor.
İçindekiler
Önceki ders, sözleşmeyi tanımlayan tarafın sözleşmeyi karşılayacak tipten önce
davrandığı bir örnekle açıldı: Okuyucu ve Yazici, Dosya yazılmadan önce zaten
oradaydı. Ama kursun ilk dersindeki Kisa, üçüncü dersindeki Depolayici tam tersini
yaptı — ikisi de karşıladıkları tipin kaynağında hiç geçmedi. Bu ders o gözlemi bir
tasarım kararına dönüştürüyor: bir arayüzü kim tanımlamalı, üreten taraf mı, tüketen taraf
mı, ve bu seçimin ölçülebilir bir karşılığı var mı?
Bu soru bir tercih değil, birinci dersin ikinci iddiasının doğal uzantısı. Kaynak, bir tipin bir arayüzü karşıladığını hiç söylemiyorsa, o arayüzün üretici tarafın kaynağında bulunması gerekmiyor demektir — tüketici, üreticinin hiç bilmediği bir sözleşmeyi sonradan yazıp aynı tipe uygulayabiliyor. Bu ders bu imkânı bir kez daha, bu kez ölçekte ve bir bedelle birlikte gösteriyor: tek bir tip, birden çok bağımsız tüketici tarafından kaç ayrı sözleşmeyle sarılabiliyor, ve bu esnekliğin karşılığında hangi görünmezlik ortaya çıkıyor.
Tüketici Tarafında Tanımlama
Aşağıdaki ölçüm, dört yöntemli bir tipi (Depo) hiçbir arayüze atıf yapmadan yazıyor.
Ardından dört ayrı tüketici, birbirinden habersiz, kendi tek yöntemli arayüzünü
tanımlıyor. Ölçüm, Deponun bu dört arayüzden kaçını karşıladığını sayıyor.
// depo.go — uretici tarafin kodu, hicbir arayuze atif yapmadan yaziliyor
package main
import "fmt"
type Depo struct{ ogeler []string }
func (d *Depo) Ekle(o string) { d.ogeler = append(d.ogeler, o) }
func (d *Depo) Sil(o string) bool { return true }
func (d *Depo) Listele() []string { return d.ogeler }
func (d *Depo) Boyut() int { return len(d.ogeler) }
// Dort ayri tuketicinin kendi tanimladigi, birbirinden habersiz dort arayuz.
type Yazici interface{ Ekle(o string) }
type Temizleyici interface{ Sil(o string) bool }
type Raporlayici interface{ Listele() []string }
type Olculebilir interface{ Boyut() int }
type Yedekleyici interface{ Yedekle() error }
func main() {
var d any = &Depo{}
_, yaziciMi := d.(Yazici)
_, temizleyiciMi := d.(Temizleyici)
_, raporlayiciMi := d.(Raporlayici)
_, olculebilirMi := d.(Olculebilir)
_, yedekleyiciMi := d.(Yedekleyici)
sayisi := 0
for _, tamam := range []bool{yaziciMi, temizleyiciMi, raporlayiciMi, olculebilirMi, yedekleyiciMi} {
if tamam {
sayisi++
}
}
fmt.Println("Yazici mi :", yaziciMi)
fmt.Println("Temizleyici mi :", temizleyiciMi)
fmt.Println("Raporlayici mi :", raporlayiciMi)
fmt.Println("Olculebilir mi :", olculebilirMi)
fmt.Println("Yedekleyici mi :", yedekleyiciMi)
fmt.Println("bes adaydan karsilanan sayisi:", sayisi)
}
Yazici mi : true Temizleyici mi : true Raporlayici mi : true Olculebilir mi : true Yedekleyici mi : false bes adaydan karsilanan sayisi: 4
Deponun kaynağı dört satırdan ibaret: Ekle, Sil, Listele, Boyut. Bu dört satırda
Yazici, Temizleyici, Raporlayici, Olculebilir adlarının hiçbiri geçmiyor — Depo
yazılırken bu dört arayüz henüz var olmayabilirdi bile. Buna rağmen Depo dördünü de
karşılıyor, çünkü karşılamanın koşulu yalnızca yöntem imzalarının eşleşmesi. Beşinci aday
(Yedekleyici) bilinçli olarak eklendi: Deponun yazmadığı bir yöntem (Yedekle)
istiyor ve false dönüyor — ölçüm yalnız uyanları değil, uymayanı da gösteriyor, ki
sonuç seçici bir örnekleme değil gerçek bir sınır olsun.
Dört arayüzün her biri farklı bir tüketici senaryosunu temsil ediyor: Yazici yalnızca
yeni öge ekleyen bir kod parçasının ihtiyacı, Temizleyici yalnızca silme yapan bir
işlevin, Raporlayici yalnızca listeleme yapan bir görünümün, Olculebilir yalnızca
boyut sınayan bir denetimin. Bu dört tüketici birbirini hiç görmemiş olabilir, ayrı
paketlerde, ayrı ekipler tarafından yazılmış olabilir — tek ortak noktaları Deponun
kendisi. Yine de dördü de kendi dar sözleşmesini kurup Depoyu ona uydurabiliyor, çünkü
uyum kontrolü merkezî bir kayıt yerine her tüketicinin kendi bakış açısından yapılıyor.
AR13. Dört tüketici arayüzü tek bir yöntem istiyor; bu bilinçli bir seçim. Kursun
ilk dersindeki küçük arayüz ölçümü, yöntem sayısı arttıkça karşılayan tip sayısının
düştüğünü göstermişti. Burada tersi kullanılıyor: yöntem sayısını bire indirmek,
Deponun tek bir yöntemi olan her tüketiciyi karşılamasını neredeyse garantiliyor —
tüketici tarafında tanımlamanın pratikte bu kadar sık işlemesinin bir nedeni de bu.
Bu düzende üreten paket bir kez yazılıyor ve hiç değişmiyor; her yeni tüketici kendi
arayüzünü kendi ihtiyacına göre tanımlıyor. Kursun terminolojisinde bu, arayüzün
karşılanmasının bağını kullanım yerine taşıyan aynı mekanizma — birinci dersteki
Kisanın ve üçüncü dersteki Depolayicinin yaptığı da tam olarak buydu. Fark, bu
dersin onu tek bir örnekten çıkarıp bir tasarım gözlemine dönüştürmesi: dört tüketici,
dört farklı ihtiyaç, dört farklı ad, ve üretici tarafın bunların hiçbirinden haberi yok.
Tasarım İlkeleri kursunun arayüz ayrımı ilkesi bu deseni bir öğüt olarak sunuyor; bu ders
öğüt vermiyor, deseni sayıyor.
Sayının kendisi de bir şey söylüyor: Depo dört arayüzü karşılıyor, ama Deponun kendi
kaynağında dört değil sıfır arayüz adı geçiyor. Deponun kaynağı hiç değişmeden,
Deponun mevcut dört yönteminin herhangi bir alt kümesini isteyen yeni bir arayüz her an
tanımlanabilir ve Depo onu da karşılamaya devam eder — beşinci, altıncı, onuncu
tüketici için de aynı kural geçerli, üretici tarafın kaynağının bundan haberi olması
gerekmiyor. Depoyu yazan taraf, kaç tüketicinin kendisini kaç farklı sözleşmeyle
sardığını bilmek zorunda değil.
Tüketicinin Kendi Bileşiği
Dördüncü ders gömmenin sözleşmeleri üreten tarafta birleştirdiğini ölçmüştü:
Okuyucu ve Yazici, Dosya yazılmadan önce zaten Gomulu içinde birleşmişti. Aynı
birleşme tüketici tarafında da mümkün — yalnız bu kez birleştiren taraf üretici değil,
tüketicinin kendisi.
// bilesik-tuketici.go — tuketici, uretici tipin dort yontemini kendi sozlesmesinde topluyor
package main
import "fmt"
type Depo struct{ ogeler []string }
func (d *Depo) Ekle(o string) { d.ogeler = append(d.ogeler, o) }
func (d *Depo) Sil(o string) bool { return true }
func (d *Depo) Listele() []string { return d.ogeler }
func (d *Depo) Boyut() int { return len(d.ogeler) }
type Yazici interface{ Ekle(o string) }
// Besinci tuketici, dort yontemi kendi bilesik sozlesmesinde topluyor; gomme kullanmiyor,
// dort imzayi elle yaziyor.
type TamDepo interface {
Ekle(o string)
Sil(o string) bool
Listele() []string
Boyut() int
}
func main() {
var d any = &Depo{}
_, yaziciMi := d.(Yazici)
_, tamDepoMu := d.(TamDepo)
fmt.Println("dar sozlesme, Yazici mi :", yaziciMi)
fmt.Println("genis sozlesme, TamDepo mu :", tamDepoMu)
}
dar sozlesme, Yazici mi : true genis sozlesme, TamDepo mu : true
TamDepo, Deponun dört yönteminin tamamını tek bir sözleşmede topluyor, ama bunu
gömerek değil, dördünü tek tek yazarak yapıyor — TamDepo kendi başına bir gömme
zinciri değil. Buna rağmen Depo onu da karşılıyor, tıpkı tek yöntemli Yaziciyi
karşıladığı gibi. Dördüncü dersteki ölçüm gömmenin üretici tarafın sözleşmesini nasıl
birleştirdiğini gösterdi; bu ölçüm aynı birleşmenin tüketici tarafında, üreticinin hiç
haberi olmadan da kurulabildiğini gösteriyor. İki model burada çakışmıyor, aynı tipte yan
yana duruyor: Depo hem en dar sözleşmeyi (Yazici) hem en geniş sözleşmeyi (TamDepo)
aynı anda karşılıyor, çünkü ikisi de aynı dört yöntemin farklı alt kümelerini istiyor.
AR14. TamDeponun Deponun tam olarak yazdığı dört yöntemi istemesi bilinçli bir
seçim — sözleşme üreticinin yöntem kümesini aşsaydı, Depo onu karşılamazdı. Ölçümün
gösterdiği şey sınırsız bir uyum değil: tüketici ne kadar geniş bir sözleşme yazarsa
yazsın, üreticinin gerçekte yazdığı yöntemlerin dışına çıkamıyor.
Bu, tüketici tarafında tanımlamanın gerçek sınırını netleştiriyor: tüketici, üreticinin
neyi yazdığını seçemiyor, yalnızca üreticinin yazdıklarından hangisini istediğini
seçebiliyor. TamDepo beşinci bir yöntem (Yedekle) isteseydi, dördüncü bölümdeki
Yedekleyici gibi karşılanmazdı — geniş bir sözleşme yazmak, üretici tarafın kaynağına
yeni bir yöntem eklemenin yerine geçmiyor. Tüketici tarafında tanımlamanın verdiği
özgürlük, var olan yöntemlerin seçimi üzerinedir, var olmayan yöntemlerin
icadı üzerinde değil.
Sınırlayıcı Ölçüm: Aynı Sözleşme, İki Ayrı Ad
Tüketici tarafında tanımlama bedelsiz değil. İki ayrı tüketici aynı yöntemi isteyen iki ayrı arayüz tanımladığında, bu iki arayüz kaynakta hiçbir ortak noktaya sahip görünmüyor — ikisi de kendi paketinde, kendi adıyla, birbirinden habersiz duruyor. Ama Go’nun tip sistemi arayüzleri adına göre değil yöntem kümesine göre karşılaştırıyor; bu yüzden iki ayrı ad, aynı imzayı taşıdığı sürece birbirinin yerine geçiyor.
// iki-ad.go — ayni imzayi tasiyan iki ayri arayuz adi, birbirinin yerine geciyor
package main
import "fmt"
type Depo struct{ ogeler []string }
func (d *Depo) Ekle(o string) { d.ogeler = append(d.ogeler, o) }
// Iki tuketicinin birbirinden habersiz tanimladigi, ayni imzali iki arayuz.
type Yazici interface{ Ekle(o string) }
type Girdi interface{ Ekle(o string) }
func birinciTuketici(y Yazici) string { y.Ekle("A"); return "Yazici uzerinden eklendi" }
func ikinciTuketici(g Girdi) string { g.Ekle("B"); return "Girdi uzerinden eklendi" }
func main() {
d := &Depo{}
var g Girdi = d
var y Yazici = g // Girdi tipindeki bir deger dogrudan Yazici degiskenine atanabiliyor mu
fmt.Println("Girdi degeri Yazici degiskenine atanabildi mi:", y != nil)
fmt.Println(birinciTuketici(g)) // Girdi degeri Yazici parametresine geciyor
fmt.Println(ikinciTuketici(y)) // Yazici degeri Girdi parametresine geciyor
fmt.Println("toplam eklenen oge sayisi:", len(d.ogeler))
}
Girdi degeri Yazici degiskenine atanabildi mi: true Yazici uzerinden eklendi Girdi uzerinden eklendi toplam eklenen oge sayisi: 2
Yazici ile Girdi, iki ayrı ad taşıyan, birbirinden habersiz yazılmış iki ayrı arayüz.
Aralarında hiçbir gömme, hiçbir ortak tanım yok. Buna rağmen var y Yazici = g satırı
derleniyor — bir Girdi değeri doğrudan bir Yazici değişkenine atanabiliyor — ve
birinciTuketici, Yazici beklerken bir Girdi değeri kabul ediyor. Fark yalnızca adda;
Go’nun tip sistemi arayüzleri isimleriyle değil, istedikleri yöntemlerle tanımlıyor. İki
farklı tüketici aynı yöntemi isteyen iki farklı arayüz tanımladığında, bu iki tanım
istemeden eşdeğer oluyor.
AR15. Bu satır kaynaktan okunarak tahmin edilemez: Yazici ve Girdi tanımlarına
tek tek bakan biri, aralarında bir ilişki olduğunu göremiyor, çünkü ilişki hiçbir yerde
yazılı değil — yalnızca ikisinin yöntem kümesi karşılaştırıldığında ortaya çıkıyor.
Ölçüm bu köru noktayı doğrudan koşturarak gösteriyor; kaynağı okumak yetmiyor, iki
tanımı yan yana koyup imzaları karşılaştırmak gerekiyor.
Bu eşdeğerliğin sınırı da net: imzalar birebir aynı olmak zorunda, yalnız benzemesi yetmiyor. Aşağıdaki ölçüm bunu doğrudan derleyiciye soruyor.
// sinir.go — ayni imza gecerli kiliyor, farkli donus tipi reddediyor
package main
import (
"fmt"
"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
}
func main() {
ortak := `package sinama
type Yazici interface{ Ekle(o string) }
func f(y Yazici) {}
`
ayniImza := ortak + `type Girdi interface{ Ekle(o string) }
func g(x Girdi) { f(x) }
`
farkliImza := ortak + `type Aktarici interface{ Ekle(o string) error }
func g(x Aktarici) { f(x) }
`
fmt.Println("ayni imzali arayuz gecerli mi :", derlenirMi(ayniImza))
fmt.Println("farkli imzali arayuz gecerli mi:", derlenirMi(farkliImza))
}
ayni imzali arayuz gecerli mi : true farkli imzali arayuz gecerli mi: false
Girdi, Yazici ile birebir aynı imzayı (Ekle(string)) taşıdığı için Yazici
beklenen yerde kabul ediliyor. Aktarici ise aynı adı (Ekle) taşısa da farklı bir
dönüş tipi (error) istiyor, ve bu fark tek başına eşdeğerliği bozmaya yetiyor —
derleniyor mu sorusu falsee dönüyor. Eşdeğerlik ada değil, imzanın her parçasına
bakıyor: yöntem adı, parametre tipleri, dönüş tipleri, hepsi birden eşleşmek zorunda.
İki tüketicinin bağımsız yazdığı arayüzlerin örtük olarak eşleşmesi, ancak ikisi de
aynı sözleşmeyi harfiyen tekrarladığında oluyor; küçük bir fark bile bu örtük bağı
koparıyor.
Bu, tüketici tarafında tanımlamanın maliyetidir: bağımsız tüketiciler bağımsız sözleşmeler yazdığında, aynı sözleşmenin birden çok kez, birbirinden habersiz tekrarlanması hiçbir yerde engellenmiyor. Sonuç yanlış değil — iki arayüz gerçekten eşdeğer ve tip sistemi bunu doğru işliyor — ama kaynağı okuyan biri için görünmez bir eşdeğerlik bu. Dört arayüzün her biri kendi tüketicisine göre en dar sözleşmeyi kurduğu için yararlı olan aynı özgürlük, iki tüketici aynı ihtiyacı ayrı ayrı tanımladığında fazladan bir sözleşmeye dönüşüyor.
Özet
- Tüketici tarafında tanımlamada, üreten taraf hiçbir arayüz yazmadan bir tip üretiyor ve tüketen taraf kendi arayüzünü kendi ihtiyacına göre tanımlıyor.
Depodört tüketici arayüzünün dördünü de karşılıyor, kaynağında bu dört adın hiçbirine atıf yok; beşinci bir aday (Yedekleyici) karşılanmayarak sınırı gösteriyor.- Küçük, tek yöntemli tüketici arayüzleri bu deseni pratikte kolaylaştırıyor — yöntem sayısı düştükçe karşılanma olasılığı artıyor.
- Go’nun arayüz karşılaştırması ada değil yöntem kümesine bakıyor; bu yüzden aynı imzayı taşıyan iki ayrı adlı arayüz, kaynakta hiçbir bağlantı yazılı olmasa da birbirinin yerine geçiyor.
- Bu, tüketici tarafında tanımlamanın bedelidir: bağımsız tüketiciler aynı sözleşmeyi habersizce yineleyebiliyor, ve bu yineleme yalnızca imzalar karşılaştırıldığında görünür oluyor.
- Eşdeğerliğin sınırı kesin: yöntem adı aynı olsa bile parametre ya da dönüş tipi farklıysa, iki arayüz birbirinin yerine geçmiyor; derleyici bu farkı doğrudan reddediyor.
Sonraki Adım
Arayüzü tüketicinin tanımlaması, bu kurs boyunca ölçülen bir gerçeğin doğal sonucu: bağ kaynakta hiç yazılmıyorsa, sözleşmeyi kimin yazdığı da üretici tarafın kararına bağlanmıyor. Beş ders boyunca ölçülen çerçeve (bildirim, kullanım yeri, değer) burada da geçerliydi: gömme bildirimde, tüketici tarafında tanımlama kullanım yerinde, ikisi de aynı örtük gerçekleştirim ilkesinin iki farklı yüzü. Sıradaki konu bu modelin en çok kullanıldığı yere bakıyor. Go’da hata bir arayüzdür, ve her hata değeri onu örtük karşılar — üç yöntemli, dört yöntemli bir sözleşme değil, tek bir yöntemli en dar sözleşme. Bir sonraki dersin sorusu: bir hata değerinin kimliği nerede duruyor, ve bir hatayı hata yapan şey nedir?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.