Ana sayfa → Teknik
SOLID: Amaç Değil, Araç — Ne Zaman Kullanmalı, Ne Zaman Kaçınmalı?
Code review’da bir PR: “Hoş geldin mesajı.” 5 dosya, 2 interface, 2 class,
1 factory, 30 satır. İşin tamamı tek bir string: “Hoş geldin, Ayşe!”
Yorumuma gelen cevap: “SOLID’e uygun olsun diye.” Kurala uyuyordu — kısmen:
bir yıl önce code review rehberine “her servisin bir interface’i olur”
satırını yazan bendim; o satır IGreetingService’i açıklıyor. Provider ve factory
ise “ileride farklı diller olabilir” diye eklenmişti.
- SOLID bir amaç değil, araç. Beş prensibin her biri belirli bir acıyı çözmek için var. Acı yoksa araç da gerekmez.
- Doğru yer: değişimin ve çoğulluğun olduğu yer. Sık değişen iş kuralı, birden fazla implementasyon, test edilemeyen class, gerçekçi bir provider değişimi.
- Yanlış yer: tek implementasyon, kısa ömür, “ileride lazım olur”. Burada prensipler esneklik değil, dosya sayısı üretir.
- Ölçü prensibe uyum değil. Kodun anlaşılırlığı ve değişikliğin kaç dosyaya dokunduğu.
- Doğru soru: “SOLID’e uyuyor mu?” değil, “bu yapı hangi problemi çözüyor ve o problem gerçekten var mı?”
SOLID neyi çözmek için var?
Beş harf, beş ayrı acı. Her birini önce “acı” ile, sonra kodla anlatıyorum. Örnekler C#; fikir dilden bağımsız.
S — Single Responsibility: tek iş değil, tek değişme sebebi
Fatura tutarını hesaplayan class, faturayı e-posta ile de gönderiyorsa iki ayrı sebeple değişir: vergi oranı değişince ve e-posta şablonu ya da gönderim yöntemi değişince. İkisi aynı class’taysa e-posta değişikliği hesaplama tarafını da riske atar, yeniden test ister; hesaplama testi de e-posta bağımlılığı yüzünden zorlaşır.
public class InvoiceService
{
public decimal CalculateTotal(Invoice invoice) { /* hesaplama */ }
public void SendByEmail(Invoice invoice) { /* e-posta gonderimi */ }
}
public class InvoiceCalculator
{
public decimal CalculateTotal(Invoice invoice) { /* hesaplama */ }
}
public class InvoiceEmailSender
{
public void Send(Invoice invoice) { /* e-posta gonderimi */ }
}
Dikkat: “single responsibility” “tek metot” demek değil. Ölçü, class’ı kaç farklı sebebin değiştirdiği.
O — Open/Closed: yeni else if değil, yeni class
Her yeni ödeme yönteminde aynı metodu açıp bir dal daha ekliyorsan, çalışan kodu her seferinde yeniden riske atıyorsun. Her yeni yöntem, öncekilerin hepsini yeniden test etmek demek.
public void Pay(string method, decimal amount)
{
if (method == "CreditCard") { /* kredi karti */ }
else if (method == "PayPal") { /* PayPal */ }
// yeni yontem = yeni else if = calisan kodu tekrar test et
}
public interface IPaymentMethod
{
void Pay(decimal amount);
}
public class CreditCardPayment : IPaymentMethod
{
public void Pay(decimal amount) { /* kredi karti */ }
}
public class PayPalPayment : IPaymentMethod
{
public void Pay(decimal amount) { /* PayPal */ }
}
Dürüst olalım: method string’inden hangi class’ın seçileceği kararı yok
olmuyor, tek bir yere taşınıyor — bir factory, bir dictionary ya da DI kaydı. Yeni yöntemde
orası da değişir. Kazanç if zincirinin kaybolması değil; çalışan ödeme kodunun her
seferinde yeniden riske girmemesi.
L — Liskov: “matematikte öyle” değil, behavior contract
Alt class, üst class’ın yerine konduğunda üst tipi kullanan kodun beklentileri — yani
contract — bozulmamalı. Davranışın farklı olması normal, polimorfizm bunun için var;
bozulmaması gereken şey çağıran kodun doğruluğu. Klasik tuzak: kare bir
dikdörtgendir, evet — matematikte. Kodda Square’i
Rectangle’dan türetip genişliği değiştirince yüksekliği de değiştirirsen,
dikdörtgen bekleyen kod yanlış alan hesaplar.
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area => Width * Height;
}
public class Square : Rectangle
{
public override int Width { set { base.Width = base.Height = value; } }
public override int Height { set { base.Width = base.Height = value; } }
}
Rectangle r = new Square();
r.Width = 5;
r.Height = 4;
Console.WriteLine(r.Area); // 20 bekleniyor, 16 cikiyor
public interface IShape
{
int Area { get; }
}
public class Rectangle : IShape
{
public int Width { get; set; }
public int Height { get; set; }
public int Area => Width * Height;
}
public class Square : IShape
{
public int Side { get; set; }
public int Area => Side * Side;
}
I — Interface Segregation: dev interface değil, amaca özel interface
Basit bir yazıcı, tarama ve faks metotlarını da uygulamak zorunda kalıyorsa interface yanlış
kesilmiş demektir. Belirtisi net: throw new NotSupportedException() ile dolu
class’lar.
public interface IMachine
{
void Print(Document doc);
void Scan(Document doc);
void Fax(Document doc);
}
public class BasicPrinter : IMachine
{
public void Print(Document doc) { /* yazdir */ }
public void Scan(Document doc) => throw new NotSupportedException();
public void Fax(Document doc) => throw new NotSupportedException();
}
public interface IPrinter { void Print(Document doc); }
public interface IScanner { void Scan(Document doc); }
public class BasicPrinter : IPrinter
{
public void Print(Document doc) { /* yazdir */ }
}
public class MultiFunctionPrinter : IPrinter, IScanner
{
public void Print(Document doc) { /* yazdir */ }
public void Scan(Document doc) { /* tara */ }
}
D — Dependency Inversion: detaya değil, kendi tanımladığın abstraction’a bağlan
İş mantığı doğrudan SqlServerOrderRepository’yi kendi içinde oluşturuyorsa,
o class’ı test etmek için gerçek bir SQL Server gerekir. Veritabanı değişince de iş mantığına
dokunursun. Prensibin söylediği şu: üst seviye modül (sipariş mantığı) alt seviye detaya (SQL
Server) bağlı olmasın; ikisi de bir abstraction’a bağlansın ve o abstraction’ı üst seviye
taraf kendi ihtiyacına göre tanımlasın. IOrderRepository bu yüzden veri erişim
katmanının değil, sipariş mantığının interface’i. Bunu pratikte uygulamanın yolu da
dependency’yi dışarıdan vermek: dependency injection. İkisi aynı şey değil; constructor’dan
somut SqlServerOrderRepository almak “dışarıdan verir” ama bağımlılığı
tersine çevirmez.
public class OrderService
{
private readonly SqlServerOrderRepository _repository = new();
public void PlaceOrder(Order order) => _repository.Save(order);
}
public interface IOrderRepository
{
void Save(Order order);
}
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public void PlaceOrder(Order order) => _repository.Save(order);
}
Artık OrderService SQL Server, PostgreSQL ya da testteki mock repository ile aynı
şekilde çalışır. Beş prensibin içinde en sık işe yarayanı bu; çünkü “test için bir mock” ihtiyacı neredeyse her projede gerçek.
Ne zaman gerçekten işe yarar?
Prensipler belirli sinyaller için üretildi. Sinyal projende gerçekten varsa karşılığını fazlasıyla alırsın. Sinyalin ne olduğunu ve hangi prensibin karşılık geldiğini tabloya koydum:
| Sinyal | Hangi prensip | Ne kazanırsın |
|---|---|---|
| İş kuralı sık değişiyor (kampanya, vergi, fiyatlandırma) | S, O | Değişikliğin etkisi tek class’la sınırlı kalır; regresyon alanı küçülür |
| Aynı yapının birden fazla implementasyonu var (2 ödeme provider’ı, e-posta + SMS + push) | O, D | Yeni provider = yeni class; mevcut kod dokunulmadan kalır |
| Class’ı test etmek için gerçek DB / dış servis gerekiyor | D | Mock’la test yazılabilir hâle gelir |
| Provider değişimi gerçekçi (kütüphane, bulut, storage) | D, I | Geçiş bir class’ın değişimine iner |
| Inheritance hiyerarşisinde “bu metot bende çalışmaz” var | L, I | Alt class’lar sürpriz üretmez; NotSupportedException kaybolur |
| Yıllarca yaşayacak, birden fazla ekip dokunacak | Hepsi — değişen ve çoğalan parçalarda | Kimse kimin kodunu neden değiştirdiğini sormak zorunda kalmaz. Ama bu sinyal “her parçayı soyutla” demez; uzun ömürlü projede de tek implementasyonlu parçalar olur |
Ne zaman zarar verir?
Madalyonun bu yüzü daha az konuşuluyor. Prensipleri ihtiyaç olmadan uygulamak kodu daha iyi değil, daha uzun yapar. Sinyaller:
- Tek implementasyon. Bir class’ın bir implementasyonu var ve yakın gelecekte ikincisi görünmüyor. Interface burada esneklik değil, bir dosya daha.
- Küçük ya da kısa ömürlü proje. Birkaç ekranlık iç araç, iki haftalık prototip. Layered architecture, factory ve abstraction hiyerarşisi harcanan emeğe değmez.
- “İleride lazım olabilir.” Bu cümleyle yazılan abstraction’ların çoğu hiç kullanılmaz; ama okunacak, anlaşılacak ve bakımı yapılacak kod olarak projede kalır.
- Birkaç satırlık iş için beş layer. Basit bir okuma için Controller → Service → Manager → Repository → Mapper. Bir alan eklemek 5 dosyaya dokunmak demek.
- Kontrol listesi olarak SOLID. Odak çözülen problemden “doğru görünen” koda kayar. PR’daki cevap tam olarak buydu: “SOLID’e uygun olsun diye.”
- İkinci implementasyon bugün var ya da sprint planında
- Class, mock olmadan test edilemiyor
- Aynı pattern üçüncü kez karşına çıktı
- Değişiklik her seferinde aynı
ifzincirini açıyor
- Tek implementasyon, ikincisi “belki bir gün”
- Interface’in tek implementasyonu var ve onu yalnızca DI kaydı biliyor
- Factory’nin ürettiği tek bir tip var
- Layer’lar parametreyi olduğu gibi bir alta geçiriyor
Sahadan: 30 satır ile 4 satır arasında
Girişteki PR. Özellik: kullanıcıya hoş geldin mesajı göstermek. “SOLID’e tam uyumlu” hâli şuydu:
public interface IGreetingMessageProvider
{
string GetMessage(string userName);
}
public class DefaultGreetingMessageProvider : IGreetingMessageProvider
{
public string GetMessage(string userName) => $"Hoş geldin, {userName}!";
}
public interface IGreetingService
{
string Greet(string userName);
}
public class GreetingService : IGreetingService
{
private readonly IGreetingMessageProvider _provider;
public GreetingService(IGreetingMessageProvider provider)
{
_provider = provider;
}
public string Greet(string userName) => _provider.GetMessage(userName);
}
public class GreetingServiceFactory
{
public IGreetingService Create() =>
new GreetingService(new DefaultGreetingMessageProvider());
}
İhtiyaç ise şuydu:
public class GreetingService
{
public string Greet(string userName) => $"Hoş geldin, {userName}!";
}
İlk sürümde iki interface, iki class ve bir factory var; hiçbirinin çözdüğü gerçek bir problem yok. Tek mesaj biçimi, tek implementasyon, değişme ihtimali düşük bir kural. Tabloya vurursan: sık değişen iş kuralı yok, ikinci implementasyon yok, test için mock gerekmiyor (string döndüren saf bir fonksiyon), provider değişimi diye bir şey yok, inheritance yok. Altı sinyalin beşi yok. Altıncısı — ürün yıllarca yaşayacak — doğru; ama o sinyal projenin değişen ve çoğalan parçaları için konuşur, tek satırlık bir mesaj için değil.
Konuşma beş dakika sürdü ve şu soruda bitti: “Yarın farklı dil ya da kullanıcı tipine göre mesaj gerekirse ne yaparız?” Cevap: o gün interface çıkarıp DI’a kaydederiz; çağrı yeri sayısına bağlı, küçük bir refactor. Bugün yazarsak bir yıl boyunca her okuyan beş dosyayı dolaşır. PR beş tipten bire, 30 satırdan 4 satıra indi.
Asıl düzeltme kodda değildi. Code review rehberindeki “her servisin bir interface’i olur” satırını sildim; yerine “ikinci implementasyon ya da test ihtiyacı varsa interface” yazdım. Bu kural implementasyon sayısına bakar; aşağıdaki rule of three ise tekrar eden koda. İki ayrı soru: “kaç gerçek implementasyon var?” ve “aynı kod kaç kez yazıldı?” Eski kural kısaydı, uygulaması kolaydı ve yanlıştı. Yanlış kural, kuralsızlıktan daha pahalı; çünkü herkes ona uyar.
Dengeyi nasıl kurarsın?
- YAGNI. You Aren’t Gonna Need It: bugün ihtiyaç duymadığın esnekliği bugün inşa etme. Esnekliğin de bakım maliyeti var; kullanılmayan esneklik saf maliyet.
- Rule of three. İlk kez doğrudan yaz. İkincide tekrarı fark et. Üçüncüde abstraction çıkar. Böylece abstraction tahmine değil, gerçek pattern’e dayanır; interface’in şekli de üç somut örnekten çıktığı için doğru olma ihtimali artar.
- KISS. Keep It Simple, Stupid: gereksiz karmaşıklık ekleme. Bir metodun ne yaptığını görmek için üç dosya gezmek gerekiyorsa karmaşıklık var demektir.
- Refactoring’e güven. Testle desteklenen bir codebase’de basit başlayıp ihtiyaç doğduğunda yapıyı büyütmek, baştan her ihtimale göre tasarlamaktan ucuz. Instance bir metottan interface çıkarmak IDE’de tek komut.
- Soruyu değiştir. “Bu kod SOLID’e uyuyor mu?” yerine “Bu yapı hangi problemi çözüyor ve o problem gerçekten var mı?”
Ne izlemeli?
- Tek implementasyonlu interface sayısı — IDE’de “implementations” sayısı 1 olan her interface bir soru işareti — test double’ı saymadan; gerekçe test ise interface hakkını verir (DIP bölümündeki
IOrderRepositorygibi). - Bir alan eklemek için dokunulan dosya sayısı — 5 ve üstü, layer’ların iş yapmadan parametre geçirdiğini gösterir.
- Tek tip üreten factory’ler — factory seçim yapmıyorsa
newyeterli. NotSupportedExceptionfırlatan implementasyonlar — interface yanlış kesilmiş (I) ya da inheritance yanlış (L).- Her yeni durumda büyüyen
if/switch— üçüncü daldan sonra O için gerçek bir sinyal. - Gerçek DB ya da dış servis isteyen unit test — D’nin en somut ihtiyacı.
Kontrol listesi
- Bu abstraction’ın ikinci implementasyonu bugün var mı, ya da sprint planında mı?
- Class’ı mock olmadan test edebiliyor muyum? (Evetse interface’in gerekçesi ne?)
- Aynı pattern üçüncü kez mi karşıma çıktı, yoksa ilk mi?
- Bu değişiklik sık değişen bir iş kuralına mı dokunuyor?
- Eklediğim layer bir iş yapıyor mu, yoksa parametreyi olduğu gibi aşağı mı geçiriyor?
- Factory bir seçim yapıyor mu?
- Bir alan eklemek kaç dosyaya dokunuyor; 5 ve üstü mü?
- Hiyerarşide
NotSupportedExceptionfırlatan metot var mı? - Abstraction’ı bugün değil ihtiyaç günü ekleseydim kaç dakika sürerdi?
- Code review rehberimde “her X için Y” biçiminde koşulsuz bir kural var mı?
Sonuç
SOLID, değişen gereksinimler karşısında kodun bozulmasını önlemek için üretilmiş değerli bir araç seti. Değişimin yoğun olduğu, birden fazla implementasyonun bulunduğu, yıllarca yaşayacak projelerde vazgeçilmez. Küçük, basit ya da kısa ömürlü işlerde aynı prensipler gereksiz layer’a ve anlaşılması zor bir yapıya dönüşür.
O PR’daki 30 satır beş prensibin hiçbirini çiğnemiyordu. Problem kodda değil, kuraldaydı: prensibi araç olarak değil, amaç olarak yazmıştım. Beş tipi bire indirmek beş dakika sürdü; yanlış cümle rehberde bir yıl yaşadı.
İyi tasarımın ölçüsü prensiplere ne kadar uyulduğu değil; kodun ne kadar anlaşılır olduğu ve değişikliğin kaç dosyaya dokunduğu. Prensibi ezberleyip her yere uygulamak kolay; arkasındaki acıyı tanıyıp bu projede var mı diye sormak zor. Ustalık ikincisinde.