İçindekiler
SOLID İlkeleri, Nesne Tabanlı sınıf tasarımının beş ilkesidir. Bir sınıf yapısı tasarlarken izlenecek bir dizi kural ve en iyi uygulamalardır.
Bu beş ilke, genel olarak belirli tasarım modellerine ve yazılım mimarisine olan ihtiyacı anlamamıza yardımcı olur. Bu yüzden her geliştiricinin öğrenmesi gereken bir konu olduğuna inanıyorum.
Bu makale, SOLID ilkelerini projelerinize uygulamak için bilmeniz gereken her şeyi size öğretecektir.
Bu dönemin tarihine bir göz atarak başlayacağız. Daha sonra, bir sınıf tasarımı oluşturarak ve onu adım adım geliştirerek, her bir ilkenin neden ve nasıl olduğu gibi ayrıntılara gireceğiz.
Öyleyse bir fincan kahve veya çay alın ve hemen içeri girelim!
Arka fon
SOLID ilkeleri ilk olarak 2000 yılında ünlü Bilgisayar Bilimcisi Robert J. Martin (Bob Amca) tarafından makalesinde tanıtıldı. Ancak SOLID kısaltması daha sonra Michael Feathers tarafından tanıtıldı.
Bob Amca aynı zamanda çok satan Clean Code and Clean Architecture kitaplarının yazarıdır ve “Agile Alliance” ın katılımcılarından biridir .
Bu nedenle, tüm bu temiz kodlama, nesne yönelimli mimari ve tasarım kalıpları kavramlarının bir şekilde bağlantılı ve birbirini tamamlayıcı olması şaşırtıcı değildir.
Hepsi aynı amaca hizmet ediyor:
"Birçok geliştiricinin işbirliği içinde çalışabileceği anlaşılır, okunabilir ve test edilebilir kod oluşturmak için."
Her prensibe tek tek bakalım. SOLID kısaltmasının ardından bunlar:
- S Sorumluluk İlkesi ingle
- O kalem Kapalı İlke
- L iskov ikame prensibi
- Ben nterface ayrılığı prensibi
- D ependency ters ilişki ilkesi
Tek Sorumluluk İlkesi
Tek Sorumluluk İlkesi, bir sınıfın tek bir şey yapması gerektiğini ve bu nedenle değişmek için yalnızca tek bir nedeni olması gerektiğini belirtir .
Bu prensibi daha teknik bir şekilde ifade etmek için: Yazılımın spesifikasyonundaki yalnızca bir potansiyel değişiklik (veritabanı mantığı, kayıt mantığı, vb.), Sınıfın spesifikasyonunu etkileyebilmelidir.
Bu, bir sınıfın Kitap sınıfı veya Öğrenci sınıfı gibi bir veri kabıysa ve bu varlıkla ilgili bazı alanları varsa, yalnızca veri modelini değiştirdiğimizde değişmesi gerektiği anlamına gelir.
Tek Sorumluluk İlkesine uymak önemlidir. Öncelikle, birçok farklı ekip aynı proje üzerinde çalışabildiğinden ve farklı nedenlerle aynı sınıfı düzenleyebildiğinden, bu uyumsuz modüllere yol açabilir.
İkincisi, sürüm kontrolünü kolaylaştırır. Örneğin, veritabanı işlemlerini işleyen bir kalıcılık sınıfımız olduğunu ve GitHub kayıtlarında bu dosyada bir değişiklik gördüğümüzü varsayalım. SRP’yi takip ederek, bunun depolama veya veri tabanı ile ilgili şeylerle ilgili olduğunu bileceğiz.
Birleştirme çatışmaları başka bir örnektir. Farklı ekipler aynı dosyayı değiştirdiğinde görünürler. Ancak, SRP izlenirse, daha az çakışma görünecektir – dosyaların değişmesi için tek bir neden olacaktır ve var olan çakışmaların çözülmesi daha kolay olacaktır.
Yaygın Tuzaklar ve Karşıtlıklar
Bu bölümde, Tek Sorumluluk İlkesini ihlal eden bazı yaygın hatalara bakacağız. Sonra onları düzeltmenin bazı yollarından bahsedeceğiz.
Örnek olarak basit bir kitapçı fatura programının koduna bakacağız. Faturamızda kullanmak üzere bir kitap sınıfı tanımlayarak başlayalım.
class Book {
String name;
String authorName;
int year;
int price;
String isbn;
public Book(String name, String authorName, int year, int price, String isbn) {
this.name = name;
this.authorName = authorName;
this.year = year;
this.price = price;
this.isbn = isbn;
}
}
Bu, bazı alanları olan basit bir kitap dersidir. Süslü bir şey yok. Alıcılar ve ayarlayıcılarla uğraşmamıza gerek kalmaması ve bunun yerine mantığa odaklanabilmemiz için alanları özel yapmıyorum.
Şimdi de fatura oluşturma ve toplam fiyatı hesaplama mantığını içerecek fatura sınıfını oluşturalım. Şimdilik, kitabevimizin sadece kitap sattığını ve başka hiçbir şey satmadığını varsayalım.
public class Invoice {
private Book book;
private int quantity;
private double discountRate;
private double taxRate;
private double total;
public Invoice(Book book, int quantity, double discountRate, double taxRate) {
this.book = book;
this.quantity = quantity;
this.discountRate = discountRate;
this.taxRate = taxRate;
this.total = this.calculateTotal();
}
public double calculateTotal() {
double price = ((book.price - book.price * discountRate) * this.quantity);
double priceWithTaxes = price * (1 + taxRate);
return priceWithTaxes;
}
public void printInvoice() {
System.out.println(quantity + "x " + book.name + " " + book.price + "$");
System.out.println("Discount Rate: " + discountRate);
System.out.println("Tax Rate: " + taxRate);
System.out.println("Total: " + total);
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
İşte fatura sınıfımız. Ayrıca faturalandırmayla ilgili bazı alanlar ve 3 yöntem içerir:
- toplam fiyatı hesaplayan hesaplamaToplam yöntemi,
- Faturayı konsola yazdırması gereken printInvoice yöntemi ve
- Faturayı bir dosyaya yazmaktan sorumlu olan saveToFile yöntemi.
Bir sonraki paragrafı okumadan önce bu sınıf tasarımında neyin yanlış olduğunu düşünmek için kendinize biraz zaman vermelisiniz.
Tamam öyleyse burada neler oluyor? Sınıfımız, Tek Sorumluluk İlkesini çeşitli şekillerde ihlal ediyor.
İlk ihlal, yazdırma mantığımızı içeren printInvoice yöntemidir. SRP, sınıfımızın değişmesi için yalnızca tek bir nedeni olması gerektiğini ve bu nedenin sınıfımız için fatura hesaplamasında bir değişiklik olması gerektiğini belirtir.
Ancak bu mimaride baskı formatını değiştirmek istersek sınıfı değiştirmemiz gerekirdi. Bu nedenle aynı sınıfta iş mantığı ile karışık baskı mantığına sahip olmamalıyız.
Sınıfımızda SRP’yi ihlal eden başka bir yöntem daha var: saveToFile yöntemi. Kalıcılık mantığını iş mantığıyla karıştırmak da son derece yaygın bir hatadır.
Sadece bir dosyaya yazmayı düşünmeyin – bir veritabanına kaydetme, bir API çağrısı yapma veya kalıcılıkla ilgili diğer şeyler olabilir.
Öyleyse bu yazdırma işlevini nasıl düzeltebiliriz diye sorabilirsiniz.
Yazdırma ve kalıcılık mantığımız için yeni sınıflar oluşturabiliriz, böylece bu amaçlar için fatura sınıfını değiştirmemize gerek kalmaz.
InvoicePrinter ve InvoicePersistence olmak üzere 2 sınıf oluşturup metodları taşıyoruz
public class InvoicePrinter {
private Invoice invoice;
public InvoicePrinter(Invoice invoice) {
this.invoice = invoice;
}
public void print() {
System.out.println(invoice.quantity + "x " + invoice.book.name + " " + invoice.book.price + " $");
System.out.println("Discount Rate: " + invoice.discountRate);
System.out.println("Tax Rate: " + invoice.taxRate);
System.out.println("Total: " + invoice.total + " $");
}
}
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
}
Artık sınıf yapımız Tek Sorumluluk İlkesine uyar ve uygulamamızın bir yönünden her sınıf sorumludur. Harika!
Açık Kapalı Prensibi
Açık-Kapalı Prensibi, sınıfların genişletmeye açık ve değişikliğe kapalı olmasını gerektirir .
Modifikasyon, mevcut bir sınıfın kodunu değiştirmek anlamına gelir ve genişletme, yeni işlevsellik eklemek anlamına gelir.
Öyleyse bu ilkenin söylemek istediği şey şudur: Sınıf için mevcut koda dokunmadan yeni işlevler ekleyebilmeliyiz. Bunun nedeni, mevcut kodu her değiştirdiğimizde, potansiyel hatalar oluşturma riskini almamızdır. Bu yüzden mümkünse test edilmiş ve güvenilir (çoğunlukla) üretim koduna dokunmaktan kaçınmalıyız.
Ama sınıfa dokunmadan yeni işlevleri nasıl ekleyeceğiz diye sorabilirsiniz. Genellikle arayüzler ve soyut sınıflar yardımı ile yapılır.
Artık prensibin temellerini ele aldığımıza göre, onu Fatura uygulamamıza uygulayalım.
Diyelim ki patronumuz bize geldi ve faturaların bir veri tabanına kaydedilmesini istediklerini söyledi, böylece onları kolayca arayabiliriz. Tamam diye düşünüyoruz, bu kolay peasy patron, bana bir saniye ver!
Veritabanını oluşturuyoruz, ona bağlanıyoruz ve InvoicePersistence sınıfımıza bir kaydetme yöntemi ekliyoruz :
public class InvoicePersistence {
Invoice invoice;
public InvoicePersistence(Invoice invoice) {
this.invoice = invoice;
}
public void saveToFile(String filename) {
// Creates a file with given name and writes the invoice
}
public void saveToDatabase() {
// Saves the invoice to database
}
}
Ne yazık ki, kitapçı için tembel geliştirici olarak, sınıfları gelecekte kolayca genişletilebilir şekilde tasarlamadık. Bu özelliği eklemek için InvoicePersistence sınıfını değiştirdik .
Sınıf tasarımımız Açık-Kapalı ilkesine uysaydı, bu sınıfı değiştirmemize gerek kalmazdı.
Bu nedenle, kitapçı için tembel ama zeki bir geliştirici olarak, tasarım sorununu görüyoruz ve ilkeye uymak için kodu yeniden düzenlemeye karar veriyoruz.
interface InvoicePersistence {
public void save(Invoice invoice);
}
InvoicePersistence türünü Arayüz olarak değiştirip bir kaydetme yöntemi ekliyoruz. Her kalıcılık sınıfı bu kaydetme yöntemini uygulayacaktır.
public class DatabasePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// Save to DB
}
}
public class FilePersistence implements InvoicePersistence {
@Override
public void save(Invoice invoice) {
// Save to file
}
}
Yani sınıf yapımız şimdi şöyle görünüyor:
Artık kalıcılık mantığımız kolaylıkla genişletilebilir. Patronumuz bizden başka bir veritabanı eklememizi isterse ve MySQL ve MongoDB gibi 2 farklı veritabanı türüne sahipse, bunu kolayca yapabiliriz.
Arayüz olmadan birden fazla sınıf oluşturabileceğimizi ve hepsine bir kaydetme yöntemi ekleyebileceğimizi düşünebilirsiniz.
Ancak uygulamamızı genişlettiğimizi ve InvoicePersistence , BookPersistence gibi birden fazla kalıcılık sınıfımız olduğunu ve tüm kalıcılık sınıflarını yöneten bir PersistenceManager sınıfı oluşturduğumuzu varsayalım :
public class PersistenceManager {
InvoicePersistence invoicePersistence;
BookPersistence bookPersistence;
public PersistenceManager(InvoicePersistence invoicePersistence,
BookPersistence bookPersistence) {
this.invoicePersistence = invoicePersistence;
this.bookPersistence = bookPersistence;
}
}
Artık InvoicePersistence arayüzünü uygulayan herhangi bir sınıfı polimorfizm yardımıyla bu sınıfa geçirebiliriz . Bu, arayüzlerin sağladığı esnekliktir.
Liskov İkame Prensibi
Liskov İkame İlkesi, alt sınıfların temel sınıflarının yerine geçmesi gerektiğini belirtir.
Bu, B sınıfının A sınıfının bir alt sınıfı olduğu düşünüldüğünde, B sınıfından bir nesneyi A sınıfından bir nesne bekleyen herhangi bir yönteme geçirebilmemiz gerektiği ve bu durumda yöntemin herhangi bir tuhaf çıktı vermemesi gerektiği anlamına gelir.
Bu beklenen davranıştır, çünkü kalıtımı kullandığımızda, alt sınıfın üst sınıfın sahip olduğu her şeyi miras aldığını varsayıyoruz. Çocuk sınıfı davranışı genişletir ama asla daraltmaz.
Bu nedenle, bir sınıf bu ilkeye uymadığında, tespit edilmesi zor bazı kötü hatalara yol açar.
Liskov’un prensibi anlaşılması kolay ancak kodda tespit edilmesi zordur. Öyleyse bir örneğe bakalım.
class Rectangle {
protected int width, height;
public Rectangle() {
}
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
Basit bir Rectangle sınıfımız ve dikdörtgenin alanını döndüren getArea fonksiyonumuz var.
Şimdi Squares için başka bir sınıf oluşturmaya karar veriyoruz. Bildiğiniz gibi kare, genişliğin yüksekliğe eşit olduğu özel bir dikdörtgen türüdür.
class Square extends Rectangle {
public Square() {}
public Square(int size) {
width = height = size;
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
Square sınıfımız Rectangle sınıfını genişletir. Yapıcıda yüksekliği ve genişliği aynı değere ayarlıyoruz, ancak herhangi bir müşterinin (kodunda sınıfımızı kullanan birinin) boy veya ağırlığı kare özelliğini ihlal edecek şekilde değiştirmesini istemiyoruz.
Bu nedenle, biri değiştirildiğinde her iki özelliği de ayarlamak için ayarlayıcıları geçersiz kılıyoruz. Ancak bunu yaparak Liskov ikame ilkesini ihlal etmiş olduk.
GetArea işlevi üzerinde testler yapmak için bir ana sınıf oluşturalım .
class Test {
static void getAreaTest(Rectangle r) {
int width = r.getWidth();
r.setHeight(10);
System.out.println("Expected area of " + (width * 10) + ", got " + r.getArea());
}
public static void main(String[] args) {
Rectangle rc = new Rectangle(2, 3);
getAreaTest(rc);
Rectangle sq = new Square();
sq.setWidth(5);
getAreaTest(sq);
}
}
Ekibinizin testçisi getAreaTest test işlevini buldu ve size getArea işlevinizin kare nesneler için testi geçemediğini söyledi.
İlk testte, genişliği 2 ve yüksekliği 3 olan bir dikdörtgen oluşturuyoruz ve getAreaTest’i çağırıyoruz. Çıktı beklendiği gibi 20, ancak kareden geçerken işler ters gidiyor. Bunun nedeni, testteki setHeight işlevine yapılan çağrının genişliği de ayarlaması ve beklenmeyen bir çıktıyla sonuçlanmasıdır.
Arayüz Ayrıştırma Prensibi
Ayrıştırma, her şeyi ayrı tutmak anlamına gelir ve Arayüz Ayırma İlkesi, arayüzleri ayırmakla ilgilidir.
İlke, birçok müşteriye özel arayüzün tek bir genel amaçlı arayüzden daha iyi olduğunu belirtir. Müşteriler ihtiyaç duymadıkları bir işlevi uygulamaya zorlanmamalıdır.
Bu, anlaşılması ve uygulanması için basit bir ilkedir, öyleyse bir örnek görelim.
public interface ParkingLot {
void parkCar(); // Decrease empty spot count by 1
void unparkCar(); // Increase empty spots by 1
void getCapacity(); // Returns car capacity
double calculateFee(Car car); // Returns the price based on number of hours
void doPayment(Car car);
}
class Car {
}
Çok basitleştirilmiş bir otoparkı modelledik. Saatlik ücret ödediğiniz otopark türüdür. Şimdi ücretsiz bir park yeri yapmak istediğimizi düşünün.
public class FreeParking implements ParkingLot {
@Override
public void parkCar() {
}
@Override
public void unparkCar() {
}
@Override
public void getCapacity() {
}
@Override
public double calculateFee(Car car) {
return 0;
}
@Override
public void doPayment(Car car) {
throw new Exception("Parking lot is free");
}
}
Otopark arayüzümüz 2 unsurdan oluşmuştur: Park ile ilgili mantık (park etme, park etme, kapasite alma) ve ödeme ile ilgili mantık.
Ama çok spesifik. Bu nedenle, FreeParking sınıfımız, ödemeyle ilgili alakasız yöntemleri uygulamaya zorlandı. Arayüzleri ayıralım veya ayıralım.
Şimdi park alanını ayırdık. Bu yeni modelle daha da ileri gidebilir ve PaidParkingLot’u farklı ödeme türlerini desteklemek için bölebiliriz .
Artık modelimiz çok daha esnek, genişletilebilir ve müşterilerin alakasız bir mantık uygulamasına gerek yok çünkü otopark arayüzünde sadece parkla ilgili işlevsellik sağlıyoruz.
Bağımlılık Ters Çevirme Prensibi
Bağımlılığı Ters Çevirme ilkesi, sınıflarımızın somut sınıflar ve işlevler yerine arayüzlere veya soyut sınıflara bağlı olması gerektiğini belirtir.
Onun içinde makalesinde şöyle (2000), Bob amca bu ilkeyi özetliyor:
"OCP, OO mimarisinin amacını belirtirse, DIP birincil mekanizmayı belirtir".
Bu iki ilke gerçekten birbiriyle ilişkilidir ve bu modeli daha önce Açık-Kapalı İlkesini tartışırken uyguladık.
Sınıflarımızın genişlemeye açık olmasını istiyoruz, bu nedenle bağımlılıklarımızı somut sınıflar yerine arayüzlere bağlı olacak şekilde yeniden düzenledik. PersistenceManager sınıfımız, bu arabirimi uygulayan sınıflar yerine InvoicePersistence’a bağlıdır.
Sonuç
Bu makalede, SOLID ilkelerinin tarihçesi ile başladık ve ardından her bir ilkenin neden ve nasıl olduğuna dair net bir anlayış edinmeye çalıştık. Hatta SOLID ilkelerine uymak için basit bir Fatura uygulamasını yeniden düzenledik.
Zaman ayırıp makalenin tamamını okuduğunuz için teşekkür etmek istiyorum ve umarım yukarıdaki kavramlar anlaşılırdır.
Kodunuzu tasarlarken, yazarken ve yeniden düzenlerken bu ilkeleri akılda tutmanızı öneririm, böylece kodunuz çok daha temiz, genişletilebilir ve test edilebilir olacaktır.
Dilerseniz Diğer Blog Sayfalarımıza Göz Atabilirsiniz…
Bir sonraki yazımızda görüşmek üzere hoşçakalın webodasıyla kalın….
Hata!
Yorumunuz Çok Kısa, Yorum yapabilmek için en az En az 10 karakter gerekli