Her iş görüşmesinde sorulan, sorulmasada dokundurulan o konu SOLID prensiplerine gelin bir bakış atalım.
SOLID aslında tek başına bir şey değil. Bir kurallar dizisidir.
S (single responsibility)
O (Open Closed)
L (Liskov Substitution)
I (Interface Sagregation)
D (Dependency Inversion)
Gördüğünüz gibi şair yürekli bir abimiz askortiş yapmış buda milletin başına bela olmuş, mu acaba? 🙂 Tabi ki hayır. Aslında buradakilerin hepsi birer kural ve bunlar uygulandığında projelerimizin geliştirilebilirliği ve anlaşılabilirliği en üst seviyeye çıkmış oluyor. Bu kurallara uyarak yazılan bir kod temiz ve anlaşılır oluyor. (Elimde bununla yazılmış open source bir proje var bir sonraki yazımda bundan bahsedeceğim.)
Gelelim bu kuralları açıklamaya ve örneklerle her birinin ne olduğunu anlatmaya.
SINGLE RESPONSIBILITY
Evet adında anlaşılacağı ve ingilizcesi olan herkesin tahmin edebileceği gibi tek sorumluluk taşıma anlamına geliyor. Bir sınıf yada bileşenin tek bir sorumluluğu olmalıdır. Bu şekilde kod daha anlaşılır ve bakımı daha kolay yapılabilir hale gelir. Ne demek istiyorum, yani şöyle mesela bir mesaj şablonu kaydedip mesaj şablonlarının parse edilip kullanıcıya özel bir şekilde geleceği bir kod yazacaksınız. Şimdi bu ikisini birbirinden ayırmanız gerekiyor. Yani mesaj şablonunu kaydetme, silme, güncelleme gibi işlemleri bir sınıfa, mesajı parse etme kullanıcıya göre getirme işlemlerini bir sınıfa ve metoda vermeliyiz. Bunların hepsi mesaj şablonu ile ilgili diye karga tulumba bir sınıf içine doldurmamalıyız.
public class MessageService {
public virtual string GetMessageById(int id) {
}
public virtual void Insert(string message) {
}
public virtual void Delete(int id) {
}
public virtual void Update(int id, string message) {
}
}
public class MessageParserService {
public virtual string ParseMessage(string message, User user) {
}
}
Örnekteki gibi iki sınıfa ayırdık ve işlemleri iki sınıf üzerinden yapıyorsak bu sınıfların ikisinin de sorumlulukları belli aynı zamanda metotlarının da almış olduğu sorumluluklar tek bir tane. Buda bu metotları değiştirmek yada güncellemek istediğinizde işinizi oldukça kolaylaştıracaktır. Mesela mesajı farklı farklı metotlar içinde parse etmeyi kodu çoklayarak ayrı ayrı yerlerde yaptığınızı düşünün. Tek bir şeyi değiştirmek istediniz parse işleminde tek bir metotda değiştirmek yerine kodun bir çok yerinde değiştirmek zorunda kalacaksınız. O yüzden parse işlemini bu sınıf ve bu metotdan başka kimse yapmamalıdır. Bu başka sınıf ve metotlara başka sorumluluklar yüklemek anlamına geliyor.
OPEN CLOSED İLKESİ
Evet belki de uygularken zorlanacağımız şeylerden bir tanesi bu ilke. Kodun değiştirilememesi ve genişletilebilmeye açık olması demek oluyor. Bu şekilde bir sınıf, modül veya metodu değiştirmemeliyiz ama kalıtım ile ona yeni özellikler kazandırabiliriz. Ne demek istiyorum bir örnek ile açıklayayım. Mesela diyelimki bir ödeme sistemi yazıyorsunuz ve birden çok ödeme yöntemini destekleyeceksiniz. Ama sürekli ödeme yöntemi geliyor önce kredi kartı yaptın. Sonra dediki patron ya ben yurt dışına açılacağım paypal da destekleyelim. Onu da yazdın ekledin. Sonra dedi ki ya bana birde banka havalesi yaz yiğenim. Haydaa. Koda metot ekleyip duruyorsun. Ifler ile ayırıyorsun şu ile gelirse bunu yap bu ile gelirse bunu yap bir sürü ödeme oldu if uzadı gidiyor ödeme yapan metodunda ve şu şekilde görünüyor.
public class PaymentProcessor
{
public void ProcessPayment(string paymentType, decimal amount)
{
if (paymentType == "credit_card")
{
ProcessCreditCard(amount);
}
else if (paymentType == "paypal")
{
ProcessPayPal(amount);
}
else if (paymentType == "bank_transfer")
{
ProcessBankTransfer(amount);
}
else
{
throw new ArgumentException("Unknown payment type");
}
}
private void ProcessCreditCard(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount}");
}
private void ProcessPayPal(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount}");
}
private void ProcessBankTransfer(decimal amount)
{
Console.WriteLine($"Processing bank transfer of {amount}");
}
}
else if else if gideceğiz sanırım. Hayır işte open closed özelliği burda devreye giriyor. PaymentProcessoru öyle bir hale getirelim ki her seferinde if yazmadan bu işi çözebileyim.
public abstract class PaymentProcessor
{
public abstract void Process(decimal amount);
}
public class CreditCardPayment : PaymentProcessor
{
public override void Process(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount}");
}
}
public class PayPalPayment : PaymentProcessor
{
public override void Process(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount}");
}
}
public class BankTransferPayment : PaymentProcessor
{
public override void Process(decimal amount)
{
Console.WriteLine($"Processing bank transfer of {amount}");
}
}
Gördüğünüz gibi. PaymentProcessor sadece process e sahip ve tip falan almıyor. Hepsi sınıflara ayrılmış ve iflere ve paymentprocessoru oynamama gerek kalmadan yeni sınıf ekleyerek bu sınıfın yapmasını istediğim şeyi yaptırabiliyorum. Ee peki bunu nasıl kullanacağım diyorsun. Bu yazının konusu değil ama örnek olması için bir Handler yazılıp çok kolay bir biçimnde buradaki sınıfları kullanabiliriz. Aşağıdaki örneği inceleyelim.
public class PaymentHandler
{
private readonly PaymentProcessor _processor;
public PaymentHandler(PaymentProcessor processor)
{
_processor = processor;
}
public void Handle(decimal amount)
{
_processor.Process(amount);
}
}
class Program
{
static void Main()
{
PaymentHandler payment = new PaymentHandler(new CreditCardPayment());
payment.Handle(100m);
payment = new PaymentHandler(new PayPalPayment());
payment.Handle(200m);
payment = new PaymentHandler(new BankTransferPayment());
payment.Handle(300m);
}
}
Evet gördüğünüz gibi handle sınıfı gönderilen tipte paymenti işliyor. Iflerle ayırma yok kodu tekrar yazma yok. Sizden patron yeni payment tipi eklemenizi isterse şimdi patron düşünsün 🙂
Liskov Substutition – Liskovun Yerine Ben Geçeyim Prensibi 🙂
Evet yukardaki iki prensip bile kodumuzu yeterince topladı. Şimdi bu prensibin amacına bir bakalım. Aslında yukarda Open Closed kısmında veridiğimiz örnek çok güzel bir şekilde bu prensibin amacını da karşılıyor. Ne demek istediğimi yazayım sonra başka bir örnek ile inceleyelim konuyu. Bu prensibin ana mottosu aslında kalıtım yaptığımız sınıfların genişletmediğimiz özelliklerinin yeni yarattığımız sınıfta da sorunsuz çalışmasını amaçlamaktadır. Şimdi diyelimki aşağıdaki gibi bir kod var elimizde. Bir dikdörtgen sınıfmız var ve bunun çevresini hesaplayan bir metodumuz var. Gitmişiz birde kare türetmişiz çevre hesaplayan sınıfı kullanacağız ya. Sonra düşünmüşüz ya karede bu çevre hesaplaması metodunu bozmadan nasıl aynı metotları kullanırım gibip uzunluk ve genişliği virtual yapıp set edilme aşamasında ikisini eşitlemişiz 🙂 hangisi en son set edilirse karenin kenar uzunluğu o olmuş. Tamamen saçmalık bir kod 🙂
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int GetArea()
{
return Width * Height;
}
}
public class Square : Rectangle
{
public override int Width
{
set { base.Width = base.Height = value; }
}
public override int Height
{
set { base.Height = base.Width = value; }
}
}
Sonuç olarak bu kod çok saçma. Ve gidip base classın propertylerinin davranışlarını değiştiriyoruz. Peki ne olmalıydı ?
public class Shape
{
public virtual int GetArea()
{
return 0;
}
}
public class Rectangle : Shape
{
public int Width { get; set; }
public int Height { get; set; }
public override int GetArea()
{
return Width * Height;
}
}
public class Square : Shape
{
public int Side { get; set; }
public override int GetArea()
{
return Side * Side;
}
}
Gördüğünüz gibi aslında kare de dikdörtgende şekil isminde bir nesneden türemeliydi. ve bunların alan hesabı metodu her biri için override edilmeli ve kendi propertylerine sahip olmalıydılar. Bu şekilde Shape nesnesi bu diğer iki nesnenin yerine geçebilir ve Shape nesnesini kullanabiliriz nasıl mı?
Shape rect = new Rectangle { Width = 5, Height = 10 };
Console.WriteLine($"Rectangle Area: {rect.GetArea()}"); // Output: 50
Shape square = new Square { Side = 5 };
Console.WriteLine($"Square Area: {square.GetArea()}"); // Output: 25
Umarım bu kısım açıklayıcı olmuştur.
INTERFACE SAGREGATION
Interface sagregation arayüz ayrımı anlamına gelmektedir. Yani kısaca şunu söyleyelibirim her arayüzün olabildiğince minimize edilmiş ve sadece ortak paydada buluşan şeyler için kullanılması gerekiyor. Neden bahsediyorum gelin bir bakalım. Bu prensibe uymayan örneğimizde havyan arayüzümüz olsun ve bu hayvan arayüzü bir takım metotları barındırsın.
public interface IAnimal {
void Fly();
void Walk();
void Swim();
}
public class Dog: IAnimal {
public void Fly(){
throw new NotImplementedException(); // Köpekler uçamaz
}
public void Walk(){
...
}
public void Swim(){
throw new NotImplementedException(); // Köpekler yüzemez
}
}
Gördüğünüz gibi hiç alakası olmayan iki metot tanımladığımız köpek classında boş boş duruyor. (Aslında köpekler yüzebilir 🙂 Ama konumuz bu değil.)
Peki ne olmalıydı ?
public interface IFlyable {
void Fly();
}
public interface IWalkable {
void Walk();
}
public interface ISwimmable {
void Swim();
}
public class Dog: IWalkable {
public void Walk(){
...
}
}
Evet gördüğünüz gibi arayüzlerimizi ayırdık ve sadece köpeğin ihtiyacı olan özelliği köpeğe atamış olduk.
DEPENDENCY INVERSION
Buradaki temel amaç bağımlıkları gevşetmek ve tersine çevirmektir. Aslında burada bütün olay interfaceler ve baseclasslar üzerinden yürümeye açıktır. Ne demek istiyorum. Şöyleki mesela bir mesaj servisi yazıyoruz. Bu mesaj serivisi email ile notification atacak ama yarın öbür gün tabi sms servisi de eklenebilir. Şimdi Dependecy inversion prensibine uymayan örneğe bir bakalım.
public class EmailService
{
public void SendEmail(string message)
{
Console.WriteLine($"Sending Email: {message}");
}
}
public class Notification
{
private EmailService _emailService;
public Notification()
{
_emailService = new EmailService();
}
public void Send(string message)
{
_emailService.SendEmail(message);
}
}
// Usage
class Program
{
static void Main()
{
Notification notification = new Notification();
notification.Send("Hello, this is a notification!");
}
}
Şimdi notification oluşturuyor ve e mail gönderiyor. Peki ya sms ile gönderecek olsaydık ne yapacaktık? Gidip send içini değiştrecek ve oraya sms gönderecek kodu yazacaktık değil mi? Peki dönem dönem bunu değiştirdiklerini düşünün iyice işin içinden çıkılmaz bir şekilde kodu sürekli değiştirmek gerek yada parametre vs geçilecek bu kısıma ifler yazılacak kod gerçekten iğrenç bir hal alacak 🙂 Peki şimdi temiz örneğimize bakalım.
// Abstraction
public interface IMessageService
{
void SendMessage(string message);
}
// Low-level Module: Email Service
public class EmailService : IMessageService
{
public void SendMessage(string message)
{
Console.WriteLine($"Sending Email: {message}");
}
}
// Low-level Module: SMS Service
public class SMSService : IMessageService
{
public void SendMessage(string message)
{
Console.WriteLine($"Sending SMS: {message}");
}
}
// High-level Module
public class Notification
{
private readonly IMessageService _messageService;
public Notification(IMessageService messageService)
{
_messageService = messageService;
}
public void Send(string message)
{
_messageService.SendMessage(message);
}
}
// Usage
class Program
{
static void Main()
{
IMessageService emailService = new EmailService();
Notification notification = new Notification(emailService);
notification.Send("Hello, this is an email notification!");
IMessageService smsService = new SMSService();
Notification smsNotification = new Notification(smsService);
smsNotification.Send("Hello, this is an SMS notification!");
}
}
Gördüğünüz gibi nofication içine eğer yarattığımız bir IMessageService arayüzünü parametre olarak inject edersek notification tam olarak bizim istediğimiz şeyi gönderektir. Programın classının içine bakacak olursanız hem sms hem e mail notification sınıfında artık herhangi bir değişikliğe gerek kalmadan çalışmaya başladı.
Evet bu son prensipti. Bunları uygulamak zor evet ama projelerimizde kullanmaya gayret ettikçe bir bakmışsınızki başka türlü kod yazamıyorsunuz. Başlarda çok fazla class açıyorum çok fazla metot tanımı yapıyorum bu ne ya diye düşünebilirsiniz ama örneklerden gördüğünüz gibi bakımı ve genişletilebilirliği en üst seviyeye taşıyorsunuz. 3 – 5 satırlık bir kod ile bir kaç basit iş yapan program yazıyorsanız ve yazdığınız programın bir geleceği yok ise tabi ki istediğiniz gibi yazabilirsiniz. Ama ben çok falza proje gördüm sadece mesela bir bildirim yöntemi değişikiği için projenin her yerini değiştirmek zorunda olan ve en az bir ay eforu çıkan. İşte bu prensiplere uyarsanız başta biraz zaman harcayabilirsiniz ama ilerde hayatınızı kurtaracaktır emin olun.
Okuduğunuz için çok teşekkür ederim bir sonraki yazıda görüşmek dileği ile sağlıkla kalın.
İlk Yorumu Siz Yapın