Bölüm 6 / 15
SOLID: Interface Segregation
Arayüz ayrımı ilkesi.
12 dakikalık okuma
Önceki ders, NotSupportedException fırlatan bir
uygulamayı gördüğünde teşhisi koymuştu: sorun uygulamada değil, arayüzün fazla
geniş olmasındadır. ISP o teşhisin ilkesidir. Konu yalnız estetik değil: geniş
arayüz, birbirine hiç benzemeyen kodları birbirine bağlar — bir metot eklediğinde o
metotla ilgisi olmayan on sınıf derlenmez olur.
01Ne öğreneceksin
- ISP'nin tanımı ve "istemci" kelimesinin buradaki teknik anlamı
- Şişman arayüzün üç somut maliyeti: sahte uygulama, yeniden derleme, test kurulumu
- İhlal belirtileri ve arayüzü bölme teknikleri — rol arayüzleri
- Arayüzün sahibi kimdir: uygulayan mı, çağıran mı
- .NET'teki ayrım aileleri:
IEnumerable→IList→IReadOnlyList - Açık arayüz uygulaması, varsayılan arayüz üyeleri ve DI'da tek sınıfı iki arayüzle kaydetme
- Sınır: arayüz enflasyonu ve ISP ile SRP'nin farkı
02Tanım
Robert C. Martin'in ifadesiyle:
"Hiçbir istemci, kullanmadığı metotlara bağımlı olmaya zorlanmamalıdır."
Buradaki istemci, arayüzü çağıran koddur — onu uygulayan sınıf değil. İlke iki yönlü çalışır ve iki farklı acıyı hedefler:
- Çağıran tarafta: yalnızca
Kaydetmetoduna ihtiyaç duyan bir sınıf, 14 metotluk bir arayüzü parametre olarak alıyorsa, o 14 metodun her değişikliğine bağımlı hâle gelir. Testte de o sınıfın sahtesini kurmak için 14 metodun hepsini karşılamak gerekir. - Uygulayan tarafta: sınıf, doğal olarak yapamadığı işleri de üstlenmek zorunda kalır ve bunları boş gövdeyle ya da istisnayla "kapatır" — yani LSP'yi ihlal eder.
İki taraf aynı kökten gelir: arayüz, gerçekte birlikte kullanılmayan işleri tek pakette toplamıştır. Bir arayüzün doğru boyutu, onu uygulayan sınıfın büyüklüğüne göre değil, çağıranların ihtiyacına göre belirlenir.
03Şişman arayüz problemi
Tipik ihlal, "bir kavramın yapabileceği her şeyi" tek arayüzde toplamakla başlar:
public interface IOfisCihazi
{
void Yazdir(Belge b);
void Tara(Belge b);
void Faksla(Belge b);
void CiftYuzluYazdir(Belge b);
void ZimbalaVeTeslimEt();
}
Çok fonksiyonlu bir cihaz için sorun yok. Ama basit bir yazıcı bu arayüzü uygulamak zorunda kalınca ortaya şu çıkar:
public sealed class BasitYazici : IOfisCihazi
{
public void Yazdir(Belge b) { /* gerçek iş */ }
public void Tara(Belge b) => throw new NotSupportedException(); // LSP ihlali
public void Faksla(Belge b) => throw new NotSupportedException(); // LSP ihlali
public void CiftYuzluYazdir(Belge b) => Yazdir(b); // sessizce yanlış
public void ZimbalaVeTeslimEt() { } // boş gövde: en tehlikelisi
}
Üç ayrı kusur bir arada: istisna fırlatan uygulama sözleşmeyi reddediyor, tek yüzlü yazdırma "çift yüzlü" adıyla sunuluyor, boş gövde ise hatayı tamamen görünmez kılıyor — çağıran zımbalandığını sanıyor. Bunların üçü de arayüz doğru boyutlanmış olsaydı yazılamazdı.
Şişman arayüzün üç ölçülebilir maliyeti vardır:
- Sahte uygulama zorunluluğu. Yeni bir uygulama eklemek, işe yaramayan metotları da doldurmayı gerektirir. Bu kod hiçbir zaman doğru olmaz; yalnızca derleyiciyi susturur.
- Yeniden derleme yayılması. Arayüze bir metot eklediğinde, o metotla hiçbir ilgisi olmayan tüm uygulamalar derlenmez hâle gelir. Bu, kaynak kodda görünmeyen ama derleme zamanında çok pahalıya patlayan bir bağlantıdır; büyük çözümlerde tek satırlık bir arayüz değişikliği onlarca dosyayı açtırır.
- Test kurulum maliyeti. Bir testin ihtiyacı olan tek şey
Yazdirise, sahte nesnede beş metodu da tanımlamak gürültüdür. Testte hissedilen zorluk, tasarımdaki hatanın en erken habercisidir — 15. hatta bu sinyali sistemli biçimde kullanacaksın.
04İhlal belirtileri
NotSupportedException,throw new NotImplementedException()ya da boş gövdeli metotlar içeren uygulamalar.- Arayüz adında "Manager", "Service", "Helper" gibi genel sözcükler ve 8-10 üyeden fazlası. Sayı tek başına ölçüt değildir, ama hiçbir çağıranın üyelerin yarısından fazlasını kullanmaması ölçüttür.
- Bir metot eklendiğinde ilgisiz sınıfların derlenmemesi.
- Testlerde sürekli "bu metodu kullanmıyoruz ama doldurmak zorundayız" yorumları ya da devasa sahte sınıflar.
- Uygulamaların bir kısmının belgelenmiş "yalnızca şu metotları çağırın" notuyla gelmesi — yazılmamış bir alt arayüzün itirafıdır.
- Çağıranın aldığı bağımlılığın adı ile o bağımlılıktan kullandığı tek metot
arasındaki uçurum:
IKullaniciYonetimialıp yalnızAdiGetirçağırmak gibi.
05Ayırma: rol arayüzleri ve arayüzün sahibi
Çözüm, arayüzü rollere bölmektir. Rol, "bu tipin ne olduğu" değil, "bu tipin hangi işte kullanıldığı"dır:
public interface IYazdirabilir { void Yazdir(Belge b); }
public interface ITarayabilir { void Tara(Belge b); }
public interface IFakslayabilir { void Faksla(Belge b); }
public sealed class BasitYazici : IYazdirabilir
{
public void Yazdir(Belge b) { /* ... */ }
}
public sealed class CokFonksiyonluCihaz : IYazdirabilir, ITarayabilir, IFakslayabilir
{
public void Yazdir(Belge b) { /* ... */ }
public void Tara(Belge b) { /* ... */ }
public void Faksla(Belge b) { /* ... */ }
}
Kazanç uygulamada değil, çağıran tarafta görünür:
// Önce: neye ihtiyacı olduğu belli değil, hepsine bağımlı
public FaturaYazdirici(IOfisCihazi cihaz) { ... }
// Sonra: imza, ihtiyacı da sınırı da söylüyor
public FaturaYazdirici(IYazdirabilir yazici) { ... }
İkinci imza bir belge hâline gelir: bu sınıf tarama yapmaz, faks çekmez —
derleyici bunu garanti eder. Testte sahte nesne tek metotludur. Ve
ITarayabilir'e yarın bir metot eklendiğinde FaturaYazdirici
hiç etkilenmez.
Bölerken kullanılan pratik teknikler:
- Okuma/yazma ayrımı. En sık işe yarayan bölme çizgisidir:
IOkuyucu/IYazici. Çağıranların çoğu yalnız okur; okuyucu bağımlılığı alan bir sınıf veriyi değiştiremeyeceğini de ilan etmiş olur. - Komut/sorgu ayrımı. Durum değiştiren işlemlerle sorgu işlemlerini ayrı arayüzlere koymak; 19. hattaki CQRS fikrinin küçük ölçekli hâlidir.
- Çağıran başına arayüz. Uçta kalan bir teknik: arayüzü çağıranın ihtiyacına göre çağıranın katmanında tanımlamak. Bir sonraki dersin (Dependency Inversion) tam merkezinde bu vardır.
Arayüzün sahibi kimdir? Kıdemli tasarımı acemi tasarımdan ayıran soru
budur. Yaygın alışkanlık, arayüzü
uygulamanın yanına koymaktır: SqlKullaniciDeposu yazılır, üstüne
"arayüz olsun" diye IKullaniciDeposu çıkarılır. Bu, arayüzü
uygulamanın gölgesi yapar — uygulama değişince arayüz de değişir, hiçbir şey
korunmuş olmaz.
ISP'nin işaret ettiği düzen tersidir: arayüzü, ona ihtiyaç duyan taraf tanımlar. İhtiyaç "kullanıcıyı e-postasıyla bul" ise arayüz tek metotludur; uygulama tarafındaki sınıfın 20 metodu olması çağıranı ilgilendirmez.
// Çağıranın katmanında tanımlanır — ihtiyaç kadar
public interface IKullaniciBulucu
{
Task<Kullanici?> EpostaIleBul(string eposta, CancellationToken ct);
}
// Altyapı katmanındaki tek sınıf, birden çok rolü karşılayabilir
public sealed class SqlKullaniciDeposu : IKullaniciBulucu, IKullaniciYazici { /* ... */ }
DI kaydında aynı örneği iki arayüz altında sunmak istersen, ikinci kaydı
yönlendirme ile yaparsın — yoksa iki ayrı nesne oluşur ve
scoped bir sınıfta bu sessiz bir hata kaynağıdır:
services.AddScoped<SqlKullaniciDeposu>();
services.AddScoped<IKullaniciBulucu>(sp => sp.GetRequiredService<SqlKullaniciDeposu>());
services.AddScoped<IKullaniciYazici>(sp => sp.GetRequiredService<SqlKullaniciDeposu>());
Bu kayıt biçimini 5. hatta ayrıntısıyla göreceksin; buradaki tasarım gerekçesi şudur: tek uygulama, birden çok küçük sözleşme. Uygulamayı bölmek zorunda değilsin — bölünmesi gereken şey arayüzdür.
06.NET'te ayrım aileleri ve dil araçları
.NET'in koleksiyon arayüzleri ISP'nin ders kitabı örneğidir; her biri bir öncekine yetenek ekler ve çağıran ihtiyacı kadarını ister:
IEnumerable<T> // yalnız üzerinde dolaşırım
IReadOnlyCollection<T> // + Count
IReadOnlyList<T> // + [i] ile erişim
ICollection<T> // + Add / Remove / Clear
IList<T> // + [i] ile yazma, Insert, RemoveAt
Pratik kural: parametrede en dar, dönüşte en açıklayıcı tipi seç. Bir metot
yalnız dolaşacaksa parametresi IEnumerable<T> olsun — böylece
dizi, liste, HashSet ve LINQ sorgusu hepsi geçebilir. Ama iki uyarı
vardır: IEnumerable tembel olabileceği için birden fazla dolaşmak
sorguyu iki kez çalıştırır (3. hattaki LINQ dersi), ve elemana indeksle erişmen
gerekiyorsa IReadOnlyList<T> daha dürüst bir imzadır. Dönüşte ise
IReadOnlyList<T> hem "bu koleksiyonu değiştirme" der hem
Count verir.
Aynı ayrım IReadOnlyList'in doğuş sebebini de açıklar:
IList<T> uygulayıp Add'i reddetmek yerine, salt okunur
olan tipin dürüst bir arayüz uygulaması sağlanmıştır. Bu, önceki dersin
NotSupportedException örneğinin resmî çözümüdür.
İki dil aracı ISP'yi uygularken işini kolaylaştırır:
Açık arayüz uygulaması (explicit implementation). Bir üyeyi yalnızca o arayüz üzerinden erişilebilir yapar; sınıfın genel yüzeyini kirletmez:
public sealed class Sayac : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator() { /* ... */ }
// Eski, tipsiz sürüm gizlenir: sayac.GetEnumerator() yalnız jenerik olanı çağırır
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
İki arayüz aynı imzayı istediğinde de tek çözüm budur. Dikkat: açık uygulanan üye
yalnız arayüz tipi üzerinden çağrılabilir; bir struct'ta bu, arayüze
dönüştürme (boxing) maliyeti doğurur.
Varsayılan arayüz üyeleri (C# 8). Arayüze gövdeli bir üye eklemeye izin verir; asıl amacı, yayınlanmış bir arayüze mevcut uygulamaları kırmadan üye eklemektir:
public interface IBildirici
{
void Gonder(string mesaj, Oncelik oncelik);
void Gonder(string mesaj) => Gonder(mesaj, Oncelik.Normal); // varsayılan gövde
}
Ölçülü kullan. Bu üye yalnız arayüz tipi üzerinden görünür (sınıf referansından çağrılamaz) ve arayüzü davranış taşıyan bir yapıya dönüştürür. Şişman arayüzü meşrulaştırmak için kullanılmaz — sorunun çözümü yine bölmektir.
07Sınır: arayüz enflasyonu ve ISP ile SRP farkı
Her ilke gibi ISP de aşırıya kaçabilir. Her metot için ayrı arayüz üretmek, SRP dersindeki sınıf enflasyonunun arayüz hâlidir: yüzlerce tek metotlu tip, bağlantı kurmak için sürekli açılıp kapanan dosyalar ve DI kayıt listesi. Ölçüt sayı değil, birlikte kullanılmadır: birden çok çağıran hep aynı üç metodu birlikte istiyorsa, o üçü bir arayüzdür.
Ayrıca tek uygulaması olan ve olması da beklenmeyen bir sınıf için arayüz çıkarmak, önceki derste konuştuğumuz YAGNI ihlalidir. Arayüzü şu üç durumda çıkar: gerçekten birden fazla uygulama var; testte yerine geçecek bir sahte gerekiyor; ya da arayüz bir katman sınırını işaretliyor (bir sonraki dersin konusu). Aksi hâlde somut sınıfı doğrudan kullan.
ISP ile SRP karıştırılır, farkı şudur: SRP uygulamanın sorumluluklarıyla ilgilenir — sınıf kaç sebeple değişiyor? ISP sözleşmenin genişliğiyle ilgilenir — çağıran neye bağımlı olmak zorunda kalıyor? İkisi çoğu zaman aynı yöne işaret eder, ama bağımsızdırlar: tek sorumluluklu bir sınıfın arayüzü hâlâ fazla geniş olabilir (bütün CRUD'u tek arayüzde sunan bir depo gibi), ve dar arayüzler şişman bir sınıfın üzerine de takılabilir.
Sıradaki ders Dependency Inversion: bu derste "arayüzü çağıran tanımlar" dediğimiz cümlenin mimari karşılığı. Orada bağımlılık okunun yönünü tersine çevirip iş kurallarını altyapıdan tamamen ayıracağız — ve bunun .NET'teki aracı olan bağımlılık enjeksiyonuyla ilkenin farkını netleştireceğiz.
08Mini alıştırma — Şişman arayüzü böl
Yaklaşık 20 dakika · dotnet run
- İhlali kur:
IOfisCihaziarayüzünü beş üyeyle yaz,BasitYaziciile uygula. Kullanmadığın üyeleri bilerek boş gövdeyle bırak.ZimbalaVeTeslimEt()çağıran bir kod yaz ve programın hiçbir şey olmamış gibi devam ettiğini gör. - Maliyeti ölç: arayüze
void Renkli Yazdir(Belge b)ekle. Kaç dosya derlenmez oldu? Bu metotla gerçekten ilgisi olan kaç sınıf vardı? - Böl:
IYazdirabilir,ITarayabilir,IFakslayabilirarayüzlerini çıkar.BasitYaziciyalnız birincisini,CokFonksiyonluCihazüçünü uygulasın. 2. adımdaki eklemeyi tekrarla ve etkilenen dosya sayısını karşılaştır. - Çağıranı düzelt:
FaturaYazdiricisınıfının kurucusunuIOfisCihazi'ndanIYazdirabilir'e çevir. İmzanın tek başına ne anlattığını bir cümleyle yaz. - Sahte nesne farkı: her iki sürüm için elle bir sahte sınıf yaz
(
SahteCihaz/SahteYazici). Satır sayılarını karşılaştır. - Okuma/yazma ayrımı:
IKullaniciBulucuveIKullaniciYaziciarayüzlerini tanımla, ikisini de tek birBellekKullaniciDeposusınıfıyla uygula. Yalnız okuyan bir rapor sınıfı yaz ve içinden kayıt eklemeye çalış — derleyicinin engellediğini gör. - Açık uygulama (isteğe bağlı): aynı imzaya sahip iki arayüzü
(
IEskiKayit.Kaydet()/IYeniKayit.Kaydet()) tek sınıfta açık uygulamayla karşıla ve her ikisini arayüz tipleri üzerinden çağır.
09Kendini sına
ISP nedir, buradaki "istemci" kimdir?
ISP, hiçbir istemcinin kullanmadığı metotlara bağımlı olmaya zorlanmaması gerektiğini söyler. İstemci, arayüzü çağıran koddur — onu uygulayan sınıf değil. Bu yüzden bir arayüzün doğru boyutu, onu uygulayan sınıfın büyüklüğüne göre değil, çağıranların ihtiyacına göre belirlenir. İlke iki acıyı birden hedefler: çağıran taraf kullanmadığı üyelerin her değişikliğine bağımlı hâle gelir; uygulayan taraf yapamadığı işleri boş gövde ya da NotSupportedException ile kapatmak zorunda kalır, bu da LSP ihlalidir. İkisinin ortak kökü, gerçekte birlikte kullanılmayan işlerin tek pakette toplanmasıdır.
Şişman arayüzün somut maliyetleri neler?
Üç tanesi ölçülebilir. (1) Sahte uygulama zorunluluğu: yeni bir uygulama, işe yaramayan metotları da doldurmak zorundadır; bu kod hiçbir zaman doğru olmaz, yalnız derleyiciyi susturur — boş gövde en tehlikelisidir, çünkü hatayı tamamen görünmez kılar. (2) Yeniden derleme yayılması: arayüze bir üye eklendiğinde o üyeyle hiç ilgisi olmayan bütün uygulamalar derlenmez olur; kaynak kodda görünmeyen, derleme zamanında pahalıya patlayan bir bağlantıdır. (3) Test kurulum maliyeti: testin ihtiyacı tek metotken sahte nesnede beşini birden tanımlamak gerekir. Testte hissedilen bu zorluk, tasarım hatasının en erken habercisidir.
Arayüzü kim tanımlamalı — uygulayan mı, çağıran mı?
Çağıran. Yaygın alışkanlık önce somut sınıfı yazıp üstünden arayüz "çıkarmaktır"; bu, arayüzü uygulamanın gölgesi yapar — uygulama değişince arayüz de değişir ve hiçbir şey korunmuş olmaz. Doğru düzende arayüzü ona ihtiyaç duyan taraf, kendi katmanında ve ihtiyacı kadar tanımlar: ihtiyaç "kullanıcıyı e-postasıyla bul" ise arayüz tek metotludur, uygulama sınıfının 20 metodu olması çağıranı ilgilendirmez. Altyapıdaki tek sınıf birden çok küçük rolü aynı anda karşılayabilir; DI'da aynı örneği ikinci bir arayüz altında sunmak için GetRequiredService ile yönlendirme kaydı yapılır, yoksa iki ayrı nesne oluşur ve scoped kayıtlarda bu sessiz bir hata kaynağıdır.
.NET koleksiyon arayüzleri ISP'yi nasıl örnekler? Parametre tipi nasıl seçilir?
IEnumerable, IReadOnlyCollection, IReadOnlyList, ICollection ve IList bir merdiven oluşturur; her biri bir öncekine yetenek ekler ve çağıran ihtiyacı kadarını ister. IReadOnlyList'in var olma sebebi tam olarak budur: salt okunur bir tipin IList uygulayıp Add'i NotSupportedException ile reddetmesi yerine dürüst bir sözleşme sunulmuştur. Pratik kural parametrede en dar, dönüşte en açıklayıcı tipi seçmektir: yalnız dolaşan bir metot IEnumerable alsın, böylece dizi, liste, HashSet ve LINQ sorgusu geçebilir. İki uyarı: IEnumerable tembel olabildiği için birden fazla dolaşmak sorguyu tekrar çalıştırır, ve indeksle erişim gerekiyorsa IReadOnlyList daha dürüst bir imzadır. Dönüşte IReadOnlyList hem "değiştirme" der hem Count verir.
10Sınav köşesi
Yazılıda nasıl sorulur
(1) Tanım: "Interface Segregation Principle nedir?" — cevapta "istemci"nin
çağıran kod olduğu mutlaka geçmeli. (2) Kod okuma: beş üyeli bir arayüz ve onu
NotSupportedException'larla uygulayan bir sınıf verilip hangi
ilkelerin ihlal edildiğinin (ISP ve LSP birlikte) ve nasıl düzeltileceğinin
yazılması. (3) Ayırt etme: "ISP ile SRP arasındaki fark nedir?" — SRP uygulamanın
değişme sebepleriyle, ISP sözleşmenin genişliğiyle ilgilenir. (4) .NET bilgisi:
IList<T> ile IReadOnlyList<T> farkı ve
IReadOnlyList'in neden eklendiği; bir metodun parametresi için hangi
koleksiyon arayüzünün seçileceği. (5) Dil detayı: açık arayüz uygulaması
(explicit implementation) ne zaman zorunludur; varsayılan arayüz üyeleri niçin
eklendi. (6) Doğru/yanlış: "her sınıf için arayüz çıkarmak iyi tasarımdır"
(yanlış — tek uygulaması olan ve olması beklenmeyen sınıf için arayüz YAGNI
ihlalidir).
Sözlüde nasıl anlatılır — 2 dakikalık taslak
Düzen: (1) Tanım — "hiçbir çağıran, kullanmadığı metotlara bağımlı olmaya
zorlanmamalı. Buradaki kritik ayrıntı, ölçünün uygulayan sınıf değil çağıran
olması." (2) Belirti — "NotSupportedException ya da boş gövdeli
metotlar görüyorsam arayüz fazla geniştir; boş gövde en kötüsüdür, çünkü hatayı
görünmez kılar." (3) Somut maliyet — "arayüze bir üye eklediğimde o üyeyle ilgisi
olmayan sınıflar derlenmez oluyor; bu, kaynak kodda görünmeyen bir bağlantı.
Testte de kullanmadığım metotları doldurmak zorunda kalıyorum." (4) Çözüm — "rol
arayüzlerine bölerim ve arayüzü uygulamanın değil, çağıranın katmanında
tanımlarım; tek sınıf birden çok rolü karşılayabilir, DI'da ikinci kaydı
yönlendirmeyle yaparım." (5) Kıdem vuruşu — "sınırı da söylerim: her metoda bir
arayüz açmak enflasyondur. Ölçüt birlikte kullanılmadır — çağıranlar hep aynı üç
metodu birlikte istiyorsa o üçü bir arayüzdür. .NET'in
IEnumerable/IReadOnlyList/IList merdiveni
bu ilkenin standart kütüphanedeki uygulanışıdır."
