SOLID Prensipleri Nedir?Beğendin
SOLID prensipleri Robert C. Martin tarafından tanımlanmış ve ismini 5 ana tasarım kuralının baş harflerinden alan tasarım kuralları bütünüdür. Bu beş kuralı aşağıdaki gibi anlatabiliriz.
1. Tekil Sorumluluk (Single Responsibility):
Bir sınıfın sadece tek bir sorumluluk alanı olması gerekir. Sadece tek bir sebeple değiştirilmeye ihtiyaç duymalı ve üstüne vazife olmayan görevler üstlenmemeli. Basit bir örnek;
internal class Rectangle : Shape
{
private readonly double _a;
private readonly double _b;
public Rectangle(double a, double b)
{
_a = a;
_b = b;
}
public override double GetArea()
{
return _a * _b;
}
public override double GetPerimeter()
{
return 2 * (_a + _b);
}
}
Yukarıdaki Rectangle sınıfı dikdörtgeni temsil etmekte ve basitçe alan ve çevre değerlerini dönen fonksiyonlar içermektedir. Aşağıdaki ShapePrinter sınıfı ise bu şeklin alan ve çevre bilgisini yazdırma görevini üstlenmiştir.
internal class ShapePrinter
{
private readonly IShape _shape;
public ShapePrinter(IShape shape)
{
_shape = shape;
}
public void Print()
{
Console.WriteLine($"Shape:{_shape.GetType()}, Perimeter:{_shape.GetPerimeter()}, Area:{_shape.GetArea()}");
}
}
Nereye, nasıl, hangi formatta yazdırılacağı sorumluluğu ShapePrinter sınıfına aittir. Eğer yazdırma fonksiyonunu da Rectangle sınıfı içerisinde gerçekleştirseydik, ilerde Console ile birlikte örneğin dokümana da yazdırmak istediğimizde Rectangle sınıfında da değişiklik yapmamız gerekecekti. Halbuki bu görev Rectangle sınıfı ile ilgili değil. Bu yüzden tekil sorumluluk prensibine uymamış olacaktık.
2. Açık-Kapalı (Open-Closed):
Bir yazılım modülü genişlemeye açık, ama değiştirilmeye kapalı olmalıdır. Bu özellik genellikle interface veya abstract sınıflar kullanılarak gerçekleştirilir. Örnek ile anlatmak gerekirse;
public abstract class Shape : IShape
{
public abstract double GetArea();
public abstract double GetPerimeter();
}
Bir önceki başlıkta Dikdörtgen sınıfı oluşturmuştuk. Bu sınıf abstract Shape sınıfından türemekte. Yeni bir şekil olan Üçgen sınıfını oluşturmak istediğimizde Şekil sınıfında herhangi bir değişiklik yapmadan, bu sınıfı yine Dikdörtgen sınıfında olduğu gibi kalıtım yoluyla türeterek yeni bir şekil elde edebiliriz. Bu şekilde Shape sınıfı değişime kapalı ama genişlemeye açık bir hale gelmekte.
internal class Triangle : Shape
{
private readonly double _a;
private readonly double _b;
private readonly double _c;
public Triangle(double a, double b, double c)
{
_a = a;
_b = b;
_c = c;
}
public override double GetArea()
{
var s = GetPerimeter()/2;
return Math.Sqrt(s * (s - _a) * (s - _b) * (s - _c));
}
public override double GetPerimeter()
{
return _a + _b + _c;
}
}
Oluşturduğumuz şekilleri aşağıdaki gibi alanlarını yazdırmak istediğimizde Shape sınıfında herhangi bir değişiklik yapma ihtiyacımız olmadan dilediğimiz kadar yeni şekil tanımlayabilir ve sisteme ekleyebiliriz. Yeni şekillerimizin Shape sınıfından türüyor olması yeterlidir ve Açık-Kapalı prensibine örnektir.
var shapes = new List<IShape>
{
new Rectangle(3, 5),
new Triangle(3, 4, 5)
};
foreach (var shape in shapes)
{
new ShapePrinter(shape).Print();
}
Açık-kapalı prensibine uymadığımız takdirde, Shape sınıfının direkt olarak değiştirilmesi, onu referans alan tüm üst modüllere etki verecek, zincirleme değişim gerektirecek veya derleme veya çalışma zamanı hatalarına sebep olacaktır.
3. Liskov Yer Değiştirmesi: (Liskov Substitution):
Eğer A sınıfı B sınıfının türevi ise, başka bir deyişle B sınıfı A sınıfının atası ise, A sınıfına ait tüm nesneler bir B nesnesi gibi davranabilmelidir. Örnek ile anlatacak olursak;
internal class Square : Rectangle
{
public Square(double a)
: base(a, a) { }
}
Yeni bir "Kare" sınıfı oluşturduk. Kare kısa ve uzun kenarları birbirine eşit olan bir dikdörtgendir. Haliyle kare sınıfından yarattığımız tüm nesnelerin aynı zamanda bir dikdörtgen gibi davranabiliyor olması gerekir.
Rectangle rectangle = new Square(5);
shapes.Add(rectangle);
foreach (var shape in shapes)
{
new ShapePrinter(shape).Print();
}
Yukarıdaki gibi bir dikdörtgen görüntüsünde bir kare eklediğimizde bu kare için sonucun "Shape:Square, Perimeter:20, Area:25" şeklinde olması gerekir. Bu nesneyi Rectangle sınıfını kabul eden tüm metod, sınıf vb. modüllerde hatasız kullanabiliyor olmamız gerekir. Bu prensibi doğru uygulamadığımız takdirde, çalışma zamanında istenmeyen hatalarla karşılaşabiliriz.
4. Arayüz Ayrışması (Interface Segregation):
Arayüzleri (interface) kullanan sınıflar, ihtiyacı olmayan özellikleri gerçekleştirmek (implement) zorunda olmamalıdır. Dolayısıyla birçok özelliği barındıran büyük arayüzler yerine mikro tanımlı birden çok arayüz olmalıdır. Sınıflar sadece ihtiyaçları olan arayüzleri gerçekleştirmelidir. Örnek;
internal interface IHasEdges
{
int GetEdgeCount();
}
Kenarları olan şekiller için, kenar sayısını veren bir fonksiyona ihtiyacımız var. Bu metodu IShape içerisine yazabiliriz. Bu takdirde tüm 2 Boyutlu şekillerin kenarlarının olmasını bekleriz. Halbuki çember de 2 boyutlu bir şekil ama kenarları yok (ya da sonsuz kenarı var dersek daha doğru olur). Bu durumda onun bu fonksiyonu sağlama mecburiyeti de yok. Öyleyse bu fonksiyonaliteyi yukarıdaki gibi farklı bir arayüz ile tanımlayabiliriz.
Bu durumda sadece kenarları olan şekiller bu arayüzü gerçekleştirecektir.
internal class Rectangle : Shape, IHasEdges
{
//...
public int GetEdgeCount()
{
return 4;
}
}
internal class Triangle : Shape, IHasEdges
{
//...
public int GetEdgeCount()
{
return 3;
}
}
internal class Circle : Shape
{
//...
}
Yukarıda Çember sınıfı kenarları olmadığı için IHasEdges arayüzünü gerçekleştirmiyor, Dikdörtgen ve Üçgen sınıfları gerçekleştiriyor.
5. Bağımlılığı Tersine Çevirme (Dependency Inversion):
Üst-katman modülleri alt-katman modüllerine bağımlı olmamalı, tüm modüller arayüzlere bağımlı olmalıdır. Daha önce belirttiğimiz ShapePrinter sınıfı zaten buna güzel bir örnek teşkil ediyordu. Sadece IShape arayüzüne bağımlı olduğu için DI prensibine uygun tasarlanmıştı diyebiliriz. Biz biraz daha suyunu çıkarıp! ShapePrinter sınıfını da soyutlayabileceğimiz bir yapı kuralım. Örnek;
internal interface IShapePrinter
{
void Print(IShape shape);
}
Soyut sınıf:
internal abstract class ShapePrinter : IShapePrinter
{
public abstract void Print(IShape shape);
}
Konsol için somut sınıf:
internal class ShapeConsolePrinter : ShapePrinter
{
public override void Print(IShape shape)
{
Console.WriteLine($"Shape:{shape.GetType().Name}, Perimeter:{shape.GetPerimeter()}, Area:{shape.GetArea()}");
if (shape is IHasEdges shapeHasEdges)
{
Console.WriteLine($"Edge Count:{shapeHasEdges.GetEdgeCount()}");
}
}
}
Dosya için somut sınıf:
internal class ShapeFilePrinter : ShapePrinter
{
public override void Print(IShape shape)
{
File.AppendAllLines(@"./temp.txt", new[] {$"Shape:{shape.GetType().Name}, Perimeter:{shape.GetPerimeter()}, Area:{shape.GetArea()}"});
if (shape is IHasEdges shapeHasEdges)
{
File.AppendAllLines(@"./temp.txt", new[] {$"Edge Count:{shapeHasEdges.GetEdgeCount()}"});
}
}
}
IShapePrinter arayüzü ve ShapePrinter soyut sınıfımızı yukarıdaki gibi güncelledik. Sonrasında konsol ve dosyaya yazması için 2 farklı sınıf türettik. Aşağıda ise bağımlılığı üzerine yıkabileceğim bağımlılık çözücü gibi basit bir sınıf yazdım (enum ile factory->create tipi bir yapı da kurulabilirdi). Bu hangi tip ShapePrinter sınıfının kullanılacağına karar verecek.
internal static class DependencyResolver
{
public static IShapePrinter GetShapePrinter()
{
return new ShapeConsolePrinter();
}
}
Ve ShapePrinter fonksiyonlarını aşağıdaki gibi kullanabiliriz.
var shapePrinter = DependencyResolver.GetShapePrinter();
foreach (var shape in shapes)
{
shapePrinter.Print(shape);
}
Bu sayede ana sınıfımız ShapePrinter somut sınıflarına olan bağımlılığından kurtularak bağımlılığını IShapePrinter arayüzü üzerine kurmuş oldu. Bu vesileyle son prensibe de bir örnek vermiş olduk. Sonuç ekranımız da aşağıdaki gibi olacaktır. (Örnek kodlara bu bağlantı üzerinden erişebilirsiniz.)
Sonuç olarak; bu 5 temel prensip nesne yönelim programlama yazılım dilleri ile geliştirilen yazılımlarda, üst seviye tasarım kalitesi sağlamak için çok önemlidir. Bu prensipler kullanılmadan da yazılım geliştirilir mi? Evet geliştirilir ve geliştiriliyor da. Sonuç odaklı yazılım geliştirirseniz, bu prensiplerin hiç birini kullanmadan her satırı tek bir sınıf/metod altına yazarak bile aynı sonucu aynı sürede elde eden bir uygulama oluşturabilirsiniz. Ancak orta ve uzun vadede yazan kişinin bile bakıp da anlamayacağı, bakımı çok zor arap saçı gibi bir yapı ortaya çıkacaktır. Bu tip prensipler yazılım dış kalite niteliklerini değil iç kalite niteliklerini hedef almaktadır.