Nesneye Yönelik Programlamada SOLID prensipleri, yazılım tasarımını daha anlaşılır, esnek ve sürdürülebilir hale getirmek için kullanılan beş tasarım ilkesi içerir. SOLID kelimesi, Michael Feathers tarafından sunulan bu ilkelerin baş harfleridir. Bu ilkeler, Robert C. Martin tarafından desteklenen birçok ilkenin bir alt kümesidir.
5 SOLID prensibi:
- Single-responsibility principle
- Open-closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
Single-Responsibility Principle (SRP)
Bu prensibe göre bir sınıfın yalnızca tek bir sorumluluğu olmalıdır. Başka bir deyişle, yazılımınız içinde birden fazla farkı görevi yapan kısım olmamalıdır. Bu ilkede, yazılım kodunda her bir sınıfın, modülün veya metodun yalnızca bir iş yapması istenir. Örneğin, sınıf sadece konusu ile ilgili değişken ve yöntemleri içermelidir. Bu durumu basit bir örnek üzerinden anlatalım.
1 2 3 4 5 6 7 8 9 10 |
class Musteri{ public void Ekle(){ try{ // Veritabanı Ekleme kodu } catch (Exception ex){ //Kodda hata oluştuğunda çalışan bölüm. Bir text dosyaya hataları yazıyor. System.IO.File.WriteAllText(@"c:\Hata.txt", ex.ToString()); } } } |
Yukarıdaki müşteri sınıfı, veritabanına müşteri ekleme için tasarımına başlanıyor. Müşteri Ekle() metodu bir müşterinin kaydını eklemeye çalışıyor. Eğer kodda hata çıkarsa “Hata.txt” adındaki bir dosyaya hata mesajı yazılıyor. İlk bakışta sorun yok gibi, ancak SRP’ye göre müşteri ve hata bölümleri farklı iki görevdir. Hatta, hata konusu loglama olarak düşünülebilir. SRP ilkesine göre bu iki bölümün ayrı ayrı yapılması gerekir. Bu durumda kodu aşağıdaki gibi tasarlayabiliriz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//1. Bölüm class DosyaLog{ public void Isle(string hata){ System.IO.File.WriteAllText(@"c:\Error.txt", error); } } //2.Bölüm class Musteri{ private DosyaLog obj = new DosyaLog(); public void Ekle(){ try{ // Veritabanı Ekleme kodu } catch (Exception ex){ obj.Isle(ex.ToString()); } } } |
Görüldüğü gibi kodu iki bölüme ayırdık. Bu sayede loglama ve musteri sınıfları ayrı ayrı geliştirilebilir. Hatta dosya loglama birçok sınıf tarafından kullanılabilir. Konuyu resimler üzerinden özetleyecek olursak:
Tüm yükü bir sınıfınıza, metodunuza vermeyin.
Bir sınıfınıza, metodunuza birden fazla sorumluluk yüklemeyin.
Bir sınıf veya metodu mümkün olduğunca kadar basit bir şekilde tutun.
Open-Closed Principle (OCP)
Yazılım varlıkları uzantı (eklenti) için açık ancak değişiklikler için kapalı olmalıdır. OCP, geniş çapta uyarlanabilen ancak aynı zamanda değişmeden kalan varlıkları gerektirir. Bu noktada, çok şekillilik (polimorfizm) konusu ile özel davranışlara sahip yinelenen varlıklar yaratmamız gerekir. Konuyu örnek kodumuz üzerinden inceleyelim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Musteri { private int _MusType; public int MusType{ get { return _CustType; } set { _CustType = value; } } public double getIndirim(double ToplamSatis){ if (_MusType == 1){ return TotalSales - 100; } else { return TotalSales - 50; } } } |
MusType müşteri tipi için belirlenmiş bir property’dir. Yeni bir müşteri tipi geldiğinde tekrar kodu değiştirmeliyiz. Ancak, OCP ilkesine göre değişiklik için kapalı olmalı kuralını çiğnemiş oluruz. Bu ilke eklenti için açıktı. Bu durumu nasıl eklenti haline dönüştürmeliyiz. İşte bu noktada polimorfizm ve kalıtım çözümü düşünülebilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Musteri{ public virtual double getIndirim(double ToplamSatis){ return ToplamSatis; } } class GumusMusteri : Musteri{ public override double getIndirim(double ToplamSatis){ return base.getIndirim(ToplamSatis) - 50; } } class AltinMusteri : Musteri{ public override double getIndirim(double ToplamSatis){ return base.getIndirim(ToplamSatis) - 100; } } |
Musteri sınıfında indirim yoktur, ancak metot virtual bırakılmıştır. Bu metodu alt sınıflarda override edebiliriz. Örneğin, GumusMusteri için 50, AltinMusteri için 100 indirim uygulanır. Yeni bir müşteri tipi geldiğinde mevcut sınıflarda değişikliğe gerek yoktur. Direkt yeni bir sınıf oluşturup kalıtım sayesinde mevcut özellikleri alınıp gerekli özellikler override edilebilir. Başka bir deyişle, benzersiz türetilmiş sınıf üzerinde çalışabiliriz ve üzerinde yaptığımız herhangi bir değişikliğin ebeveyni veya diğer türetilmiş sınıfı etkilemeyeceğinden emin olmalıyız.
Liskov Substitution Principle (LSP)
Bir programdaki nesneler, o programın doğruluğunu değiştirmeden alt türlerinin örnekleriyle değiştirilebilir olmalıdır. Başka bir deyişle, her alt sınıf, alt sınıfa özgü tüm yeni davranışlarla birlikte temel sınıftaki tüm davranışları korumalıdır. Alt sınıf, aynı istekleri işleyebilmeli ve üst sınıfıyla aynı görevleri tamamlayabilmelidir.
LSP’nin avantajı, aynı türdeki tüm alt sınıfların tutarlı bir kullanımı paylaştığı için yeni alt sınıfların geliştirilmesini hızlandırmasıdır. Yeni oluşturulan tüm alt sınıfların mevcut kodla çalışabilir. Yeni bir alt sınıfa ihtiyacınız olduğu zaman mevcut kodu yeniden çalışmadan oluşturabilmenize olanak sağlar.
Yukarıdaki kod üzerinden devam edelim. Potansiyel müşteriler indirim sorgulayabilsin fakat ekle metodu çalışmasın. Yani, ekleme metodu hata üretsin. Aşağıdaki gibi bir tasarım yapabiliriz.
1 2 3 4 5 6 7 8 9 |
class PotansiyelMusteri : Musteri{ public override double getIndirim(double ToplamSatis){ return base.getIndirim(ToplamSatis) - 5; } public override void Add(){ throw new Exception("Eklenemez..."); } } |
Aşağıdaki gibi bir tasarım oluşur.
Bu tasarımın sorunlarına bakalım. Örneğin aşağıdaki gibi bir kodda sorun yaşarız.
1 2 3 4 5 6 7 8 |
List<Musteri> Musteriler = new List<Musteri>(); Musteriler.Add(new GumusMusteri()); Musteriler.Add(new AltinMusteri()); Musteriler.Add(new PotansiyelMusteri()); foreach (Musteri m in Musteriler){ m.Add(); } |
Ne yazık ki bu kod, PotansiyelMusteri eklemesinde çalışma anında hata verir.
Çalıştırmadan önce hata vermesi için daha iyi bir tasarım yapmalıyız.
Bu durumu çözmek için LSP ilkesi, ebeveynin alt nesneyi kolayca değiştirmesi gerektiğini söyler. Bu durumu çözmek için indirim ve veritabanına ekleme için iki Interface oluşturabiliriz.
1 2 3 4 5 6 7 |
interface Iindirim{ double getIndirim(double ToplamSatis); } interface Iveritabani{ void Ekle(); } |
Potansiyel Musteri için sınıfımız aşağıdaki gibi olur.
1 2 3 4 5 |
class PotansiyelMusteri : Iindirim{ public double getIndirim(double ToplamSatis){ return TotalSales - 5; } } |
Bu sayede potansiyel müşterilerimizin veritabanı ile ilişkisi kesilmiş oldu. Musteri sınıfı da aşağıdaki gibi yapabiliriz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Musteri : Iindirim, Iveritabanı{ private MyException obj = new MyException(); public virtual void Ekle(){ try{ //Ekleme kodu } catch (Exception ex){ obj.Handle(ex.Message.ToString()); } } public virtual double getIndirim(double ToplamSatis){ return ToplamSatis; } } |
Bu tasarım sayesinde potansiyel dışındaki müşterilerin veritabanı ve indirimi kullanmasına olanak sağlandı. Bu durumda kod çalışmadan önce hata verir.
LSP ilkesinde interface’leri veya abstract sınıfları doğru kullanmak çok önemlidir.
Interface Segregation Principle (ISP)
İsteme özel birçok arayüz, tek bir genel amaçlı arayüzden daha iyidir. ISP’de sınıflar kullanmadıkları davranışları içermesi istenmez. Aslında, bu durum ilk SOLID ilkemizle de ilgilidir. Çünkü, bu ilke programa doğrudan katkıda bulunmayan tüm değişkenleri, metotları veya davranışları bir sınıftan çıkarır. ISP ise metotların daha spesifik metotlara dönüştürülmesidir. Bu sayede,
- Daha az kod taşıyan metotlar elde edilir. Kodun ihtiyaç durumunda güncellemesi hızlanır.
- Davranıştan bir metot sorumlu olduğu için davranışta karşılaşılan problem hızlı çözülür.
Bir örnek üzerinden olayı inceleyelim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public interface ICalisan{ string ID { get; set; } string Name { get; set; } string Email { get; set; } float MonthlySalary { get; set; } float OtherBenefits { get; set; } float HourlyRate { get; set; } float HoursInMonth { get; set; } float CalculateNetSalary(); float CalculateWorkedSalary(); } public class TamZamanliPersonel : ICalisan { public string ID { get; set; } public string Name { get; set; } public string Email { get; set; } public float MonthlySalary { get; set; } public float OtherBenefits { get; set; } public float HourlyRate { get; set; } public float HoursInMonth { get; set; } public float CalculateNetSalary() => MonthlySalary + OtherBenefits; public float CalculateWorkedSalary() => throw new NotImplementedException(); } public class SozlesmeliPersonel : ICalisan{ public string ID { get; set; } public string Name { get; set; } public string Email { get; set; } public float MonthlySalary { get; set; } public float OtherBenefits { get; set; } public float HourlyRate { get; set; } public float HoursInMonth { get; set; } public float CalculateNetSalary() => throw new NotImplementedException(); public float CalculateWorkedSalary() => HourlyRate * HoursInMonth; } |
ICalisan Interface birçok metot içeriyor ve bu metotları hepsi sınıflara aktarılıyor ve iki farklı personel için metotlarda çakışıyor. Aslında bazı metotlar personel grupları için gereksizdir. Bu metotlar aşağıdaki gibi daha düzgün bir hale getirilebilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public interface IBaseCalisan { string ID { get; set; } string Name { get; set; } string Email { get; set; } } public interface ITamZamanliCalisanUcret : IBaseCalisan { float MonthlySalary { get; set; } float OtherBenefits { get; set; } float CalculateNetSalary(); } public interface ISozlesmeliCalisanUcret : IBaseCalisan { float HourlyRate { get; set; } float HoursInMonth { get; set; } float CalculateWorkedSalary(); } public class TamZamanliPersonel : ITamZamanliCalisanUcret { public string ID { get; set; } public string Name { get; set; } public string Email { get; set; } public float MonthlySalary { get; set; } public float OtherBenefits { get; set; } public float CalculateNetSalary() => MonthlySalary + OtherBenefits; } public class SozlesmeliPersonel : ISozlesmeliCalisanUcret { public string ID { get; set; } public string Name { get; set; } public string Email { get; set; } public float HourlyRate { get; set; } public float HoursInMonth { get; set; } public float CalculateWorkedSalary() => HourlyRate * HoursInMonth; } |
Ücret hesaplama metotları tam zamanlı ve sözleşmeli personel için ayrılmıştır. Bu sayede daha rahat okunur ve değiştirilebilir bir kod elde edilmiştir.
Dependency Inversion Principle (DIP)
Abstraction (Soyutlama) konusu sınıf ve doğru özelliklerin sınıfa eklenmesi açısından Nesneye Yönelik Programlamanın en önemli konularından biridir. DIP iki kısma sahiptir:
- Yüksek seviyeli modüller, düşük seviyeli modüllere bağlı olmamalıdır. Bunun yerine, her ikisi de soyutlamalara (Interface) bağlı olmalıdır.
- Soyutlamalar ayrıntılara bağlı olmamalıdır. Ayrıntılar (somut uygulamalar gibi) soyutlamalara bağlı olmalıdır.
Yazılımcılar, konuyu parça parça öğrendikleri için sınıflarını yüklenirler. Bir anlamda yüksek seviyeli bileşenlere sahip programlar yazarlar. DIP ilkesinin amacı düşük ve yüksek seviyeli bileşenleri ayırıp her ikisini de soyutlamalara bağlamaktır. Bu durumda, yüksek ve düşük seviyeli bileşenler birbirinden yararlanabilir ama birindeki değişiklik doğrudan diğerini etkilememelidir. Konuyu bir örnek üzerinden inceleyelim.
Örneğimizde; bir interface, üst düzey, alt düzey ve ayrıntılı bileşenlerle genel bir program oluşturacağız. Öncelikle, müşteri adına ulaşmak için bir interface’de metodumuzu yazalım.
1 2 3 4 |
public interface IMusteriDataAccess { string GetMusteriAdi(int id); } |
Şimdi, IMusteriDataAccess arayüzüne bağlı olacak ayrıntıları uygulayacağız. Bunu yapmak, DIP ilkesinin ikinci bölümünü gerçekleştirir.
1 2 3 4 5 6 7 8 9 10 |
public class MusteriDataAccess: IMusteriDataAccess { //Yapıcı public MusteriDataAccess() { } public string GetMusteriAdi(int id) { return "Bir Müşteri İsmi!!!"; } } |
Şimdi IMusteriDataAccess soyut arayüzünü uygulayan ve onu kullanılabilir bir biçimde döndüren bir fabrika sınıfı oluşturacağız. Döndürülen MusteriDataAccess sınıfı, düşük seviyeli bileşenimizdir.
1 2 3 4 5 6 7 |
public class DataAccessFactory { public static IMusteriDataAccess GetMusteriDataAccessObj() { return new MusteriDataAccess(); } } |
Son olarak, IMusteriDataAccess arayüzünü de uygulayan üst düzey bir MusteriBusinessLogic bileşeni uygulayacağız. Üst düzey bileşenimizin düşük düzey bileşenimizi uygulamadığına ve yalnızca onu kullandığına dikkatinizi çekerim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class MusteriBusinessLogic { IMusteriDataAccess _musDataAccess; public MusteriBusinessLogic() { _musDataAccess = DataAccessFactory.GetMusteriDataAccessObj(); } public string GetMusteriAdi(int id) { return _musDataAccess.GetMusteriAdi(id); } } |
Sonuç
SOLID ilkeleri, kodunuzu geliştirmenin ve değişiklikleri kolaylaştırmanın mükemmel bir yoludur. Başlangıç aşamasında iseniz bu ilkeleri kullanmak zor olabilir, ama kod yazdıkça bu ilkelere ihtiyaç duyacaksınız. Özellikle framework’lerde sizi bu yönde kod geliştirmenizi sağlamaya çalışmaktadır. Bu konuyu kodlarınızda kullanmaya başladığınızda nesneye yönelik programlama dersinde gördüğünüz polimorfizm, abstraction, encapsulation ve inheritance gibi programınız içinde daha doğru şekilde kullanmış olacaksınız.
Bol kodlu günler 🙂
Kaynaklar:
- SOLID – Wikipedia
- Martin, Robert C. (2000). “Design Principles and Design Patterns” (PDF). Archived from the original (PDF) on 2015-09-06.
- Fenton, Steve (2017). Pro TypeScript: Application-Scale JavaScript Development. p. 108. ISBN 9781484232491.
- SOLID Architecture Principles Using Simple C# Examples – CodeProject
- S.O.L.I.D. Principles of Object-Oriented Programming in C# (educative.io)