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 ilkeleri birçok nedenden dolayı önemlidir:
Şimdi SOLID ilkelerinin her birini gerçek dünyadan örneklerle ayrıntılı olarak inceleyelim.
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:
SavingsAccount
sınıfının temel mantığında herhangi bir değişiklik olup olmadığı ( debit
, credit
vb. gibi).
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.
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:
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.
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. } }
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 İ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