paint-brush
जावा में ठोस सिद्धांत: एक शुरुआती मार्गदर्शिकाद्वारा@ps06756
14,604 रीडिंग
14,604 रीडिंग

जावा में ठोस सिद्धांत: एक शुरुआती मार्गदर्शिका

द्वारा Pratik Singhal14m2024/03/22
Read on Terminal Reader

बहुत लंबा; पढ़ने के लिए

SOLID सिद्धांत स्केलेबल सॉफ़्टवेयर विकसित करने के लिए आवश्यक ऑब्जेक्ट ओरिएंटेड प्रोग्रामिंग के सिद्धांत हैं। सिद्धांत हैं: एस: एकल उत्तरदायित्व सिद्धांत ओ: खुला/बंद सिद्धांत एल: लिस्कोव का प्रतिस्थापन सिद्धांत I: इंटरफ़ेस पृथक्करण सिद्धांत डी: निर्भरता व्युत्क्रम सिद्धांत
featured image - जावा में ठोस सिद्धांत: एक शुरुआती मार्गदर्शिका
Pratik Singhal HackerNoon profile picture
0-item

SOLID डिज़ाइन सिद्धांत सबसे महत्वपूर्ण डिज़ाइन सिद्धांत हैं जिन्हें आपको क्लीन कोड लिखने के लिए जानने की आवश्यकता है। SOLID सिद्धांतों पर ठोस पकड़ होना किसी भी प्रोग्रामर के लिए एक अनिवार्य कौशल है। वे वह आधार हैं जिस पर अन्य डिज़ाइन पैटर्न विकसित किए जाते हैं।


इस लेख में, हम कुछ वास्तविक जीवन के उदाहरणों का उपयोग करके SOLID डिज़ाइन सिद्धांतों से निपटेंगे और उनके महत्व को समझेंगे।


बहुरूपता, अमूर्तता और वंशानुक्रम के साथ, उद्देश्य-उन्मुख प्रोग्रामिंग में अच्छा होने के लिए SOLID सिद्धांत वास्तव में महत्वपूर्ण हैं।

ठोस सिद्धांत महत्वपूर्ण क्यों हैं?

SOLID सिद्धांत कई कारणों से महत्वपूर्ण हैं:


  • SOLID सिद्धांत हमें स्वच्छ और रखरखाव योग्य कोड लिखने की अनुमति देते हैं: जब हम एक नया प्रोजेक्ट शुरू करते हैं, तो शुरू में कोड की गुणवत्ता अच्छी होती है क्योंकि हमारे पास सीमित सुविधाओं का सेट होता है जिन्हें हम लागू करते हैं। हालाँकि, जैसे-जैसे हम अधिक सुविधाएँ शामिल करते हैं, कोड अव्यवस्थित होने लगता है।


  • ठोस सिद्धांत अमूर्तता, बहुरूपता और वंशानुक्रम की नींव पर निर्मित होते हैं और सामान्य उपयोग के मामलों के लिए डिजाइन पैटर्न का नेतृत्व करते हैं। इन डिज़ाइन पैटर्न को समझने से प्रोग्रामिंग में सामान्य उपयोग के मामलों को लागू करने में मदद मिलती है।


  • SOLID सिद्धांत हमें स्वच्छ कोड लिखने में मदद करते हैं जिससे कोड की परीक्षण क्षमता में सुधार होता है। ऐसा इसलिए है क्योंकि कोड मॉड्यूलर और शिथिल युग्मित है। प्रत्येक मॉड्यूल को स्वतंत्र रूप से विकसित और स्वतंत्र रूप से परीक्षण किया जा सकता है।


आइए अब वास्तविक दुनिया के उदाहरणों के साथ प्रत्येक SOLID सिद्धांतों को विस्तार से देखें।

1. एकल उत्तरदायित्व सिद्धांत

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 वर्ग को देखें, तो यह कई कारणों से बदल सकता है:


  1. यदि SavingsAccount वर्ग के मूल तर्क (जैसे debit , credit , आदि) में कोई बदलाव है।


  2. यदि बैंक एक नया अधिसूचना माध्यम (मान लें कि व्हाट्सएप) शुरू करने का निर्णय लेता है।


यह 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 वर्ग में भी परिवर्तन होंगे।


यह उस उल्लंघन को ठीक करता है जो हमने पहले उदाहरण में देखा था।

2. खोलें/बंद करने का सिद्धांत

ओपन क्लोज़ सिद्धांत कहता है कि हमें कक्षाओं को इस तरह से डिज़ाइन करना चाहिए ताकि वे विस्तार के लिए खुले हों (अतिरिक्त सुविधाएँ जोड़ने के मामले में) लेकिन संशोधन के लिए बंद हों। संशोधन के लिए बंद होने से हमें दो तरह से मदद मिलती है:


  • कई बार, मूल वर्ग स्रोत उपलब्ध भी नहीं हो पाता है। यह आपके प्रोजेक्ट द्वारा उपभोग की गई निर्भरता हो सकती है।


  • मूल वर्ग को अपरिवर्तित रखने से बग की संभावना कम हो जाती है। चूँकि अन्य वर्ग भी हो सकते हैं जो उस वर्ग पर निर्भर हों जिसे हम संशोधित करना चाहते हैं।


ओपन क्लोज़ सिद्धांत का एक उदाहरण देखने के लिए, आइए एक शॉपिंग कार्ट के उदाहरण पर नज़र डालें (जैसे कि ई-कॉमर्स वेबसाइटों पर लागू किया गया)।


मैं 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() विधि लागू की जाएगी।

3. लिस्कोव का प्रतिस्थापन सिद्धांत

लिस्कोव का प्रतिस्थापन सिद्धांत बताता है कि किसी दिए गए कोड में, भले ही हम सुपरक्लास के ऑब्जेक्ट को चाइल्ड क्लास के ऑब्जेक्ट से बदल दें, कोड टूटना नहीं चाहिए। दूसरे शब्दों में, जब एक उपवर्ग एक सुपरक्लास को विरासत में लेता है और उसके तरीकों को ओवरराइड करता है, तो उसे सुपरक्लास में विधि के व्यवहार के साथ स्थिरता बनाए रखनी चाहिए।


उदाहरण के लिए, यदि हम निम्नलिखित वर्गों को 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. } }


4. इंटरफ़ेस पृथक्करण सिद्धांत

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 डिज़ाइन पैटर्न और निम्न-स्तरीय डिज़ाइन साक्षात्कार के बारे में अधिक जानने में रुचि रखते हैं, तो मेरे उच्च श्रेणी के पाठ्यक्रम ऑब्जेक्ट ओरिएंटेड प्रोग्रामिंग + जावा डिज़ाइन पैटर्न को अवश्य देखें।