SOLID Prensipleri
Single Responsibility Principle
S — Tek Sorumluluk Prensibi
Amaç: Bir sınıfın değişmek için yalnızca tek bir nedeni olmalıdır. Yani her sınıf tek bir işten (tek bir paydaşın ihtiyacından) sorumlu olmalıdır.
Basit Anlatım
Teknik terim yok — günlük hayattan bir benzetme
Bir restoranı düşünün. Aşçı yemek yapar, garson sipariş alır ve servis eder, kasiyer hesabı keser. Her biri kendi işinde uzmandır.
Şimdi bütün bu işleri tek bir kişinin yaptığını düşünün: hem yemek pişiriyor, hem masalara koşuyor, hem kasada duruyor. Kasada sıra birikince yemek yanar. Yemek yanınca müşteriler bekler. Bir işteki sorun diğer bütün işleri etkiler. Ve bu kişi hastalandığında restoran tamamen durur.
İsviçre çakısı benzetmesi
İsviçre çakısı her şeyi yapar: bıçak, makas, tornavida, şişe açacağı. Kamp için pratiktir. Ama bir marangoz atölyesinde kimse çakıyla çalışmaz; her iş için o işe özel alet kullanılır. Makas bozulduğunda bütün çakıyı tamire vermek zorunda kalmak istemezsiniz.
Teknik Anlamı
Bir yazılımcının bakış açısıyla
Robert C. Martin (Uncle Bob) bu prensibi şöyle tanımlar: "Bir modülün değişmek için bir ve yalnızca bir nedeni olmalıdır." Daha sonra bunu netleştirmiştir: "Bir modül, yalnızca bir aktöre (paydaşa) karşı sorumlu olmalıdır."
"Tek sorumluluk" ne demek?
Yaygın yanlış anlama: "Her sınıfın tek bir metodu olmalı." Doğrusu: Sınıftaki her şey aynı nedenle değişmelidir. Örneğin bir fatura sınıfında:
- Vergi hesaplama kuralını muhasebe departmanı değiştirir.
- Faturanın veritabanında nasıl saklandığını DBA / altyapı ekibi değiştirir.
- E-posta şablonunu pazarlama ekibi değiştirir.
Üç farklı paydaş = değişmek için üç farklı neden = üç farklı sınıf olmalı.
İhlal belirtileri (code smells)
- Sınıf adında "Manager", "Helper", "Utils", "Processor" gibi belirsiz ifadeler.
- Yüzlerce satırlık sınıflar, çok sayıda bağımlılık (constructor'da 7-8 parametre).
- Sınıfın ne yaptığını anlatırken "ve" bağlacını sık kullanmak: "Faturayı hesaplar ve kaydeder ve e-posta atar."
- Birbiriyle ilgisiz değişikliklerin aynı dosyada merge conflict yaratması.
Faydaları
- Değişikliklerin etki alanı küçülür; bir yerdeki değişiklik başka yeri bozmaz.
- Küçük ve odaklı sınıflar kolayca test edilir ve yeniden kullanılır.
- Kod okunabilirliği artar: sınıf adı ne yaptığını tam olarak söyler.
C# ile Uygulama
Adım adım, çalışan kod örneği
Senaryo: Bir fatura işlemi. Önce prensibi ihlal eden tasarımı, ardından düzeltilmiş halini göreceğiz. (Kötü örnek Kotu namespace'inde duruyor.)
Adım 1 YANLIŞ Her şeyi yapan sınıf
Hesaplama, kalıcılık (veritabanı), bildirim ve biçimlendirme aynı sınıfta. Vergi oranı değişse de, veritabanı değişse de, e-posta şablonu değişse de bu sınıfa dokunmak zorundayız.
public class InvoiceService
{
public void Process(string customer, decimal amount)
{
// 1) İş kuralı: vergi hesaplama
decimal total = amount * 1.20m;
// 2) Kalıcılık: veritabanına kaydetme
Console.WriteLine($" INSERT INTO Invoices VALUES ('{customer}', {total})");
// 3) Bildirim: e-posta gönderme (SMTP ayarları dahil!)
Console.WriteLine($" SMTP smtp.firma.com:587 -> {customer}: Faturanız {total} TL");
// 4) Sunum: rapor biçimlendirme
Console.WriteLine($" === FATURA === {customer} | {total:N2} TL");
}
}Adım 2 DOĞRU Her sorumluluk kendi sınıfında
Her sınıfın değişmek için tek bir nedeni var. Vergi kuralı değişirse sadece TaxCalculator, veritabanı değişirse sadece InvoiceRepository değişir.
public record Invoice(string Customer, decimal Amount, decimal Total);
public class TaxCalculator // Değişim nedeni: vergi mevzuatı
{
private const decimal VatRate = 0.20m;
public decimal AddVat(decimal amount) => amount * (1 + VatRate);
}
public class InvoiceRepository // Değişim nedeni: veri saklama teknolojisi
{
public void Save(Invoice invoice) =>
Console.WriteLine($" [DB] Fatura kaydedildi: {invoice.Customer}, {invoice.Total}");
}
public class InvoiceMailer // Değişim nedeni: iletişim kanalı/şablon
{
public void Send(Invoice invoice) =>
Console.WriteLine($" [Mail] {invoice.Customer}: Faturanız {invoice.Total:N2} TL");
}
public class InvoicePrinter // Değişim nedeni: görsel format
{
public string Format(Invoice invoice) =>
$"=== FATURA === {invoice.Customer} | {invoice.Total:N2} TL";
}Adım 3 DOĞRU Koordinasyon: sadece akışı yöneten sınıf
InvoiceProcessor hiçbir detayı bilmez; yalnızca adımları sırayla çağırır. Onun değişim nedeni de tektir: iş akışının kendisi.
public class InvoiceProcessor
{
private readonly TaxCalculator _tax = new();
private readonly InvoiceRepository _repository = new();
private readonly InvoiceMailer _mailer = new();
private readonly InvoicePrinter _printer = new();
public void Process(string customer, decimal amount)
{
var invoice = new Invoice(customer, amount, _tax.AddVat(amount));
_repository.Save(invoice);
_mailer.Send(invoice);
Console.WriteLine(" " + _printer.Format(invoice));
}
}Adım 4 Kullanım
Dışarıdan bakıldığında iki versiyon aynı işi yapıyor; fark bakımda ortaya çıkar:
Console.WriteLine("Kötü tasarım:");
new Kotu.InvoiceService().Process("ABC Ltd.", 1000m);
Console.WriteLine("İyi tasarım:");
new InvoiceProcessor().Process("ABC Ltd.", 1000m);
// Sorumluluklar ayrı olduğu için tek başına test edilebilir:
var tax = new TaxCalculator();
Console.WriteLine($"Vergi testi: 1000 -> {tax.AddVat(1000m)} (beklenen 1200,00)");Konsol çıktısı
Kötü tasarım:
INSERT INTO Invoices VALUES ('ABC Ltd.', 1200,00)
SMTP smtp.firma.com:587 -> ABC Ltd.: Faturanız 1200,00 TL
=== FATURA === ABC Ltd. | 1.200,00 TL
İyi tasarım:
[DB] Fatura kaydedildi: ABC Ltd., 1200,00
[Mail] ABC Ltd.: Faturanız 1.200,00 TL
=== FATURA === ABC Ltd. | 1.200,00 TL
Vergi testi: 1000 -> 1200,00 (beklenen 1200,00)Kodda dikkat edilmesi gerekenler
- Kötü tasarımda vergi hesaplamasını test etmek için veritabanı ve e-posta kodunu da çalıştırmak zorundasınız. İyi tasarımda
TaxCalculatortek başına test edildi. - Gerçek bir projede bu sınıflar arayüzlerle (
IInvoiceRepositoryvb.) soyutlanıp dependency injection ile verilir; bu da D (Dependency Inversion) prensibinin konusudur.