LSP (Liskov Substitution Principle) — SOLID का तीसरा सिद्धांत, जो ऑब्जेक्ट-ओरिएंटेड प्रोग्रामिंग में सही इनहेरिटेंस की शर्तों को परिभाषित करता है। यह सिद्धांत बार्बरा लिस्कोव द्वारा 1987 में तैयार किया गया था और इस प्रकार स्थापित किया गया: यदि S, T का एक उपप्रकार है, तो T प्रकार की वस्तुओं को S प्रकार की वस्तुओं से बदला जा सकता है बिना प्रोग्राम के गुणों को बदले। जैसा कि रॉबर्ट मार्टिन की पुस्तक Clean Architecture (2017) में कहा गया है, प्रतिस्थापन सिद्धांत के अनुसार उपवर्ग को आधार वर्ग के अनुबंध को कमजोर नहीं करना चाहिए।
मुख्य बातें
LSP (Liskov Substitution Principle) — प्रतिस्थापन सिद्धांत जो बारबरा लिस्कोव द्वारा 1987 में OOPSLA सम्मेलन में तैयार किया गया था। आपचारिक परिभाषा: मान लीजिए q(x) प्रकार T की वस्तुओं x का एक साबित करने योग्य गुण है। तब q(y) प्रकार S की वस्तुओं y के लिए साबित होना चाहिए, जहाँ S, T का एक उपप्रकार है। सरल शब्दों में: एक उपवर्ग की वस्तुओं को इस प्रकार व्यवहार करना चाहिए कि आधार वर्ग के साथ काम करने वाला कोड उपवर्ग के साथ भी सही ढंग से काम करता रहे।
व्यवहार में, LSP का अर्थ है कि उपवर्ग को आधार वर्ग का अनुबंध नहीं तोड़ना चाहिए। अनुबंध में पूर्वशर्तें (विधि को कॉल करने के लिए क्या आवश्यक है), पश्चात्शर्तें (कॉल के बाद क्या गारंटी है), और इन्वारिएंट (शर्तें जो वस्तु के जीवनकाल में बनी रहती हैं) शामिल हैं। उपवर्ग पूर्वशर्तों को मजबूत कर सकता है या पश्चात्शर्तों को कमजोर कर सकता है — यही LSP उल्लंघन है।
LSP उल्लंघन का एक उत्तम उदाहरण एक वर्ग है जो एक आयत से विरासत पाता है। आयत का setWidth तरीका चौड़ाई सेट करता है, जबकि वर्ग में यह चौड़ाई और ऊंचाई दोनों सेट करता है। एक ग्राहक जो आयत व्यवहार की उम्मीद करता है (एक पक्ष को बदलने से दूसरा प्रभावित नहीं होता) को एक अनपेक्षित परिणाम मिलता है। एक वर्ग आयत का मान्य उपप्रकार नहीं है।
LSP सही इनहेरिटेंस के लिए तीन शर्तें स्थापित करता है: उपवर्ग की पूर्वशर्तें आधार वर्ग की पूर्वशर्तों से अधिक सख्त नहीं हो सकतीं (उपवर्ग अधिक नहीं माँगता), उपवर्ग की पश्चात्शर्तें आधार वर्ग की पश्चात्शर्तों से कमजोर नहीं हो सकतीं (उपवर्ग कम गारंटी नहीं देता), और आधार वर्ग के इन्वारिएंट को उपवर्ग में संरक्षित रखना चाहिए। ये शर्तें बर्ट्रंड मैयर के अनुबंध द्वारा डिजाइन नियम के रूप में जानी जाती हैं।
यदि कम से कम एक शर्त का उल्लंघन होता है, तो पॉलिमॉर्फिजम का उपयोग करने वाला कोड विफल हो सकता है। कंपाइलर अर्थगत अनुबंधों की जाँच नहीं करता, केवल वाक्यरचनागत की। इसलिए, LSP स्थिर टाइपिंग का नहीं, बल्कि आर्किटेक्चरल अनुशासन का मामला है।
LSP तंत्र प्रकारों की व्यवहारिक अनुकूलता पर आधारित है। यदि वर्ग S, वर्ग T से विरासत पाता है, तो ग्राहक कोड को S का उपयोग वहाँ करने में सक्षम होना चाहिए जहाँ T की उम्मीद है, बिना अपने व्यवहार को बदले। इसमें न केवल तरीकों के हस्ताक्षर बल्कि उनके अर्थ विज्ञान भी शामिल हैं।
LSP उपवर्ग को नया व्यवहार जोड़ने से नहीं रोकता। लेकिन आधार वर्ग के लिए लिखे गए कोड की उम्मीदों का उल्लंघन वर्जित है। यदि आधार वर्ग गारंटी देता है कि save तरीका अपवाद नहीं फेंकता, तो उपवर्ग को उन्हें नहीं फेंकना चाहिए। यदि आधार वर्ग एक गैर-नकारात्मक मान लौटाता है, तो उपवर्ग को एक नकारात्मक मान नहीं लौटाना चाहिए।
वास्तविक परियोजनाओं में, LSP अक्सर उपवर्ग के तरीकों में सार्तिक तर्क जोड़ने पर टूटता है: «यदि शर्त — अपवाद फेंकों», «यदि शर्त — null लौटाओं»। इनमें से प्रत्येक «आश्चर्य» पॉलिमॉर्फिजम को कमजोर करता है और ग्राहक कोड को कॉल करने से पहले वस्तु के प्रकार की जाँच करने के लिए मजबूर करता है — जो ऑब्जेक्ट-ओरिएंटेड डिजाइन के विचार का ही विरोधात्मक है।
मोबाइल परियोजनाओं में, एक सामान्य LSP उल्लंघन आधार ViewModel बनाते समय होता है। यदि BaseViewModel गारंटी देता है कि onCleared तरीका सभी संसाधनों को मुक्त करता है, और एक उपवर्ग इस तरीके को खाली के रूप में बदल देता है — पॉलिमॉर्फिक onCleared कॉल के माध्यम से संसाधन सफाई पर निर्भर कोई भी कोड गलत काम करेगा। LSP के अनुसार उपवर्ग को super.onCleared() को कॉल करना चाहिए या स्वयं वही काम करना चाहिए। LifecycleObserver के माध्यम से संरचना एक ऐसा विकल्प है जो जीवनचक्र प्रबंधन में LSP उल्लंघन को समाप्त करता है।
LSP उल्लंघन के मुख्य संकेतों में शामिल हैं: किसी तरीके को कॉल करने से पहले instanceof या is के माध्यम से वस्तु के प्रकार की जाँच करना, खाली तरीकों का कार्यान्वयन (stubs), NotImplementedError या UnsupportedOperationException फेंकना, मान के बजाय null लौटाना। इनमें से प्रत्येक पैटर्न संकेत करता है कि उपवर्ग एक मान्य उपप्रकार नहीं है।
एक और सामान्य संकेत «है» (is-a) संबंध को मॉडल करने के बजाय कोड पुनरुपयोग के उद्देश्य से विरासत है। वर्ग Bird में fly() तरीका है। वर्ग Penguin Bird से विरासत पाता है और fly() को खाली या अपवाद फेंकने के रूप में बदल देता है। यह LSP उल्लंघन है: पैंगुइन पक्षी का मान्य उपप्रकार नहीं है।
मोबाइल डेवलपमेंट में, LSP का उल्लंघन तब होता है जब स्टड तरीकों के साथ आधार ViewHolder, Fragment या ViewController वर्ग बनाए जाते हैं। यदि एक उपवर्ग आधार वर्ग के आधे तरीकों का उपयोग नहीं करता — विरासत गलत चुनी गई। संरचना या इंटरफ़ेस पृथक्करण समस्या को अधिक सही ढंग से हल करता है।
LSP की जाँच के लिए एक सरल परीक्षण: आधार वर्ग के लिए एक यूनिट टेस्ट लिखें जो इसके अनुबंध (लौटाई गई मान, अपवाद, दव्तीयक प्रभाव) की पुष्टि करता है। इस टेस्ट को प्रत्येक उपवर्ग के लिए चलाएं। यदि टेस्ट विफल होता है — LSP टूट गया है। इस दृष्टिकोण को «आधार वर्ग अनुबंध के माध्यम से परीक्षण» कहा जाता है।
Android परियोजनाओं में, ऐसा परीक्षण ViewModel और Repository के लिए उपयोगी है। यदि BaseViewModel एक त्रुटि से पहले Loading स्थिति की गारंटी देता है, और एक उपवर्ग Loading के बिना ही त्रुटि फेंकता है — परीक्षण CI चरण में LSP उल्लंघन का पता लगाएगा।
आइए एक Android उदाहरण को ClickListener हैंडलिंग के साथ देखें। LSP उल्लंघन तब होता है जब आधार कार्यान्वयन कुछ गारंटी देता है और उपवर्ग इसे तोड़ देता है।
// गारंटी के साथ बेस क्लास: onClick कॉल किया जाएगा
open class BaseClickListener {
open fun onClick(view: View) {
// बुनियादी हैंडलिंग
}
}
// LSP उल्लंघन: उपवर्ग एक शर्त जोड़ता है जो अपवाद फेंकता है
class RestrictedClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (!isLoggedIn) {
throw IllegalStateException("Not logged in")
}
super.onClick(view)
}
}
// सही समाधान: अनुबंध का उल्लंघन नहीं हुआ
class ConditionalClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (isLoggedIn) {
super.onClick(view)
}
}
}
एक iOS उदाहरण DataSource प्रोटोकॉल के साथ डेटा के बजाय nil लौटाकर LSP उल्लंघन दर्शाता है:
// अनुबंध के साथ प्रोटोकॉल: डेटा या त्रुटि लौटाता है
protocol DataProvider {
func fetchData() async throws -> [String]
}
// LSP उल्लंघन: त्रुटि के बिना nil लौटाता है
class SilentFailProvider: DataProvider {
func fetchData() async throws -> [String] {
return [] // त्रुटि के बजाय खाली ऐरे
}
}
// सही LSP अनुपालन
class NetworkProvider: DataProvider {
func fetchData() async throws -> [String] {
throw NetworkError.timeout
}
}
एक व्यावहारिक नियम: यदि एक उपवर्ग आधार वर्ग का अनुबंध पूरा नहीं कर सकता, तो उसे उपवर्ग नहीं होना चाहिए। एक विकल्प न्यूनतम अनुबंध के साथ एक इंटरफ़ेस निकालना और इसे प्रत्येक प्रकार में अपने तरीके से लागू करना है।
संरचना उन स्थितियों में विरासत से बेहतर होती है जहाँ «है» (is-a) संबंध अस्पष्ट या सापेक्षिक है। एक उत्तम उदाहरण: क्या Manager एक Employee है? हाँ। लेकिन क्या Square एक मान्य Rectangle है? LSP कहता है «नहीं»। यदि आप विरासत की शुद्धता के बारे में संदिग्ध हैं — संरचना चुनें।
मोबाइल डेवलपमेंट में, संरचना अक्सर निर्भरता इंजेक्शन के माध्यम से उपयोग की जाती है: आधार वर्ग से व्यवहार विरासत में लेने के बजाय, एक वर्ग इसे एक कंस्ट्रक्टर के माध्यम से प्राप्त करता है। ViewModel Repository से विरासत नहीं पाता बल्कि इसे एक निर्भरता के रूप में स्वीकार करता है। यह परिभाषा के अनुसार LSP उल्लंघन को समाप्त करता है — कोई विरासत नहीं, कोई अनुबंध उल्लंघन नहीं।
संकेत कि विरासत को संरचना से बदला जाना चाहिए: उपवर्ग आधार वर्ग के कुछ तरीकों का उपयोग नहीं करता, उपवर्ग तरीकों को खाली स्टड के रूप में बदल देता है, ग्राहक कोड instanceof के माध्यम से वस्तु के प्रकार की जाँच करता है। इन मामलों में, विरासत गलत चुनी गई और LSP टूट गया है।
इंटरफ़ेस विरासत के बिना LSP समस्या को हल करते हैं: प्रत्येक प्रकार ठीक उन तरीकों को लागू करता है जिनकी उसे आवश्यकता है। fly() तरीके के साथ एक सामान्य Bird आधार वर्ग (जहाँ Penguin उड़ नहीं सकता) के बजाय — एक Flyable इंटरफ़ेस जिसे केवल उड़ने वाले पक्षी लागू करते हैं। Penguin Bird को fly() तरीके के बिना लागू करता है — LSP नहीं टूटा।
Android आर्किटेक्चर में, यह दृष्टिकोण पृथक्कृत UseCase इंटरफ़ेस के माध्यम से लागू किया जाता है: getAll, getById, save, delete तरीकों के साथ एक बड़े UseCase के बजाय — अलग-अलग GetItemsUseCase, SaveItemUseCase इंटरफ़ेस। एक ग्राहक केवल आवश्यक इंटरफ़ेस पर निर्भर करता है, और LSP के नज़रिए से उस इंटरफ़ेस को लागू करने वाला कोई भी वर्ग सही है।
अक्सर पूछे जाने वाले प्रश्न
विरासत एक भाषा तंत्र है; LSP उस तंत्र के सही उपयोग का एक नियम है। विरासत हस्ताक्षर अनुकूलता (सिंटैक्स) की गारंटी देती है; LSP को व्यवहारिक अनुकूलता (अर्थ विज्ञान) की आवश्यकता है। LSP के बिना विरासत पॉलिमॉर्फिजम देती है जो रनटाइम में टूट जाता है।
यदि आधार वर्ग गैर-null वापसी की गारंटी देता है — हाँ। यदि अनुबंध null (वैकल्पिक मान) की अनुमति देता है — नहीं। LSP null को प्रतिबंधित नहीं करता, बल्कि अनुबंध के कमजोर होने को प्रतिबंधित करता है। आधार वर्ग के दस्तावेज़ीकरण का अध्ययन करें और जाँच करें कि उपवर्ग का अनुबंध संगत है या नहीं।
LSP प्रोटोकॉल पर वैसे ही लागू होता है जैसे कि क्लासों पर। एक प्रोटोकॉल कार्यान्वयन को अर्थगत अनुबंध का पालन करना चाहिए: यदि एक प्रोटोकॉल किसी तरीके को non-throwing के रूप में परिभाषित करता है, तो कार्यान्वयन को त्रुटियाँ नहीं फेंकनी चाहिए। Swift कंपाइलेशन स्तर पर इसकी जाँच नहीं करता — जिम्मेदारी डिवेलपर पर है।
Kotlin में Sealed class एक विशेष मामला है क्योंकि पदानुक्रम बंद है और कंपाइलर को ज्ञात है। LSP sealed class पर कम हाद्तक लागू होता है क्योंकि सभी उपप्रकारों को when अभिव्यक्ति में स्पष्ट रूप से सूचीबद्ध किया जाता है। एक सीलड उपवर्ग त्रुटि स्थानीय होगी, छिपा हुआ पॉलिमॉर्फिक त्रुटि नहीं।
आधार वर्ग के लिए एक पैरामेटराइज्ड टेस्ट लिखें जो इसके सभी उपवर्गों के लिए चलता है। यह टेस्ट मुख्य व्यवहारिक अनुबंधों की जाँच करता है: लौटाई गई मान, अपवाद, अवस्थाएँ। यदि टेस्ट एक उपवर्ग पर विफल होता है — LSP टूट गया है। CI में, ऐसा टेस्ट पॉलिमॉर्फिक कोड की रिग्रेशन को रोकता है।
सारांश
हम एक मोबाइल एप्लिकेशन टर्नकी विकसित करेंगे
IT Sectr 2017 से स्टार्टअप और व्यवसायों के लिए iOS और Android एप्लिकेशन बनाता है। हम आपको सलाह देंगे और सर्वोत्तम समाधान प्रस्तावित करेंगे।
यह भी पढ़ें