C# örnekleri ile SOLID prensipleri

Halil İbrahim Kocaöz
5 min readMay 7, 2023

SOLID kısaltması, yazılım mühendisliği alanında kullanılan “Single Responsibility”, “Open-Closed”, “Liskov Substitution”, “Interface Segregation” ve “Dependency Inversion” prensiplerinin baş harflerinden oluşur. Bu beş prensip, SOLID prensipleri olarak bilinir ve SOLID, yazılım geliştirme sürecinde yazılımın kaliteli, esnek, sürdürülebilir ve anlaşılabilir olmasını sağlamak için kullanılan bir dizi tasarım prensibidir.

Bu prensipler, nesne yönelimli programlama (OOP) prensiplerini destekler ve yapıyı daha modüler ve bakımı kolay hale getirmeye yardımcı olur.

Single Responsibility Principle — SRP

Her classın yalnızca bir sorumluluğu olmalıdır. Bir classın birden fazla sorumluluğu olması, classın karmaşıklığını artırır ve bakımını zorlaştırır.

public class Car
{
public int Id { get; set; }
public string Brand { get; set; }
public string Model { get; set; }
public decimal DailyPrice { get; set; }


public bool Rent(DateTime startDate, DateTime endDate, Customer customer)
{
// Kiralama işlemini gerçekleştiren kod
return true;
}

public bool Deliver(DateTime deliverDate)
{
// Araç teslim işlemini gerçekleştiren kod
return true;
}
}

Yukarıdaki örnekte, Car classı iki farklı sorumluluğu yerine getirmektedir. Bir yandan, araç özelliklerini içerir ve diğer yandan, kiralama işlemini gerçekleştirir. Bu classın SRP’ye uymadığı anlamına gelir.

Bunun yerine, Car classı, yalnızca araç özelliklerini içerecek şekilde tasarlanabilir ve kiralama işlemini gerçekleştiren ayrı bir yapı oluşturulabilir.

public class Car
{
public int Id { get; set; }
public string Brand { get; set; }
public string Model { get; set; }
public decimal DailyPrice { get; set; }
}

public class RentingService
{
public bool Rent(Car car, DateTime startDate, DateTime endDate, Customer customer)
{
// Kiralama işlemini gerçekleştiren kod
return true;
}

public bool Deliver(Car car, DateTime deliverDate)
{
// Araç teslim işlemini gerçekleştiren kod
return true;
}
}

Open/Closed Principle — OCP

Yazılım varlıkları (classlar, modüller, fonksiyonlar), değiştirilmeye kapalı ama geliştirilmeye ve genişletilmeye açık olmalıdır. Bu prensip, mevcut kodu değiştirmeden yeni özellikler eklemeyi mümkün kılar.

Örneğin bir ödeme alması gereken yapıda ilk başta kullanıcılarına sadece nakit ödeme seçeneği sunuyor ve buna göre yazılmış bir yapı var.

public class PaymentService
{
public bool CashPay(decimal amount)
{
// Nakit ile ödeme işlemini gerçekleştirir
return true;
}
public bool CreditCardPay(decimal amount, string cardNumber, string expirationDate, string ccv)
{
// Kredi kartı ile ödeme işlemini gerçekleştirir
return true;
}
}

Fakat uygulamayı kullanacak başka bir kullanıcı/müşteri farklı bir yöntem olan kredi kartı ile de ödeme alabiliyor ve bunun da olmasını bekliyor.

public abstract class Payment
{
public abstract bool Pay(decimal amount);
}

public class CashPayment : Payment
{
public override bool Pay(decima amount)
{
// Nakit ile ödeme işlemini gerçekleştirir
return true;
}
}

public class CreditCardPayment: Payment
{
public string CardNumber { get; set; }
public string ExpirationDate { get; set; }
public string SecurityCode { get; set; }

public override bool Pay(decimal amount)
{
// Kredi kartı ile ödeme işlemini gerçekleştirir
return true;
}
}

public class PaymentService
{
public bool Pay(Payment payment, decimal amount)
{
return payment.Pay(amount);
}
}

Yukarıdaki örnekte, Payment classı açık, ancak kapalıdır; yani yeni ödeme yöntemleri eklenerek yapıda var olan eskiden implementasyonu yapılmış özellikler değiştirilmeden, uygulamanın özellikleri genişletilebilir ve geliştirme yapılabilir. Ayrıca, PaymentService, Payment classından türetilen herhangi bir nesneyi kabul edebilir, böylece uygulama ödeme yöntemleri arasında geçiş yapabilir ve müşteriye/kullanıcıya daha fazla seçenek sunabilir.

Liskov Substitution Principle — LSP

Alt classlar, üst classların yerine geçebilmelidir. Bu prensip, bir üst classın kullanıldığı her yerde, onun alt classlarının da kullanılabileceği anlamına gelir.

Örnek olarak, bir dosya sistemi düşünelim. Dosyaları temsil etmek için bir temel class olan “File” ve bu classtan türeyen “TextFile” ve “PictureFile” classları olsun.

public abstract class File
{
public abstract void Open();
}

public class TextFile : File
{
public override void Open()
{
Console.WriteLine("Metin dosyası açılıyor.");
// Metin dosyasını açmak için gerekli işlemler
}
}

public class PictureFile : File
{
public override void Open()
{
Console.WriteLine("Resim dosyası açılıyor.");
// Resim dosyasını açmak için gerekli işlemler
}
}

Bu örnekte, File classı temel özellikleri tanımlar ve Open() metodunu içerir. PictureFile ve TextFile classları File classından türetilir ve Open() metodunu ezmek suretiyle kendi dosya açma işlemlerini tanımlarlar.

LSP’yi uygulamak için, File classından türeyen herhangi bir nesne, Open() metodunu çağırmak için kullanılabilir.

public void OpenFiles(List<File> files) 
{
foreach(file in files)
{
file.Open();
}
}

Yukarıdaki örnekte, File classından türetilen herhangi bir nesne, Open() metodunu çağırmak için kullanılabilir. Bu nedenle, TextFile ve PictureFile nesneleri Open() metodunu çağırmak için kullanılabilir ve beklenen sonuçlar üretilir.

Interface Segregation Principle — ISP

Mümkünse daha küçük, spesifik interfaceler oluşturulmalıdır. Bu prensip, bir interfacein çok fazla işlevselliğe sahip olmaması gerektiğini belirtir.

Bu prensibi implement etmek için, bir yazdırılabilir sistem örneği oluşturabiliriz.

public interface IPrinter 
{
void Print();
void Scan();
void Fax();
}

public class MultifunctionPrinter : IPrinter
{
public void Print()
{
Console.WriteLine("Printing...");
}

public void Scan()
{
Console.WriteLine("Scanning...");
}

public void Fax()
{
Console.WriteLine("Faxing...");
}
}

public class LaserPrinter : IPrinter
{
public void Print() {
Console.WriteLine("Printing...");
}

public void Scan() {
throw new NotImplementedException();
}

public void Fax() {
throw new NotImplementedException();
}
}

public class DocumentScanner : IPrinter
{
public void Print()
{
throw new NotImplementedException();
}

public void Scan()
{
Console.WriteLine("Scanning...");
}

public void Fax()
{
throw new NotImplementedException();
}
}

Bu örnekte, IPrinter interfaceı yazdırılabilir sistem özelliklerini tanımlar. Ancak, MultifunctionPrinter, LaserPrinter ve DocumentScanner classları, bu interface’ı implemente ettiği için, tüm özelliklere sahip olmak durumunda. Bu da ISP prensibinin ihlal edilmesine neden olur. Örneğin, LaserPrinter classı tarama ve faks özelliklerini desteklemediği için, bu özellikler çağrıldığında NotImplementedException hatası fırlatılır.

Bu durumu gidermek için kodunuzu şu şekile evirebilirsiniz:

public interface IPrinter 
{
void Print();
}
public interface IScanner
{
void Scan();
}
public interface IFax
{
void Fax();
}

public class MultifunctionPrinter : IPrinter, IScanner, IFax
{
public void Print()
{
Console.WriteLine("Printing...");
}

public void Scan()
{
Console.WriteLine("Scanning...");
}

public void Fax()
{
Console.WriteLine("Faxing...");
}
}

public class LaserPrinter : IPrinter
{
public void Print()
{
Console.WriteLine("Printing...");
}
}

public class DocumentScanner : IScanner
{
public void Scan()
{
Console.WriteLine("Scanning...");
}
}

Dependency Inversion Principle — DIP

Yüksek seviyeli modüller, düşük seviyeli modüllere bağımlı olmamalıdır. Bunun yerine, her iki seviyedeki modüller de soyutlamalara (abstraction) bağımlı olmalıdır.

Bir restoranın sipariş alma ve hazırlama işlemlerini yöneten bir sistem üzerinden örnek oluşturalım.

public interface IOrderReceiver 
{
void ReceiveOrder(Order order);
}

public class OrderReceiver : IOrderReceiver
{
public void ReceiveOrder(Order order)
{
// sipariş alma işlemleri
}
}

public interface IOrderProcessor
{
void ProcessOrder(Order order);
}

public class OrderProcessor : IOrderProcessor
{
public void ProcessOrder(Order order)
{
// sipariş hazırlama işlemleri
}
}

public class Order
{
public int Id { get; set; }
public List<MenuItem> Items { get; set; }
public decimal TotalPrice { get; set; }
}

public class MenuItem
{
public string Name { get; set; }
public decimal Price { get; set; }
}

public class Restaurant
{
private readonly IOrderReceiver orderReceiver;
private readonly IOrderProcessor orderProcessor;

public Restaurant(IOrderReceiver orderReceiver, IOrderProcessor orderProcessor)
{
this.orderReceiver = orderReceiver;
this.orderProcessor = orderProcessor;
}

public void TakeOrder(Order order)
{
orderReceiver.ReceiveOrder(order);
orderProcessor.ProcessOrder(order);
}
}

Örnekte, IOrderReceiver ve IOrderProcessor interfaceleri, sipariş alma ve hazırlama işlemlerini tanımlamak için kullanılır. OrderReceiver ve OrderProcessor classı, bu interfaceleri implemente eder ve gerçek işlemleri gerçekleştirir. Bu sayede, Restaurant classı, IOrderReceiver ve IOrderProcessor interfacelerine bağımlı hale gelir ve somut classlarla değil, interfacelerin birleştirilmesiyle oluşan bağımlılıklara sahip olur.

Sonuç olarak, DIP prensibi, yüksek seviyeli modüllerin düşük seviyeli modüllerden soyutlamalara (abstractions) bağımlı olması gerektiğini ve somut classlar yerine interfacelerin kullanılmasını vurgulamaktadır. Bu sayede, uygulama daha esnek ve değiştirilebilir hale gelir.

Bu yazı GPT-3'den yararlanılarak yazıldı.

--

--