paint-brush
Java'da SOLID İlkeleri: Yeni Başlayanlar İçin Kılavuzile@ps06756
14,604 okumalar
14,604 okumalar

Java'da SOLID İlkeleri: Yeni Başlayanlar İçin Kılavuz

ile Pratik Singhal14m2024/03/22
Read on Terminal Reader

Çok uzun; Okumak

SOLID İlkeleri, ölçeklenebilir yazılımlar geliştirmek için gerekli olan nesne yönelimli programlamanın ilkeleridir. İlkeler şunlardır: S: Tek Sorumluluk Prensibi O: Açık/Kapalı Prensibi L: Liskov'un Değiştirme Prensibi I: Arayüz Ayrıştırma Prensibi D: Bağımlılığı Ters Çevirme Prensibi
featured image - Java'da SOLID İlkeleri: Yeni Başlayanlar İçin Kılavuz
Pratik Singhal HackerNoon profile picture
0-item

SOLID tasarım ilkeleri, temiz kod yazmak için bilmeniz gereken en önemli tasarım ilkeleridir. SOLID ilkelerine sağlam bir hakimiyete sahip olmak her programcı için vazgeçilmez bir beceridir. Bunlar diğer tasarım modellerinin geliştirildiği temeldir.


Bu yazıda SOLID tasarım ilkelerini gerçek hayattan örnekler kullanarak ele alacağız ve bunların önemini anlayacağız.


Polimorfizm, Soyutlama ve Kalıtım ile birlikte SOLID İlkeleri, hedef odaklı programlamada iyi olmak için gerçekten önemlidir.

SOLID İlkeleri Neden Önemlidir?

SOLID ilkeleri birçok nedenden dolayı önemlidir:


  • SOLID ilkeleri temiz ve bakımı kolay kod yazmamıza olanak tanır: Yeni bir projeye başladığımızda, başlangıçta kod kalitesi iyidir çünkü uyguladığımız sınırlı sayıda özellik vardır. Ancak, daha fazla özellik ekledikçe kod karmaşıklaşmaya başlar.


  • SOLID İlkeleri Soyutlama, Polimorfizm ve Kalıtım temelleri üzerine kuruludur ve yaygın kullanım durumları için tasarım modellerine yol açar. Bu tasarım modellerini anlamak, programlamada yaygın kullanım durumlarının uygulanmasına yardımcı olur.


  • SOLID İlkeleri, kodun test edilebilirliğini artıran temiz kod yazmamıza yardımcı olur. Bunun nedeni kodun modüler ve gevşek bir şekilde bağlanmış olmasıdır. Her modül bağımsız olarak geliştirilebilir ve bağımsız olarak test edilebilir.


Şimdi SOLID ilkelerinin her birini gerçek dünyadan örneklerle ayrıntılı olarak inceleyelim.

1. Tek Sorumluluk İlkesi

SOLID ilkelerindeki S, Tek Sorumluluk İlkesi anlamına gelir. Tek Sorumluluk İlkesi, bir sınıfın değişmek için yalnızca tek bir nedeni olması gerektiğini belirtir. Bu, projemize ek gereksinimler eklerken değişiklik yapmamız gereken yer sayısını sınırlar.

Her sınıfın değişmek için tam olarak bir nedeni olmalıdır.

Örneğin, Java'da, banka, kredi ve sendUpdates gibi temel işlemlere izin veren SavingsAccount sınıfının bulunduğu bir bankacılık uygulaması tasarladığımızı varsayalım. sendUpdate yöntemi, NotificationMedium adı verilen bir numaralandırmayı (E-posta, SMS vb. gibi) alır ve güncellemeyi uygun ortamla gönderir. Bunun kodunu aşağıda gösterildiği gibi yazacağız.


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void sendNotification(NotificationMedium medium) { if (medium == NotificationMedium.SMS) { // Send SMS here } else if (medium == NotificationMedium.EMAIL) { // Send Email here } } }


 public enum NotificationMedium { SMS, EMAIL }


Şimdi, yukarıdaki SavingsAccount sınıfına bakarsanız, bunun birçok nedenden dolayı değişebileceğini görürsünüz:


  1. SavingsAccount sınıfının temel mantığında herhangi bir değişiklik olup olmadığı ( debit , credit vb. gibi).


  2. Banka yeni bir Bildirim ortamı sunmaya karar verirse (örneğin WhatsApp).


Bu, SOLID İlkelerindeki Tek Sorumluluk İlkesinin ihlalidir. Düzeltmek için bildirimi gönderen ayrı bir sınıf oluşturacağız.


Yukarıdaki kodu SOLID İlkelerine göre yeniden düzenleyelim


 public class SavingsAccount { public int balance; public String name; public SavingsAccount(int initialBalance, String name) { this.balance = initialBalance; this.name = name; System.out.println("Created a savings account with balance = " + initialBalance); } public void debit(int amountToDebit) { // debit business logic } public void credit(int amountToCredit) { // credit business logic } public void printBalance() { System.out.println("Name: " + name+ " Account Balance: " + balance); } public void sendNotification(Medium medium) { Sender.sendNotification(medium, this); } }


 public enum NotificationMedium { SMS, EMAIL }


 public class Sender { public static void sendNotification(NotificationMedium medium, SavingsAccount account) { // extract account data from the account object if (medium == NotificationMedium.SMS) { //logic to send SMS here } else if (medium == NotificationMedium.EMAIL) { // logic to send Email here } } }


Artık kodu yeniden düzenlediğimiz için NotificationMedium veya formatta herhangi bir değişiklik olursa Sender sınıfını değiştireceğiz. Ancak SavingsAccount çekirdek mantığında bir değişiklik olması durumunda SavingsAccount sınıfında da değişiklikler olacaktır.


Bu, ilk örnekte gözlemlediğimiz ihlali düzeltir.

2. Açma/Kapama Prensibi

Open Close prensibi, sınıfları genişletmeye açık (ek özellikler eklenmesi durumunda) ancak değişiklik için kapalı olacak şekilde tasarlamamız gerektiğini belirtir. Değişikliklere kapalı olmak bize iki şekilde yardımcı olur:


  • Çoğu zaman orijinal sınıf kaynağı mevcut bile olmayabilir. Projeniz tarafından tüketilen bir bağımlılık olabilir.


  • Orijinal sınıfı değiştirmeden tutmak hata olasılığını azaltır. Çünkü değiştirmek istediğimiz sınıfa bağlı başka sınıflar da olabilir.


Aç Kapat Prensibinin bir örneğini görmek için alışveriş sepeti örneğine (e-ticaret sitelerinde uygulanan örnek gibi) bakalım.


Ekleyebileceğiniz Item listesini içeren Cart adında bir sınıf oluşturacağım. Öğenin türüne ve vergilendirmesine bağlı olarak, Cart sınıfı içindeki toplam sepet değerini hesaplayan bir yöntem oluşturmak istiyoruz.


Sınıflar genişletilmeye açık, değiştirilmeye kapatılmalıdır.


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart() { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { if (item.getItemType() == GIFT) { // 8% tax on gift + 2% gift wrap cost value += (item.getValue()*1.08) + item.getValue()*0.02 } else if (item.getItemType() == ItemType.ELECTRONIC_ITEM) { value += (item.getValue()*1.11); } else { value += item.getValue()*1.10; } } return value; } }


 @Getter @Setter public abstract class Item { protected double price; private ItemType itemType; public double getValue() { return price; } }
 public enum ItemType { ELECTRONIC, GIFT }


Yukarıdaki örnekte, calculateCartValue yöntemi, sepet içindeki tüm öğeleri yineleyerek ve öğenin türüne göre mantığı çağırarak sepet değerini hesaplar.


Bu kod doğru görünse de SOLID İlkelerini ihlal ediyor.


Diyelim ki sepet değerini hesaplarken farklı bir ürün türü (mesela Bakkal) için yeni bir kural eklememiz gerekiyor. Bu durumda, orijinal Cart sınıfını değiştirmemiz ve içine Grocery türündeki öğeleri kontrol eden başka bir else if koşulu yazmamız gerekir.


Ancak, küçük bir yeniden düzenlemeyle kodun açma/kapama ilkesine uymasını sağlayabiliriz. Bakalım nasıl olacak.


Öncelikle aşağıda gösterildiği gibi Item sınıfını soyut hale getireceğiz ve farklı Item (ler) türleri için somut sınıflar yapacağız.


 public abstract class Item { protected double price; public double getValue() { return price; } }


 public class ElectronicItem extends Item { public ElectronicItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.11; } }


 public class GiftItem extends Item { public GiftItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.08 + super.getValue()*0.02; } }


 public class GroceryItem extends Item { public GroceryItem(double price) { super.price = price; } @Override public double getValue() { return super.getValue()*1.03; } }


GroceryItem , GiftItem ve ElectronicItem gibi her somut Öğe sınıfının içinde vergilendirme ve değer hesaplamasına yönelik iş mantığını içeren getValue() yöntemi uygulanır.


Şimdi, Cart sınıfını soyut Item sınıfına bağlı hale getireceğiz ve aşağıda gösterildiği gibi her bir öğe için getValue() yöntemini çağıracağız.


 import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } }


Şimdi, bu yeniden düzenlenmiş kodda, yeni Item türleri tanıtılsa bile, Cart sınıfı değişmeden kalır. Polimorfizm nedeniyle, ArrayList items içindeki Item gerçek türü ne olursa olsun, o sınıfın getValue() yöntemi çağrılacaktır.

3. Liskov'un Değiştirme Prensibi

Liskov'un ikame ilkesi, belirli bir kodda, bir üst sınıfın nesnesini alt sınıfın bir nesnesiyle değiştirsek bile kodun bozulmaması gerektiğini belirtir. Başka bir deyişle, bir alt sınıf bir üst sınıfı miras aldığında ve onun yöntemlerini geçersiz kıldığında, üst sınıftaki yöntemin davranışıyla tutarlılığı korumalıdır.


Örnek olarak aşağıdaki sınıfları Vehicle ve iki sınıfı da Car ve Bicycle sınıfını yaparsak. Şimdi Araç sınıfında startEngine() adında bir yöntem oluşturduğumuzu varsayalım, Car sınıfında override edilebilir ancak Bicycle sınıfında Bicycle motoru olmadığı için desteklenmeyecektir (aşağıdaki kod örneğine bakın)


Alt sınıf, yöntemleri geçersiz kılarken üst sınıfın davranışıyla tutarlılığı korumalıdır.

 class Vehicle { public void startEngine() { // start engine of the vehicle } } class Car extends Vehicle { @Override public void startEngine() { // Start Engine } } class Bicycle extends Vehicle { @Override public void startEngine(){ throw new UnsupportedOperationException("Bicycle doesn't have engine"); } }


Şimdi, araç türünde bir nesne bekleyen ve startEngine() yöntemine dayanan bir kod olduğunu varsayalım. Bu kod parçasını çağırırken Vehicle tipi bir nesneyi iletmek yerine bir Bicycle nesnesini geçersek, bu kodda sorunlara yol açacaktır. Bicycle (ler) sınıfının yöntemi startEngine() yöntemi çağrıldığında bir istisna oluşturacağından. Bu, SOLID İlkelerinin (Liskov'un ikame ilkesi) ihlali anlamına gelir.


Bu sorunu çözmek için, MotorizedVehicle ve NonMotorizedVehicle üzere iki sınıf oluşturabilir ve Car MotorizedVehicle sınıfından miras almasını ve Bicycle NonMotorizedVehicle sınıfından miras almasını sağlayabiliriz.


 class Vehicle { } class MotorizedVehicle extends Vehicle { public void startEngine() { // start engine here } } class Car extends MotorizedVehicle { @Override public void startEngine() { // Start Engine } } class NonMotorizedVehicle extends Vehicle { public void startRiding() { // Start without engine } } class Bicycle extends NonMotorizedVehicle { @Override public void startRiding(){ // Start riding without the engine. } }


4. Arayüz Ayırma Prensibi

SOLID İlkelerindeki “I”, Arayüz Ayrıştırma İlkesini temsil eder.


Arayüz ayırma ilkesi, uygulama sınıflarını kullanılmayan yöntemleri uygulamaya zorlayan daha büyük arayüzlere sahip olmak yerine, daha küçük arayüzlere sahip olmamız ve sınıfların uygulanmasını sağlamamız gerektiğini belirtir. Bu şekilde sınıflar yalnızca ilgili yöntemleri uygular ve temiz kalır.

Arayüzlerinizi tek bir büyük arayüz yerine birden fazla küçük arayüze bölün.

Örneğin, Java'daki yerleşik Koleksiyonlar çerçevesine bakalım. Java, diğer veri yapılarının yanı sıra LinkedList ve ArrayList veri yapısını da sağlar.


ArrayList sınıfı şu arayüzleri uygular: Serializable , Cloneable , Iterable , Collection , List ve RandomAccess .

LinkedList sınıfı Serializable , Cloneable , Iterable , Collection , Deque , List ve Queue uygular.


Bu oldukça fazla arayüz demek!


Java geliştiricileri bu kadar çok arayüze sahip olmak yerine Serializable , Cloneable , Iterable , Collecton , List ve RandomAccess tek bir arayüzde, diyelim ki IList arayüzünde birleştirebilirlerdi. Artık hem ArrayList hem de LinkedList sınıfları bu yeni IList arayüzünü uygulayabilirdi.


Ancak LinkedList rastgele erişimi desteklemediğinden, RandomAccess arayüzündeki yöntemleri uygulayabilir ve birisi onu çağırmaya çalıştığında UnsupportedOperationException atabilir.


Ancak bu, LinkedList sınıfını gerekli olmasa bile RandomAccess arayüzü içindeki yöntemleri uygulamaya "zorlayacağı" için SOLID İlkelerindeki Arayüz Ayrımı ilkesinin ihlali anlamına gelir.


Bu nedenle, arayüzü ortak davranışa göre bölmek ve her sınıfın tek bir büyük arayüz yerine birçok arayüzü uygulamasına izin vermek daha iyidir.

Bağımlılığı Ters Çevirme Prensibi

Bağımlılığı Ters Çevirme İlkesi, üst seviyedeki sınıfların doğrudan alt seviyedeki sınıflara bağlı olmaması gerektiğini belirtir. Bu, iki seviye arasında sıkı bir bağlantıya neden olur.


Bunun yerine alt sınıflar, üst düzey sınıfların bağlı olması gereken bir arayüz sağlamalıdır.


Sınıflar yerine arayüzlere bağlı olun


Örneğin yukarıda gördüğümüz Cart örneğine devam edelim ve onu bazı ödeme seçenekleri ekleyecek şekilde geliştirelim. DebitCard ve Paypal iki tür ödeme seçeneğimiz olduğunu varsayalım. Şimdi, Cart sınıfında, placeOrder , sepet değerini hesaplayacak ve sağlanan ödemeye göre ödemeyi başlatacak bir yöntem eklemek istiyoruz. yöntem.


Bunu yapmak için yukarıdaki Cart örneğinde, iki ödeme seçeneğini Cart sınıfının içindeki alanlar olarak ekleyerek bağımlılığı ekleyebilirdik. Ancak bu, Cart sınıfını DebitCard ve Paypal sınıfıyla sıkı bir şekilde birleştirecektir.


Bunun yerine bir Payment arayüzü oluşturacağız ve hem DebitCard hem de Paypal sınıflarının Payment arayüzlerini uygulamasını sağlayacağız. Artık Cart sınıfı bireysel ödeme türlerine değil, Payment arayüzüne bağlı olacaktır. Bu, sınıfların gevşek bir şekilde bağlı kalmasını sağlar.


Aşağıdaki koda bakın.


 public interface Payment { void doPayment(double amount); } public class PaypalPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate paypal payment } } public class DebitCardPayment implements Payment { @Override public void doPayment(double amount) { // logic to initiate payment via debit card } } import java.util.ArrayList; import java.util.List; public class Cart { private List<Item> items; private Payment paymentOption; public Cart(Payment paymentOption) { this.items = new ArrayList<>(); this.paymentOption = paymentOption; } public void addToCart(Item item) { items.add(item); } public double calculateCartValue() { double value = 0.0; for(Item item: items) { value += item.getValue(); } return value; } public void placeOrder() { this.paymentOption.doPayment(calculateCartValue()); } }


Nesne yönelimli programlama, GoF tasarım kalıpları ve düşük düzeyli tasarım röportajları hakkında daha fazla bilgi edinmek istiyorsanız yüksek puan alan Nesne Yönelimli Programlama + Java Tasarım Desenleri kursuma göz atın