Bölüm 3 / 15
SOLID: Single Responsibility
Tek sorumluluk ilkesi.
11 dakikalık okuma
Bu ders SOLID'in ilk harfini kuruyor — ve bunu yaparken ilkelerin tamamına giriş yapıyor. SRP mülakatlarda en çok sorulan, sahada en çok yanlış anlatılan ilkedir: "bir sınıf bir iş yapar" cümlesi ezberlenir, ama bu tanım kararı vermeye yetmez. Doğru ölçüt değişim sebebidir, ve bu ölçüt bir önceki dersteki "aynı sebeple mi değişir" sorusunun sınıf düzeyindeki karşılığıdır.
01Ne öğreneceksin
- SOLID nedir, beş ilke neyi hedefler, neden "kural" değil "eğilim"dir
- SRP'nin ezber tanımının neden yetersiz olduğu, doğru ölçüt: değişim sebebi / aktör
- İhlal belirtileri: "ve"li sınıf adları, dev test kurulumu, birlikte değişen dosyalar
- Ayırma teknikleri ve saf hesap ile yan etkinin ayrılması
- Kapsülleme ve kaynak sahipliğiyle bağlantısı
- Aşırıya kaçmanın maliyeti: sınıf enflasyonu ve bağlılık-bağımlılık dengesi
02SOLID nedir
SOLID, nesne yönelimli tasarımda 2000'li yıllarda derlenmiş beş ilkenin baş harflerinden oluşan bir hatırlatıcıdır. Beşi de tek bir amaca hizmet eder: değişimin maliyetini düşürmek. Yazılımın değişeceği kesindir; iyi tasarım, değişimi engellemeye değil, değişimin yayılma alanını daraltmaya çalışır.
| Harf | İlke | Tek cümle | Ders |
|---|---|---|---|
| S | Single Responsibility | Bir modülün değişmek için tek sebebi olmalı | Bu ders |
| O | Open/Closed | Genişlemeye açık, değişikliğe kapalı | 4. ders |
| L | Liskov Substitution | Alt tip, üst tipin yerine sorunsuz geçebilmeli | 5. ders |
| I | Interface Segregation | Kullanılmayan üyeye bağımlılık dayatılmamalı | 6. ders |
| D | Dependency Inversion | Somut değil soyutlamaya bağımlı olunmalı | 7. ders |
Baştan bir uyarı: SOLID kural kitabı değil, karar verirken kullanılan bir dizi eğilimdir. Her sınıfa körü körüne uygulanırsa bir önceki dersin konusu olan arızi karmaşıklığı üretir. Kıdemli kullanımı şudur: ilkeler sana hangi soruları soracağını öğretir, cevabı bağlam verir.
03SRP'nin doğru tanımı
Yaygın ezber: "bir sınıf yalnızca bir iş yapmalı." Bu cümle uygulanamaz, çünkü
"bir iş"in sınırı belirsizdir — SiparisServisi.SiparisiTamamla bir iş
midir, yoksa doğrulama + fiyatlama + ödeme + kayıt olmak üzere dört iş midir? Aynı
kod, bakış açısına göre hem tek hem çok iş sayılabilir.
Uygulanabilir tanım değişim üzerinden kurulur:
"Bir modülün değişmek için yalnızca bir sebebi olmalıdır."
İlkeyi derleyen Robert C. Martin bunu sonradan daha da netleştirdi, çünkü "sebep" de yoruma açıktı:
"Bir modül tek bir aktöre karşı sorumlu olmalıdır." — Aktör, o değişikliği talep eden insan grubudur: muhasebe birimi, İK, operasyon, güvenlik ekibi, veritabanı yöneticileri.
Klasik örnek bu ayrımı somutlaştırır:
public class Calisan
{
public decimal MaasHesapla() { /* ... */ } // aktör: MUHASEBE
public int CalismaSaatiRaporla() { /* ... */ } // aktör: İNSAN KAYNAKLARI
public void Kaydet() { /* ... */ } // aktör: VERİTABANI EKİBİ
}
Üç metot da "çalışanla ilgilidir" — ezber tanım burada yeşil ışık yakar. Ama üç
farklı birim, birbirinden bağımsız zamanlarda, birbirinden habersiz değişiklik
isteyecektir. Tehlike şurada belirir: muhasebenin talebiyle
MaasHesapla düzenlenirken, aynı sınıftaki ortak bir yardımcı metot da
değiştirilirse, İK'nın raporu kimsenin haberi olmadan bozulur. Sınıfın tek olması,
birbirinden bağımsız üç talebi tek noktada çarpıştırır.
Ölçüt bu yüzden şudur: bu sınıfı değiştirmeyi kimler talep eder? Cevapta birden fazla bağımsız taraf varsa SRP ihlali vardır.
04İhlal belirtileri
Sahada ihlali fark ettiren somut işaretler:
- Sınıfı anlatırken "ve" kullanmak zorunda kalmak. "Kullanıcıyı doğrular ve veritabanına yazar ve e-posta gönderir" — üç aktör, üç sebep.
- Sınıf adında genel kelimeler:
Manager,Helper,Utils,Islemci,Common. Bu adlar sınırı tanımlamadıkları için her yeni kodun sığındığı çöp alanlarına dönüşür. - Test kurulumunun büyüklüğü. Tek bir davranışı test etmek için beş sahte nesne (mock) kurman gerekiyorsa, sınıf beş şeye birden bağlıdır. Test acısı, tasarım sorununun en dürüst göstergesidir.
- Karışık soyutlama düzeyleri. Aynı sınıfta hem iş kuralı
(
indirim = tutar * 0.1m) hem altyapı ayrıntısı (SqlConnection,HttpClient) varsa iki farklı dünya birleşmiştir. - Kısmi alan kullanımı. Sınıfın metotlarının bir kısmı yalnız birinci grup alanları, diğer kısmı yalnız ikinci grup alanları kullanıyorsa, sınıf zaten içeriden ikiye bölünmüştür — akademik adı düşük cohesion'dır.
- Sürüm geçmişinde birlikte değişme. Aynı dosya her ay farklı gerekçelerle
(bir kez rapor formatı, bir kez vergi oranı, bir kez log formatı) değişiyorsa, bu
ölçülebilir bir ihlal kanıtıdır. 16. hatta
git logile bu analizi yapmayı öğreneceksin.
05Ayırma teknikleri
Aşağıdaki sınıf, gerçek projelerde en sık görülen SRP ihlalidir:
public class KullaniciServisi
{
public void Kaydet(string ad, string email, string sifre)
{
if (!email.Contains('@')) throw new ArgumentException("Geçersiz e-posta.");
if (sifre.Length < 8) throw new ArgumentException("Şifre kısa.");
var hash = BCrypt.HashPassword(sifre);
using var baglanti = new SqlConnection(_baglantiMetni);
baglanti.Execute("INSERT INTO Kullanici ...", new { ad, email, hash });
var smtp = new SmtpClient("smtp.sirket.local");
smtp.Send(new MailMessage("no-reply@sirket.local", email, "Hoş geldiniz", "..."));
File.AppendAllText("log.txt", $"{DateTime.Now}: {email} kaydedildi\n");
}
}
Bu metot dört aktöre birden bağlıdır: iş kuralları (doğrulama politikası), veritabanı ekibi (şema ve erişim), altyapı (SMTP sunucusu) ve operasyon (log biçimi). Herhangi birinin talebi bu dosyayı açtırır. Ayrıştırılmış hâli:
public class KullaniciServisi
{
private readonly IKullaniciDeposu _depo;
private readonly IBildirimGonderici _bildirim;
private readonly ILogger<KullaniciServisi> _log;
public KullaniciServisi(IKullaniciDeposu depo, IBildirimGonderici bildirim,
ILogger<KullaniciServisi> log)
=> (_depo, _bildirim, _log) = (depo, bildirim, log);
public async Task<Sonuc> KaydetAsync(KayitIstegi istek)
{
var dogrulama = KayitKurallari.Dogrula(istek); // saf iş kuralı, I/O yok
if (!dogrulama.Basarili) return dogrulama;
var kullanici = Kullanici.Olustur(istek); // hash'i tip kendi içinde kurar
await _depo.EkleAsync(kullanici);
await _bildirim.HosGeldinGonderAsync(kullanici.Email);
_log.LogInformation("Kullanıcı kaydedildi: {Email}", kullanici.Email);
return Sonuc.Ok();
}
}
Dört tekniğin hepsi burada:
- Saf hesabı yan etkiden ayır.
KayitKurallari.Dogrulahiçbir şeye dokunmaz; girdi alır, sonuç döndürür. Bu sınıfın testi için sahte nesne gerekmez, milisaniyeler sürer ve iş kuralları böylece bağımsız olarak gelişebilir. Kıdemli tasarımın en pratik kuralı budur: I/O'yu kenara it, çekirdeği saf tut. - Altyapıyı soyutlamanın arkasına al.
IKullaniciDeposuveIBildirimGonderici, veritabanı ve SMTP değişikliklerini servisin dışında tutar. (Bu, 7. dersteki DIP'in ön adımıdır; arayüzlerin nereye ait olduğu orada netleşecek.) - Değişmezleri tipe taşı.
Kullanici.Olustur, geçerli olmayan bir kullanıcı nesnesinin var olamayacağını garanti eder. 1. hattaki kapsülleme dersinin doğrudan devamıdır. - Düzenleme (orchestration) tek yerde kalır.
KullaniciServisiartık kendisi iş yapmaz, iş yapanları sıraya koyar. Bu da bir sorumluluktur ve meşrudur.
Ayırmanın bir alternatifi de cepheyi (facade) korumaktır: çağıranların alışık olduğu tek giriş noktasını yerinde bırakıp arkasını bölmek. Böylece dış API sabit kalırken iç yapı düzelir — mevcut kod tabanında en az riskli yoldur.
06Kapsülleme ve sahiplik bağlantısı
SRP, iki eski dersin doğal devamıdır.
Kapsülleme (1. hattın 11. dersi) veriyi ve onu değiştiren davranışı bir arada tutar, dışarıya yalnız güvenli bir yüz gösterir. SRP aynı fikri bir üst ölçekte sorar: bu yüzün arkasında kaç farklı ilgi alanı var? Kapsülleme "veriye kim dokunabilir" sorusunun cevabıysa, SRP "bu birimi kim değiştirmek ister" sorusunun cevabıdır.
Kaynak sahipliği (3. hattın IDisposable dersi) şu kuralı
bırakmıştı: "yaratan atar" — bir kaynağı kim oluşturuyorsa serbest bırakma
sorumluluğu da onundur, ödünç alınan kaynak kapatılmaz. Bu kural aslında bir SRP
uygulamasıdır: sahiplik tek bir yere verilir, böylece "kim kapatacak?" belirsizliği
ve iki kez kapatma hatası ortadan kalkar.
Aynı mantık nesne yaşam döngüsünün ötesine de taşınır. Bir veriyi kimin yazma yetkisi olduğu tek yerde tanımlanmışsa (tek sahip), tutarsız durum üretmek zorlaşır. Dağıtık sistemlerde bu ilke "her veri parçasının tek sahibi olan servisi vardır" biçiminde karşına çıkacak (Hat 19); eşzamanlı kodda ise 2. ve 3. hatlarda gördüğün paylaşılan durum sorunlarını doğrudan azaltır.
07Aşırıya kaçmak: sınıf enflasyonu
SRP en kolay abartılan ilkedir. Her metodu ayrı sınıfa alan bir kod tabanı, okunması için sürekli dosya değiştirmeyi gerektirir; tek bir iş akışını izlemek on dosya arasında zıplamaya dönüşür. Bu da bir bakım maliyetidir — yalnız farklı bir biçimde.
Denge iki kavramla kurulur:
- Bağlılık (cohesion) — bir birimin içindeki parçaların birbirine ait olma derecesi. Yüksek olması istenir.
- Bağımlılık (coupling) — birimler arası bağ. Düşük olması istenir.
SRP'nin amacı sınıf sayısını artırmak değil, birlikte değişenleri bir arada, ayrı değişenleri ayrı tutmaktır. Bu, ilkenin daha genel ifadesi olan Common Closure fikridir. Uygulanabilir kontrol listesi:
| Soru | Böl | Bırak |
|---|---|---|
| Değişikliği kimler talep ediyor? | Birden çok bağımsız taraf | Tek taraf |
| Metotlar aynı alanları mı kullanıyor? | İki ayrık alan kümesi var | Hepsi ortak durumu kullanıyor |
| İş kuralı ile altyapı iç içe mi? | Evet — saf çekirdeği ayır | Hayır |
| Test için kaç sahte nesne gerekiyor? | Üç ve üzeri | Sıfır veya bir |
| Bölünce ortaya tek metotluk, tek kullanımlı sınıflar mı çıkıyor? | — | Bırak; YAGNI |
Bir sonraki ders SOLID'in ikinci harfini alıyor: Open/Closed. Orada sorulacak soru şu olacak — sorumlulukları ayırdın, peki yeni bir davranış eklemek gerektiğinde çalışan kodu hiç açmadan bunu yapabilir misin?
08Mini alıştırma — Aktörleri ayır
Yaklaşık 20 dakika · dotnet run
- Aktör haritası: 5. bölümdeki ilk
KullaniciServisisürümünü yaz. Her satırın yanına, o satırı değiştirmeyi kimin talep edeceğini yaz (iş birimi / veritabanı / altyapı / operasyon). Kaç farklı aktör çıktı? - Saf çekirdeği çıkar: doğrulama kurallarını
KayitKurallariadlı, hiçbir dış bağımlılığı olmayan statik bir sınıfa taşı. Bu sınıf için sahte nesne kurmadan üç test senaryosu (geçersiz e-posta, kısa şifre, geçerli kayıt) çalıştır. - Altyapıyı arkasına al:
IKullaniciDeposuveIBildirimGondericiarayüzlerini tanımla; bellekte çalışan basit birer uygulama yaz (List<Kullanici>ve konsola yazan gönderici). Servisi bunlarla çalıştır — veritabanı olmadan uçtan uca akışın çalıştığını gör. - Değişmezi tipe taşı:
Kullanicitipinin kurucusunuprivateyap, yalnızOlusturfabrika metoduyla örneklenebilsin ve geçersiz veriyle örnek üretilemesin. Geçersiz bir kullanıcı yaratmayı dene — derleyici mi engelledi, çalışma zamanı mı? - Değişim testi: "e-posta doğrulaması artık alan adı kara listesine de bakacak" gereksinimini uygula. Hangi dosyaları açman gerekti? Aynı işi ilk sürümde yapmayı düşün ve farkı bir cümleyle yaz.
- Ters yön: şimdi kasten abart —
KayitKurallari'nı her kural için ayrı sınıfa böl (EmailKurali,SifreKurali,AdKurali). Okunabilirlik arttı mı, azaldı mı? Hangi noktada bölmeyi durdurmak gerektiğine dair kendi ölçütünü yaz.
09Kendini sına
SRP'nin doğru tanımı nedir, "bir sınıf bir iş yapar" neden yetersiz?
"Bir iş"in sınırı belirsizdir: aynı metot bakış açısına göre tek iş de dört iş de sayılabilir, bu yüzden bu tanımla karar verilemez. Uygulanabilir tanım değişim üzerinden kurulur: bir modülün değişmek için yalnızca bir sebebi olmalıdır. Robert C. Martin bunu daha sonra netleştirdi — bir modül tek bir aktöre karşı sorumlu olmalıdır; aktör, değişikliği talep eden insan grubudur (muhasebe, İK, veritabanı ekibi, operasyon). Klasik örnek MaasHesapla, ÇalışmaSaati Raporla ve Kaydet metotlarını taşıyan Calisan sınıfıdır: üçü de çalışanla ilgilidir ama üç farklı birim bağımsız zamanlarda değişiklik ister; ortak bir yardımcıya dokunulduğunda bir birimin talebi diğerinin çıktısını sessizce bozar. Uygulanabilir soru şudur: bu sınıfı değiştirmeyi kimler talep eder? Birden çok bağımsız taraf varsa ihlal vardır.
Bir sınıfta SRP ihlali olduğunu hangi belirtilerden anlarsın?
Sınıfı anlatırken "ve" kullanmak zorunda kalmak ilk işarettir ("doğrular ve kaydeder ve e-posta gönderir"). Manager, Helper, Utils gibi sınırı tanımlamayan adlar zamanla çöp alanına dönüşür. Test kurulumunun büyüklüğü en dürüst göstergedir: tek bir davranışı test etmek için beş sahte nesne gerekiyorsa sınıf beş şeye bağlıdır. İş kuralıyla altyapı ayrıntısının (SqlConnection, HttpClient) aynı sınıfta bulunması karışık soyutlama düzeyi demektir. Metotların bir kısmının yalnız birinci grup alanları, kalanının yalnız ikinci grup alanları kullanması sınıfın zaten içeriden ikiye bölündüğünü gösterir — düşük cohesion. Son olarak sürüm geçmişi ölçülebilir kanıt verir: aynı dosya her ay farklı gerekçelerle değişiyorsa sebep sayısı birden fazladır.
SRP'yi uygularken hangi teknikleri kullanırsın?
Birincisi saf hesabı yan etkiden ayırmaktır: doğrulama ve iş kuralları hiçbir I/O yapmayan, girdi alıp sonuç döndüren birimlere taşınır; testi sahte nesne gerektirmez ve hızlıdır. İkincisi altyapıyı soyutlamanın arkasına almaktır — veritabanı ve bildirim erişimi arayüzlerin arkasına alınınca bu katmanların değişimi servisi açtırmaz; bu DIP'in ön adımıdır. Üçüncüsü değişmezleri tipe taşımaktır: fabrika metodu ve private kurucu ile geçersiz durumda bir nesnenin var olamaması sağlanır, bu kapsüllemenin devamıdır. Dördüncüsü düzenleme sorumluluğunu tek yerde tutmaktır; servis artık işi kendisi yapmaz, iş yapanları sıraya koyar ve bu meşru bir sorumluluktur. Mevcut kodda en az riskli yol cephe (facade) korumaktır: dış giriş noktası sabit kalırken arkası bölünür.
SRP aşırıya kaçarsa ne olur, dengeyi nasıl kurarsın?
Her metodu ayrı sınıfa almak sınıf enflasyonu üretir: tek bir iş akışını izlemek on dosya arasında zıplamayı gerektirir, bu da bakım maliyetinin başka bir biçimidir. Denge iki kavramla kurulur — bağlılık (cohesion) yüksek, bağımlılık (coupling) düşük olmalı. SRP'nin amacı sınıf sayısını artırmak değil, birlikte değişenleri bir arada, ayrı değişenleri ayrı tutmaktır. Pratik ölçütler: değişikliği kaç bağımsız taraf talep ediyor, metotlar aynı alan kümesini mi kullanıyor, iş kuralı altyapıyla iç içe mi, test için kaç sahte nesne gerekiyor. Bölme sonucunda tek metotluk ve tek kullanımlı sınıflar çıkıyorsa bu YAGNI ihlalidir, bölme durdurulur.
10Sınav köşesi
Yazılıda nasıl sorulur
(1) Açılım: SOLID'in beş ilkesini yazın ve birer cümleyle tanımlayın — bu soru
neredeyse her sınavda çıkar. (2) Tanım: SRP nedir; beklenen cevap "değişmek için
tek sebep / tek aktör". (3) Kod analizi: doğrulama, veritabanı erişimi, e-posta
gönderimi ve loglama yapan bir sınıf verilip "hangi ilke ihlal edilmiş, nasıl
düzeltilir" sorulur. (4) Kavram çifti: cohesion ve coupling tanımı, hangisinin
yüksek hangisinin düşük olması istenir. (5) Doğru/yanlış: "SRP her sınıfta tek
metot olmasını gerektirir" (yanlış). (6) Örnek üretme: Calisan
sınıfı üzerinden üç aktör örneği verme. (7) İlişkilendirme: SRP ile kapsülleme
arasındaki bağ.
Sözlüde nasıl anlatılır — 2 dakikalık taslak
"SRP'yi anlatır mısınız?" için düzen: (1) Doğru tanımla başla — "'bir sınıf bir iş yapar' ezberi karar vermeye yetmez; ölçütüm değişim sebebidir: bir modülün değişmesini talep eden tek bir aktör olmalı." (2) Örnek — "muhasebenin maaş hesabı, İK'nın saat raporu ve veritabanı ekibinin kaydetme metodu aynı sınıftaysa birinin talebi diğerinin çıktısını sessizce bozabilir." (3) Nasıl fark ederim — "sınıfı 've' ile anlatıyorsam, testi için üç dört sahte nesne kuruyorsam ya da aynı dosya her ay farklı gerekçelerle değişiyorsa ihlal vardır." (4) Nasıl düzeltirim — "saf iş kurallarını I/O'dan ayırırım, altyapıyı arayüz arkasına alırım, değişmezleri tipin kendisine taşırım; mevcut kodda dış API'yi cephe olarak korur, arkasını bölerim." (5) Kıdem vuruşu — "SRP'yi sınıf sayısını artırmak için kullanmam; amaç birlikte değişeni bir arada tutmak. Aşırıya kaçarsa tek metotluk sınıflar ve dosya arasında zıplayan bir okuma deneyimi çıkar. Sahiplik kuralım da bununla aynı köke bağlı: kaynağı yaratan atar, veriyi değiştirme yetkisi tek yerde olur."
