Mert Özen Her Satırda Daha İleri
Yazılım Tasarımı SOLID ve Design Principles — #01

Design Principles Neden Var? Değişim, Bağımlılık ve SOLID'in Kısa Tarihi

Mert Özen 7 Kas 2026 13 dk 6 görüntülenme
Design Principles Neden Var? Değişim, Bağımlılık ve SOLID'in Kısa Tarihi

Design principle'lar, değişimi pahalı yapan plansız bağımlılıkları adlandırmak ve onlara sınır çizmek için var: rigidity, fragility, immobility ve viscosity'nin ortak kökü bağımlılıktır. Parnas'ın information hiding'inden Feathers'ın SOLID kısaltmasına uzanan tarih ve serinin Bookstore sipariş-bildirim örneği.

Sürüm notu: Bu yazıdaki kodlar C# 14 ve .NET 10 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore'da sipariş özetini iki yer kullanıyor: muhasebenin fatura satırı ve müşteriye giden SMS (Short Message Service). Geçen hafta SMS'ler operatörün karakter sınırına takıldı; işi alan arkadaş ortak OrderText.Summary method'una bir kısaltma ekledi, SMS testini çalıştırdı, test geçti, değişiklik merge edildi. Faturaya kimse dokunmadı. Bu kod çalışınca fatura satırında ne yazar?

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Order order = new(1042, "Ada",
[
    new OrderLine("Dune", 2, 12.99m),
    new OrderLine("Emma", 1, 7.50m),
    new OrderLine("The Hobbit", 1, 10.99m)
]);

Console.WriteLine(InvoiceWriter.Write(order));
Console.WriteLine(SmsNotifier.Text(order));

public record OrderLine(string Title, int Quantity, decimal UnitPrice);

public record Order(int Id, string CustomerName, IReadOnlyList<OrderLine> Lines)
{
    public decimal Total => Lines.Sum(l => l.Quantity * l.UnitPrice);
}

public static class OrderText
{
    // SMS 160 karakter sınırı yüzünden geçen hafta eklendi
    public static string Summary(Order order)
    {
        string text = string.Join(", ", order.Lines.Select(l => $"{l.Quantity} x {l.Title}"));
        return text.Length <= 30 ? text : text[..27] + "...";
    }
}

public static class InvoiceWriter
{
    public static string Write(Order order) =>
        $"INVOICE #{order.Id} | {order.CustomerName} | {OrderText.Summary(order)} | {order.Total:0.00} EUR";
}

public static class SmsNotifier
{
    public static string Text(Order order) =>
        $"Order {order.Id}: {OrderText.Summary(order)}";
}

Kimsenin Dokunmadığı Yer Bozuldu

Çıktı:

INVOICE #1042 | Ada | 2 x Dune, 1 x Emma, 1 x The... | 44.47 EUR
Order 1042: 2 x Dune, 1 x Emma, 1 x The...

Faturada üçüncü kitap "The..." olarak kesildi. SMS doğru, çünkü değişiklik onun için yapıldı. Fatura bozuk, çünkü fatura aynı method'a bağlıydı ve bu bağı kimse aklında tutmuyordu. Ay sonunda muhasebe "faturada kitap adı neden yarım?" diye sorduğunda bu satırın SMS ile ilgili bir değişiklikten geldiğini bulmak saatler sürer; git blame'de faturaya ait tek bir commit yok.

Bu yazının konusu tam olarak bu his: kod çalışıyor, testler yeşil, ama her değişiklik bir yerlerde beklenmedik bir şey kırıyor ve ekip zamanla dokunmaya korkmaya başlıyor. Design principle dediğimiz şeyler, bu hissin nedenlerini adlandırmak ve onlara karşı ne yapabileceğimizi konuşmak için var. Serinin ilk yazısında prensiplerin kendisinden çok, neden ortaya çıktıklarını konuşacağız: kötü tasarımın belirtileri, hepsinin ortak kökü olan bağımlılık ve David Parnas'tan (1970'lerde module tasarımının temellerini atan yazılım mühendisliği öncüsü) SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) kısaltmasına uzanan kısa bir tarih.

Önce düzeltmeyi görelim. Faturanın ve SMS'in özet kuralları farklı kişilerden geliyor; birini muhasebe, diğerini SMS operatörünün sınırı belirliyor. O yüzden ikisi farklı yerde yaşamalı:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Order order = new(1042, "Ada",
[
    new OrderLine("Dune", 2, 12.99m),
    new OrderLine("Emma", 1, 7.50m),
    new OrderLine("The Hobbit", 1, 10.99m)
]);

Console.WriteLine(InvoiceWriter.Write(order));
Console.WriteLine(SmsNotifier.Text(order));

public record OrderLine(string Title, int Quantity, decimal UnitPrice);

public record Order(int Id, string CustomerName, IReadOnlyList<OrderLine> Lines)
{
    public decimal Total => Lines.Sum(l => l.Quantity * l.UnitPrice);
}

public static class InvoiceWriter
{
    // Fatura muhasebenin kuralı: her satır eksiksiz yazılır
    public static string Write(Order order) =>
        $"INVOICE #{order.Id} | {order.CustomerName} | " +
        string.Join(", ", order.Lines.Select(l => $"{l.Quantity} x {l.Title}")) +
        $" | {order.Total:0.00} EUR";
}

public static class SmsNotifier
{
    private const int MaxSummaryLength = 30;

    // SMS'in kısaltma kuralı yalnız burada yaşar
    public static string Text(Order order)
    {
        string summary = string.Join(", ", order.Lines.Select(l => $"{l.Quantity} x {l.Title}"));
        if (summary.Length > MaxSummaryLength)
            summary = summary[..(MaxSummaryLength - 3)] + "...";

        return $"Order {order.Id}: {summary}";
    }
}
INVOICE #1042 | Ada | 2 x Dune, 1 x Emma, 1 x The Hobbit | 44.47 EUR
Order 1042: 2 x Dune, 1 x Emma, 1 x The...

Evet, string.Join satırı artık iki yerde. Bu bir DRY (Don't Repeat Yourself) ihlali gibi görünüyor ama değil: iki satır bugün aynı görünüyor, yarın farklı nedenlerle farklı yönlere değişecek. Aynı görünen ama farklı bilgiyi temsil eden kodu birleştirmenin bedelini ilerideki DRY, KISS (Keep It Simple, Stupid) ve YAGNI (You Aren't Gonna Need It) yazısında ayrıntılı konuşacağız; şimdilik şunu not et: ilk koddaki asıl hata tekrarı önlemek değil, iki ilgisiz değişim nedenini tek bir method'a bağlamaktı.

Kötü Tasarımın Dört Belirtisi

Robert C. Martin'in ("Uncle Bob" olarak da bilinen, Clean Code ve Clean Architecture kitaplarının yazarı) 2000 tarihli "Design Principles and Design Patterns" makalesi, çürüyen bir tasarımın dört temel belirtisini sayar. Makale 25 yıllık ama belirtiler hâlâ her code review'da karşımıza çıkıyor. Bookstore üzerinden gidelim.

Rigidity (katılık): basit bir değişiklik bile zincirleme değişiklikler gerektirir. "Siparişe bir de push bildirimi ekleyelim" iki günlük iş gibi görünür; sonra OrderService'e, müşteri tablosuna, ayarlar ekranına, raporlara dokunman gerektiği ortaya çıkar ve iş üç haftaya yayılır. Martin'in gözlemi şu: bu belirsizlik yöneticileri kritik olmayan düzeltmelere izin vermekten korkutur ve tasarım sorunu zamanla bir yönetim politikasına dönüşür. "O module'e dokunmayın" kuralı olan her ekip bunu yaşamıştır.

Fragility (kırılganlık): bir değişiklik, kavramsal olarak hiç ilgisi olmayan yerleri bozar. Az önceki fatura örneği tam olarak bu. Kırılganlık arttıkça her düzeltme yeni hata üretir ve ekip ile kullanıcı arasındaki güven aşınır.

Immobility (taşınamazlık): bir parçayı başka bir yerde yeniden kullanmak, onu söküp çıkarmaktan daha pahalıdır. İade module'ü de müşteriye SMS göndermek istiyor diyelim. SMS gönderen kod OrderService'in içinde, Order tipine, sipariş numarası formatına ve SMTP (Simple Mail Transfer Protocol) ayarlarına bağlı. Sonunda biri SMS kodunu kopyalayıp iade module'üne yapıştırır. Martin'in deyişiyle yazılım yeniden kullanılmaz, yeniden yazılır.

Viscosity (akışkanlık direnci): doğru değişikliği yapmak, hack'i yapmaktan zordur. İki türü var. Tasarımın viscosity'si: push bildirimi için yeni bir abstraction kurmak yerine switch'e bir case eklemek beş dakika sürüyorsa, ekip her seferinde o beş dakikayı seçer. Ortamın viscosity'si: build on dakika sürüyorsa geliştirici az dosyaya dokunan değişikliği tercih eder, tasarım için doğru olanı değil. .NET dünyasında ikinci tür genelde büyük bir solution'da her şeyi her şeye bağlayan project referanslarıyla gelir.

Dördü birbirinden bağımsız değil. Katı bir sistemde insanlar hack'e yönelir (viscosity), hack'ler gizli bağlar yaratır (fragility), gizli bağlar parçaları söküp taşımayı imkânsız kılar (immobility).

Kök Neden: Değişim Değil, Bağımlılık

Bu belirtilerin nedeni olarak ilk akla gelen değişen gereksinimler. Martin aynı makalede bunu reddeder: gereksinimlerin değişeceğini hepimiz biliyoruz, requirements dokümanı projedeki en oynak dokümandır. Tasarımımız sürekli değişen gereksinimler karşısında çöküyorsa kusur gereksinimlerde değil, tasarımdadır.

Asıl neden plansız bağımlılıklar. Fatura örneğinde InvoiceWriter'ın OrderText'e bağımlılığı derleyici açısından tamamen meşru; ama bu bağımlılık faturayı SMS'in değişim nedenine de bağladı. Martin'in önerdiği çare "dependency firewall": değişikliğin öteye yayılmadığı sınırlar kurmak. Serideki bütün prensipler, bu sınırların nereye ve nasıl çekileceğine dair farklı cevaplar:

  • Coupling ve cohesion: iki parça arasındaki bağ ne kadar güçlü, bir parçanın içi ne kadar bir arada?
  • SRP (Single Responsibility Principle): bir class'ın kaç farklı değişim nedeni var?
  • OCP (Open/Closed Principle) ve DIP (Dependency Inversion Principle): yeni davranışı mevcut kodu açmadan ekleyebiliyor muyuz, bağımlılık oku hangi yöne bakıyor?
  • LSP (Liskov Substitution Principle) ve ISP (Interface Segregation Principle): bir abstraction'ın verdiği söz tutuluyor mu, client'lar ihtiyaç duymadıkları şeye bağlı mı?

Kısa Bir Tarih: Parnas'tan SOLID'e

SOLID çoğu zaman 2000'lerde birden ortaya çıkmış beş kural gibi anlatılır. Oysa her harfin arkasında on yıllarca süren bir tartışma var ve sıralamayı bilmek, prensiplerin neden öyle formüle edildiğini anlamayı kolaylaştırıyor:

YılKimFikir
1972David ParnasInformation hiding: sistemi akış adımlarına göre değil, değişmesi muhtemel tasarım kararlarına göre module'lere böl
1974Stevens, Myers, ConstantineStructured Design: coupling ve cohesion kavramları
1974Edsger DijkstraSeparation of concerns
1987–1994Barbara Liskov (ve Jeannette Wing)OOPSLA (Object-Oriented Programming, Systems, Languages and Applications konferansı) 1987 açılış konuşması "Data Abstraction and Hierarchy", 1994'te behavioral subtyping'in formal tanımı
1988Bertrand MeyerObject-Oriented Software Construction: Open/Closed Principle ve Design by Contract
1992Meilir Page-JonesConnascence: coupling'i türüne ve gücüne göre derecelendirmek
1996–2000Robert C. MartinOCP, LSP, DIP, ISP üzerine makaleler; 2000'de "Design Principles and Design Patterns"
2002Robert C. MartinAgile Software Development kitabı: SRP beş prensibin yanına girer
~2004Michael Feathers (Working Effectively with Legacy Code kitabının yazarı)Prensipleri yeniden sıralayıp SOLID kısaltmasını önerir

Tablodan iki ilginç sonuç çıkıyor. Birincisi, 2000 tarihli makalede SRP yok; makale OCP, LSP, DIP ve ISP'yi class prensipleri olarak, ardından altı package prensibini anlatır. SRP'nin kökü Parnas'a ve Constantine'in cohesion'ına uzanır; Martin 2014'teki SRP yazısında bu fikirleri 1990'ların sonunda tek bir isim altında topladığını, ismi Bertrand Meyer'den almış olabileceğini ama emin olamadığını söyler. İkincisi, kısaltma prensiplerden çok sonra geldi. Beş harfin yan yana gelmesi biraz da akılda kalıcılık için; prensiplerin birbirine eşit ağırlıkta olduğu ya da başka prensip olmadığı anlamına gelmiyor. Martin'in kendisi de 2000 makalesinde OCP'yi "en önemlisi", DIP'i ise OCP'nin temel mekanizması olarak sunar.

Parnas'ın 1972 makalesi bugün hâlâ okunmaya değer. Aynı küçük programı iki şekilde module'lere ayırır: biri akış şemasındaki adımlara göre (oku, kaydır, sırala, yazdır), diğeri saklanacak tasarım kararlarına göre (verinin bellekte nasıl tutulduğu gibi). İki sürüm de çalışır; fark, bir tasarım kararı değiştiğinde kaç module'ün etkilendiğinde ortaya çıkar. "Değişmesi muhtemel olanı sakla" fikri, bu serideki hemen her prensibin atasıdır.

.NET'in Kendi Kütüphanesinde Bir Örnek

Prensiplerin soyut kalmaması için seri boyunca her birinin .NET'in kendi kodunda nerede durduğuna bakacağız. İlk örnek, .NET'in en eski abstraction'larından biri olan Stream:

using System.IO.Compression;
using System.Text;

byte[] catalog = Encoding.UTF8.GetBytes(
    string.Concat(Enumerable.Repeat("978-0441013593;Dune;Frank Herbert;12.99\n", 500)));

using MemoryStream memory = new();
Console.WriteLine($"MemoryStream: {catalog.Length} -> {Compress(catalog, memory)} bytes");

string path = Path.Combine(Path.GetTempPath(), "catalog.csv.gz");
using (FileStream file = File.Create(path))
{
    Console.WriteLine($"FileStream:   {catalog.Length} -> {Compress(catalog, file)} bytes");
}
File.Delete(path);

// GZipStream hedefin bellek mi dosya mı olduğunu bilmiyor
static long Compress(byte[] data, Stream destination)
{
    using (GZipStream gzip = new(destination, CompressionLevel.SmallestSize, leaveOpen: true))
    {
        gzip.Write(data);
    }

    return destination.Length;
}
MemoryStream: 20000 -> 140 bytes
FileStream:   20000 -> 140 bytes

Compress method'u ve GZipStream hedefin ne olduğunu bilmiyor; ellerinde yalnızca bir Stream var. Aynı kod MemoryStream'e, FileStream'e, bir network stream'ine ya da senin yazacağın bir stream'e çalışır. Yeni bir depolama türü eklemek için GZipStream'in kaynak koduna dokunmak gerekmiyor (OCP), sıkıştırma kodu somut dosya API'sine (Application Programming Interface) değil abstraction'a bağlı (DIP). Aynı tasarım ASP.NET Core'un request body'sinde, HttpClient'ın content'inde ve System.Text.Json'ın stream overload'larında tekrar karşına çıkacak.

Microsoft Learn'deki "Architectural principles" sayfası da benzer bir liste verir: separation of concerns, encapsulation, dependency inversion, explicit dependencies, single responsibility, DRY, persistence ignorance ve bounded context. Bu serinin kapsamı bunların çoğunu içeriyor; bounded context ise ilerideki Domain-Driven Design serisinin konusu.

Prensip Kural Değildir: Her Biri Bir Trade-off'u Adlandırır

Prensiplerle ilgili en pahalı yanlış anlama, onları derleyici kuralı gibi okumak. "Bu class SRP'yi ihlal ediyor" cümlesi code review'da bir karar değil, bir tartışmanın başlangıcı olmalı. Her prensip bir gerilimi adlandırır ve diğer tarafta da gerçek bir bedel vardır.

SRP'yi sonuna kadar zorlarsan her method'u ayrı bir class'a koyarsın ve bir değişiklik için on dosya açarsın. OCP'yi sonuna kadar zorlarsan hiç değişmeyecek yerlere extension point koyarsın ve kod okunmaz hâle gelir. DIP'i sonuna kadar zorlarsan her class'ın bir IXxx ikizi olur. DRY'ı sonuna kadar zorlarsan fatura örneğindeki gibi ilgisiz kodları birbirine bağlarsın. Bu serideki her yazının sonunda "aşırıya kaçınca" ve "ne zaman kullanma" bölümleri bu yüzden var.

Pratikte işe yarayan soru şu: "Bu tasarım, bu sistemde gerçekten beklediğimiz değişikliği ucuzlatıyor mu?" Değişim beklenmeyen bir yerde prensip uygulamak maliyet, değişimin sürekli geldiği bir yerde uygulamamak borçtur. İlerideki Clean Code serisinde aynı soruyu method ve isim seviyesinde, bu seride class, interface ve project seviyesinde soracağız.

Seri Boyunca Kullanacağımız Örnek

Yazılar ortak bir örnek üzerinden ilerleyecek: Bookstore'un sipariş module'ü ve ona bağlı bildirimler (e-posta, SMS, ileride push). Başlangıç noktası, pek çok gerçek projede göreceğin türden "çalışan ama bağlı" bir kod. Orders.cs:

namespace Bookstore.Orders;

public enum NotificationChannel { Email, Sms }

public record Customer(int Id, string Name, string Email, string Phone, NotificationChannel PreferredChannel);

public record OrderItem(string Isbn, string Title, int Quantity, decimal UnitPrice);

public sealed class Order(int id, Customer customer, IReadOnlyList<OrderItem> items)
{
    public int Id { get; } = id;
    public Customer Customer { get; } = customer;
    public IReadOnlyList<OrderItem> Items { get; } = items;
    public decimal Total => Items.Sum(i => i.Quantity * i.UnitPrice);
}

public sealed class OrderService
{
    private readonly List<Order> _orders = [];

    public Order PlaceOrder(Customer customer, IReadOnlyList<OrderItem> items)
    {
        if (items.Count == 0)
            throw new ArgumentException("An order needs at least one item.", nameof(items));

        Order order = new(1001 + _orders.Count, customer, items);
        _orders.Add(order);

        // Bildirim kararı sipariş kodunun ortasında veriliyor
        string message = $"Order {order.Id} confirmed: {order.Total:0.00} EUR";
        switch (customer.PreferredChannel)
        {
            case NotificationChannel.Email:
                Console.WriteLine($"[smtp] to {customer.Email}: {message}");
                break;
            case NotificationChannel.Sms:
                Console.WriteLine($"[sms] to {customer.Phone}: {message}");
                break;
        }

        return order;
    }
}

Program.cs:

using System.Globalization;
using Bookstore.Orders;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

OrderService orders = new();
Customer ada = new(1, "Ada", "ada@example.com", "+905550000001", NotificationChannel.Email);
Customer alan = new(2, "Alan", "alan@example.com", "+905550000002", NotificationChannel.Sms);

orders.PlaceOrder(ada, [new OrderItem("978-0441013593", "Dune", 2, 12.99m)]);
orders.PlaceOrder(alan, [new OrderItem("978-0141439518", "Emma", 1, 7.50m)]);
[smtp] to ada@example.com: Order 1001 confirmed: 25.98 EUR
[sms] to +905550000002: Order 1002 confirmed: 7.50 EUR

Bu kodda bugün bir sorun yok; iki müşteri, iki kanal, her şey çalışıyor. Ama değişimin nereden geleceğini tahmin etmek zor değil. Pazarlama push bildirimi isteyecek, switch büyüyecek. Muhasebe sipariş toplamının nasıl hesaplandığını değiştirmek isteyecek; o kod da bildirimle aynı method'da. Testte gerçekten SMS göndermemek için Console.WriteLine yerine bir sahte gönderici takmak isteyeceğiz, ama gönderici method'un içine gömülü. Sonraki yazılarda bu kodu prensip prensip ele alacağız: önce coupling ve cohesion ile ölçeceğiz, sonra SRP ile sorumlulukları ayıracak, OCP ile switch'i kapatacak, LSP ve ISP ile abstraction'ların sözlerini sınayacak, DIP ile bağımlılık okunu çevireceğiz.

Sık Düşülen Tuzaklar

Prensibi kısaltmasıyla tartışmak. "Burada OCP ihlali var" demek, hangi değişikliğin pahalılaştığını söylemiyorsa kimseyi ikna etmez. Bunun yerine "push kanalı geldiğinde bu dosya ve şu iki dosya değişecek" demek hem somut hem doğrulanabilir.

Prensibi acı gelmeden uygulamak. İlk günden her şeyi interface'lerin arkasına saklamak, değişimin hangi yönden geleceğini bilmeden duvar örmek demek. Duvar yanlış yerde durursa gerçek değişiklik onu delmek zorunda kalır ve elinde hem duvar hem delik olur.

Prensip ile pattern'i karıştırmak. Strategy, Decorator, Factory gibi design pattern'ler belirli problemlere hazır çözümlerdir; prensipler ise bir çözümün iyi olup olmadığını değerlendirmek için kullandığın ölçütlerdir. Pattern'lerin kendisi ilerideki Design Patterns serisinin konusu. Bir pattern'i "SOLID olsun diye" eklemek, ölçütü hedefle karıştırmaktır.

SOLID'i yalnızca OOP'ye (Object-Oriented Programming, nesne yönelimli programlama) ait sanmak. Fonksiyonel bir kod tabanında da bir module'ün tek bir değişim nedeni olması, bir fonksiyonun kendisine verilen fonksiyonun sözüne güvenebilmesi ve üst seviye politikanın alt seviye ayrıntıya bağlı olmaması anlamlıdır. C#'ta bunu Func<> parametreleriyle de yapabiliyoruz; OCP yazısında göreceğiz.

Belirtileri kişilere bağlamak. "Bu module'ü kim yazdıysa..." diye başlayan cümleler genelde haksızdır. Rigidity ve fragility çoğunlukla tek bir kötü karardan değil, makul görünen yüzlerce küçük bağımlılığın birikmesinden doğar. Fatura örneğindeki arkadaş da kendi testini çalıştırmıştı.

Küçük Bir Deneme

★☆☆ İlk örnekteki OrderText.Summary'ye bir de e-posta konu satırının bağlı olduğunu düşün. SMS için yapılan kısaltma e-postayı nasıl etkiler? Üç client'ı da düzgün çalıştıran en küçük değişikliği yaz ve çıktıyı karşılaştır.

★★☆ Kendi projende son altı ayda en çok değişen beş dosyayı bul (git log --since="6 months ago" --format= --name-only | sort | uniq -c | sort -rn | head). Her biri için "bu dosyayı kim, hangi nedenle değiştiriyor?" sorusunu cevapla. Birden fazla neden çıkan dosya var mı?

★★★ Seri örneğindeki OrderService'e push kanalını ekle. Kaç yere dokunman gerekti? Dokunduğun her satırı rigidity, fragility, immobility ve viscosity'den hangisine örnek olduğuna göre not et; bu listeyi serinin sonunda tekrar açacağız.

Bir sonraki yazıda bu sezgileri ölçülebilir hâle getiriyoruz: coupling'in türleri ve cohesion'ın seviyeleri, iki parçanın ne kadar güçlü bağlı olduğunu derecelendiren connascence ve order.Customer.Wallet.Balance gibi zincirlerin neden tehlikeli olduğunu anlatan Law of Demeter.