SOLID डिज़ाइन सिद्धांत सबसे महत्वपूर्ण डिज़ाइन सिद्धांत हैं जिन्हें आपको क्लीन कोड लिखने के लिए जानने की आवश्यकता है। SOLID सिद्धांतों पर ठोस पकड़ होना किसी भी प्रोग्रामर के लिए एक अनिवार्य कौशल है। वे वह आधार हैं जिस पर अन्य डिज़ाइन पैटर्न विकसित किए जाते हैं।
इस लेख में, हम कुछ वास्तविक जीवन के उदाहरणों का उपयोग करके SOLID डिज़ाइन सिद्धांतों से निपटेंगे और उनके महत्व को समझेंगे।
बहुरूपता, अमूर्तता और वंशानुक्रम के साथ, उद्देश्य-उन्मुख प्रोग्रामिंग में अच्छा होने के लिए SOLID सिद्धांत वास्तव में महत्वपूर्ण हैं।
SOLID सिद्धांत कई कारणों से महत्वपूर्ण हैं:
आइए अब वास्तविक दुनिया के उदाहरणों के साथ प्रत्येक SOLID सिद्धांतों को विस्तार से देखें।
SOLID सिद्धांतों में S का मतलब एकल उत्तरदायित्व सिद्धांत है। एकल उत्तरदायित्व सिद्धांत कहता है कि किसी वर्ग के पास परिवर्तन का केवल एक ही कारण होना चाहिए। यह उन स्थानों की संख्या को सीमित करता है जहां हमें अपने प्रोजेक्ट में अतिरिक्त आवश्यकताओं को शामिल करते समय परिवर्तन करने की आवश्यकता होती है।
प्रत्येक वर्ग के पास परिवर्तन का बिल्कुल एक कारण होना चाहिए।
उदाहरण के लिए, मान लें कि हम जावा में एक बैंकिंग एप्लिकेशन डिज़ाइन कर रहे हैं जहां हमारे पास एक SavingsAccount
क्लास है जो डेबिट, क्रेडिट और sendUpdates
जैसे बुनियादी संचालन की अनुमति देता है। sendUpdate
विधि NotificationMedium
(जैसे ईमेल, एसएमएस, आदि) नामक एक एनम लेती है और उचित माध्यम से अपडेट भेजती है। हम उसके लिए कोड लिखेंगे जैसा कि नीचे दिखाया गया है।
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 }
अब, यदि आप उपरोक्त SavingsAccount
वर्ग को देखें, तो यह कई कारणों से बदल सकता है:
यदि SavingsAccount
वर्ग के मूल तर्क (जैसे debit
, credit
, आदि) में कोई बदलाव है।
यदि बैंक एक नया अधिसूचना माध्यम (मान लें कि व्हाट्सएप) शुरू करने का निर्णय लेता है।
यह SOLID सिद्धांतों में एकल उत्तरदायित्व सिद्धांत का उल्लंघन है। इसे ठीक करने के लिए हम एक अलग क्लास बनाएंगे जो नोटिफिकेशन भेजती है।
आइए उपरोक्त कोड को SOLID सिद्धांतों के अनुसार पुनः क्रियान्वित करें
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 } } }
अब, चूंकि हमने कोड को दोबारा तैयार कर लिया है, अगर NotificationMedium
या प्रारूप में कोई बदलाव होता है, तो हम Sender
वर्ग को बदल देंगे। हालाँकि, यदि SavingsAccount
के मूल तर्क में कोई परिवर्तन होता है, तो SavingsAccount
वर्ग में भी परिवर्तन होंगे।
यह उस उल्लंघन को ठीक करता है जो हमने पहले उदाहरण में देखा था।
ओपन क्लोज़ सिद्धांत कहता है कि हमें कक्षाओं को इस तरह से डिज़ाइन करना चाहिए ताकि वे विस्तार के लिए खुले हों (अतिरिक्त सुविधाएँ जोड़ने के मामले में) लेकिन संशोधन के लिए बंद हों। संशोधन के लिए बंद होने से हमें दो तरह से मदद मिलती है:
ओपन क्लोज़ सिद्धांत का एक उदाहरण देखने के लिए, आइए एक शॉपिंग कार्ट के उदाहरण पर नज़र डालें (जैसे कि ई-कॉमर्स वेबसाइटों पर लागू किया गया)।
मैं Cart
नाम से एक क्लास बनाने जा रहा हूं जिसमें उन Item
की एक सूची होगी जिन्हें आप इसमें जोड़ सकते हैं। वस्तु के प्रकार और उस पर कराधान के आधार पर हम एक ऐसी विधि बनाना चाहते हैं जो Cart
वर्ग के अंदर कुल कार्ट मूल्य की गणना करे।
कक्षाएं विस्तार के लिए खुली रहनी चाहिए और संशोधन के लिए बंद होनी चाहिए
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 }
उपरोक्त उदाहरण में, calculateCartValue
विधि कार्ट के अंदर सभी आइटमों पर पुनरावृत्ति करके और आइटम के प्रकार के आधार पर तर्क का आह्वान करके कार्ट मूल्य की गणना करती है।
हालाँकि यह कोड सही दिखता है, लेकिन यह SOLID सिद्धांतों का उल्लंघन करता है।
मान लीजिए कि हमें कार्ट मूल्य की गणना करते समय एक अलग प्रकार की वस्तु (जैसे कि किराना) के लिए एक नया नियम जोड़ने की आवश्यकता है। उस स्थिति में, हमें मूल वर्ग Cart
संशोधित करना होगा और उसके अंदर एक और else if
शर्त लिखनी होगी जो कि Grocery
की वस्तुओं की जांच करती है।
हालाँकि, थोड़ी रीफैक्टरिंग के साथ, हम कोड को खुले/बंद सिद्धांत का पालन करा सकते हैं। आइए देखें कैसे.
सबसे पहले, हम Item
वर्ग को अमूर्त बनाएंगे और विभिन्न प्रकार के Item
के लिए ठोस वर्ग बनाएंगे जैसा कि नीचे दिखाया गया है।
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
और ElectronicItem
जैसे वर्ग getValue()
पद्धति को लागू करते हैं जिसमें कराधान और मूल्य गणना के लिए व्यावसायिक तर्क शामिल होते हैं।
अब, हम Cart
क्लास को अमूर्त क्लास Item
पर निर्भर बनाएंगे और नीचे दिखाए अनुसार प्रत्येक आइटम के लिए getValue()
विधि लागू करेंगे।
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; } }
अब, इस रिफैक्टर कोड में, भले ही नए प्रकार के Item
पेश किए जाएं, Cart
क्लास अपरिवर्तित रहता है। बहुरूपता के कारण, items
ArrayList
के अंदर Item
का वास्तविक प्रकार जो भी हो, उस वर्ग की getValue()
विधि लागू की जाएगी।
लिस्कोव का प्रतिस्थापन सिद्धांत बताता है कि किसी दिए गए कोड में, भले ही हम सुपरक्लास के ऑब्जेक्ट को चाइल्ड क्लास के ऑब्जेक्ट से बदल दें, कोड टूटना नहीं चाहिए। दूसरे शब्दों में, जब एक उपवर्ग एक सुपरक्लास को विरासत में लेता है और उसके तरीकों को ओवरराइड करता है, तो उसे सुपरक्लास में विधि के व्यवहार के साथ स्थिरता बनाए रखनी चाहिए।
उदाहरण के लिए, यदि हम निम्नलिखित वर्गों को Vehicle
और दो वर्गों को Car
और Bicycle
वर्ग बनाते हैं। अब, मान लें कि हम वाहन वर्ग के भीतर startEngine()
नामक एक विधि बनाते हैं, इसे Car
वर्ग में ओवरराइड किया जा सकता है, लेकिन यह Bicycle
वर्ग में असमर्थित होगा क्योंकि Bicycle
में इंजन नहीं है (नीचे कोड नमूना देखें)
तरीकों को ओवरराइड करते समय उपवर्ग को सुपरक्लास के व्यवहार के साथ एकरूपता बनाए रखनी चाहिए।
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"); } }
अब, मान लें कि कुछ कोड है जो वाहन प्रकार के ऑब्जेक्ट की अपेक्षा करता है और startEngine()
विधि पर निर्भर करता है। यदि, कोड के उस टुकड़े को कॉल करते समय Vehicle
प्रकार के ऑब्जेक्ट को पास करने के बजाय हम Bicycle
ऑब्जेक्ट को पास करते हैं, तो इससे कोड में समस्याएं पैदा होंगी। चूंकि startEngine()
विधि को कॉल करने पर Bicycle
वर्ग की विधि एक अपवाद फेंक देगी। यह SOLID सिद्धांतों (लिस्कोव के प्रतिस्थापन सिद्धांत) का उल्लंघन होगा
इस समस्या को हल करने के लिए, हम दो वर्ग MotorizedVehicle
और NonMotorizedVehicle
बना सकते हैं और Car
MotorizedVehicle
क्लास से इनहेरिट कर सकते हैं और Bicycle
NonMotorizedVehicle
से इनहेरिट कर सकते हैं।
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 सिद्धांतों में "I" इंटरफ़ेस पृथक्करण सिद्धांत के लिए है।
इंटरफ़ेस पृथक्करण सिद्धांत बताता है कि अप्रयुक्त तरीकों को लागू करने के लिए कक्षाओं को लागू करने के लिए मजबूर करने वाले बड़े इंटरफेस के बजाय, हमारे पास छोटे इंटरफेस होने चाहिए और कक्षाओं को लागू करना चाहिए। इस तरह, कक्षाएं केवल प्रासंगिक तरीकों को लागू करती हैं और स्वच्छ रहती हैं।
अपने इंटरफ़ेस को एक बड़े इंटरफ़ेस के बजाय कई छोटे इंटरफ़ेस में विभाजित करें।
उदाहरण के लिए, आइए जावा में अंतर्निहित संग्रह ढांचे को देखें। अन्य डेटा संरचनाओं के अलावा, जावा LinkedList
और ArrayList
डेटा संरचना भी प्रदान करता है,
ArrayList
वर्ग निम्नलिखित इंटरफ़ेस लागू करता है: Serializable
, Cloneable
, Iterable
, Collection
, List
और RandomAccess
।
LinkedList
क्लास Serializable
, Cloneable
, Iterable
, Collection
, Deque
, List
और Queue
लागू करता है।
यह काफी सारे इंटरफ़ेस हैं!
इतने सारे इंटरफ़ेस रखने के बजाय, जावा डेवलपर्स Serializable
, Cloneable
, Iterable
, Collecton
, List
और RandomAccess
एक इंटरफ़ेस में जोड़ सकते थे, मान लें कि IList
इंटरफ़ेस। अब, ArrayList
और LinkedList
दोनों वर्ग इस नए IList
इंटरफ़ेस को लागू कर सकते थे।
हालाँकि, चूँकि LinkedList
रैंडम एक्सेस का समर्थन नहीं करता है, इसलिए यह RandomAccess
इंटरफ़ेस में विधियों को कार्यान्वित कर सकता था और जब कोई इसे कॉल करने का प्रयास करता है तो UnsupportedOperationException
फेंक सकता था।
हालाँकि, यह SOLID सिद्धांतों में इंटरफ़ेस पृथक्करण सिद्धांत का उल्लंघन होगा क्योंकि यह लिंक्डलिस्ट क्लास को RandomAccess
इंटरफ़ेस के अंदर तरीकों को लागू करने के लिए "मजबूर" करेगा, भले ही आवश्यकता न हो।
इसलिए, सामान्य व्यवहार के आधार पर इंटरफ़ेस को विभाजित करना बेहतर है और प्रत्येक वर्ग को एक बड़े इंटरफ़ेस के बजाय कई इंटरफ़ेस लागू करने दें।
निर्भरता व्युत्क्रम सिद्धांत कहता है कि ऊपरी स्तर की कक्षाओं को सीधे निचले स्तर की कक्षाओं पर निर्भर नहीं होना चाहिए। यह दो स्तरों के बीच एक तंग युग्मन का कारण बनता है।
इसके बजाय, निचली कक्षाओं को एक इंटरफ़ेस प्रदान करना चाहिए जिस पर उच्च-स्तरीय कक्षाओं को निर्भर होना चाहिए।
कक्षाओं के बजाय इंटरफेस पर निर्भर रहें
उदाहरण के लिए, आइए ऊपर देखे गए Cart
उदाहरण को जारी रखें और कुछ भुगतान विकल्प जोड़ने के लिए इसे बढ़ाएं। आइए मान लें कि हमारे पास DebitCard
और Paypal
दो प्रकार के भुगतान विकल्प हैं। अब, Cart
क्लास में, हम placeOrder
के लिए एक विधि जोड़ना चाहते हैं जो कार्ट मूल्य की गणना करेगी और आपूर्ति किए गए भुगतान के आधार पर भुगतान शुरू करेगी। तरीका।
ऐसा करने के लिए, हम Cart
क्लास के अंदर फ़ील्ड के रूप में दो भुगतान विकल्पों को जोड़कर उपरोक्त Cart
उदाहरण में निर्भरता जोड़ सकते थे। हालाँकि, यह Cart
क्लास को DebitCard
और Paypal
क्लास के साथ मजबूती से जोड़ देगा।
इसके बजाय, हम एक Payment
इंटरफ़ेस बनाएंगे और DebitCard
और Paypal
दोनों वर्गों में Payment
इंटरफ़ेस लागू करेंगे। अब, Cart
श्रेणी Payment
इंटरफ़ेस पर निर्भर करेगी, न कि व्यक्तिगत भुगतान प्रकारों पर। यह कक्षाओं को शिथिल रूप से जोड़े रखता है।
नीचे दिया गया कोड देखें.
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()); } }
यदि आप ऑब्जेक्ट-ओरिएंटेड प्रोग्रामिंग, GoF डिज़ाइन पैटर्न और निम्न-स्तरीय डिज़ाइन साक्षात्कार के बारे में अधिक जानने में रुचि रखते हैं, तो मेरे उच्च श्रेणी के पाठ्यक्रम ऑब्जेक्ट ओरिएंटेड प्रोग्रामिंग + जावा डिज़ाइन पैटर्न को अवश्य देखें।