गोश, OCP और LSP के बारे में कुछ अजीब गलतफहमियां हैं और कुछ कुछ शब्दावली और भ्रमित उदाहरणों के बेमेल होने के कारण हैं। यदि आप उन्हें उसी तरह से लागू करते हैं तो दोनों सिद्धांत केवल "समान बात" हैं। पैटर्न आम तौर पर कुछ अपवादों के साथ एक या दूसरे तरीके से सिद्धांतों का पालन करते हैं।
मतभेदों को और नीचे समझाया जाएगा लेकिन पहले हमें सिद्धांतों में डुबकी लगाने दें:
ओपन-क्लोज्ड सिद्धांत (OCP)
अंकल बॉब के अनुसार :
आपको इसे संशोधित किए बिना, कक्षाओं के व्यवहार का विस्तार करने में सक्षम होना चाहिए।
ध्यान दें कि इस मामले में विस्तारित शब्द का मतलब यह नहीं है कि आपको वास्तविक व्यवहार की आवश्यकता है जो नए व्यवहार की आवश्यकता है। देखें कि मैंने शब्दावली के पहले बेमेल पर कैसे उल्लेख किया है? कीवर्ड का extend
अर्थ केवल जावा में सबक्लासिंग है, लेकिन सिद्धांत जावा से पुराने हैं।
मूल बर्ट्रैंड मेयर से 1988 में आया था:
सॉफ्टवेयर इकाइयां (कक्षाएं, मॉड्यूल, फ़ंक्शन, आदि) विस्तार के लिए खुली होनी चाहिए, लेकिन संशोधन के लिए बंद।
यहां यह बहुत स्पष्ट है कि सिद्धांत सॉफ्टवेयर संस्थाओं पर लागू होता है । एक खराब उदाहरण सॉफ्टवेयर इकाई को ओवरराइड करेगा क्योंकि आप कुछ बिंदु प्रदान करने के बजाय पूरी तरह से कोड को संशोधित कर रहे हैं। सॉफ्टवेयर इकाई का व्यवहार अपने आप में एक्सटेंसिबल होना चाहिए और इसका एक अच्छा उदाहरण स्ट्रेटेजी-पैटर्न का कार्यान्वयन है (क्योंकि यह गो-पैटर्न गुच्छा IMHO को दिखाना सबसे आसान है):
// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {
// Context is however open for extension through
// this private field
private IBehavior behavior;
// The context calls the behavior in this public
// method. If you want to change this you need
// to implement it in the IBehavior object
public void doStuff() {
if (this.behavior != null)
this.behavior.doStuff();
}
// You can dynamically set a new behavior at will
public void setBehavior(IBehavior behavior) {
this.behavior = behavior;
}
}
// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
public void doStuff();
}
ऊपर के उदाहरण में Context
है बंद कर दिया आगे संशोधन के लिए। अधिकांश प्रोग्रामर शायद इसे बढ़ाने के लिए कक्षा को उप-वर्ग में डालना चाहते हैं, लेकिन यहां हम नहीं मानते क्योंकि यह व्यवहार है कि यह इंटरफ़ेस को लागू करने वाली किसी भी चीज़ के माध्यम से बदला जा सकता IBehavior
है।
यानी संदर्भ वर्ग संशोधन के लिए बंद है, लेकिन विस्तार के लिए खुला है । यह वास्तव में एक और बुनियादी सिद्धांत का पालन करता है क्योंकि हम व्यवहार को विरासत के बजाय वस्तु संरचना के साथ रख रहे हैं:
" क्लास ' इनहेरिटेंस ' पर ' फेवर' ऑब्जेक्ट कंपोज़िशन ।" (गैंग ऑफ़ फोर 1995: 20)
मैं पाठक को उस सिद्धांत पर पढ़ने दूंगा क्योंकि यह इस प्रश्न के दायरे से बाहर है। उदाहरण के साथ जारी रखने के लिए, मान लें कि हमारे पास IBehavior इंटरफ़ेस के निम्नलिखित कार्यान्वयन हैं:
public class HelloWorldBehavior implements IBehavior {
public void doStuff() {
System.println("Hello world!");
}
}
public class GoodByeBehavior implements IBehavior {
public void doStuff() {
System.out.println("Good bye cruel world!");
}
}
इस पैटर्न का उपयोग करके हम रनटाइम पर संदर्भ के व्यवहार setBehavior
को विस्तार बिंदु के रूप में विधि के माध्यम से संशोधित कर सकते हैं ।
// in your main method
Context c = new Context();
c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"
c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"
इसलिए जब भी आप "बंद" संदर्भ वर्ग का विस्तार करना चाहते हैं, तो इसे उप-वर्ग के आधार पर करें, यह "खुले" सहयोगी निर्भरता है। यह स्पष्ट रूप से एक ही बात नहीं है क्योंकि इस संदर्भ को अभी भी उपवर्ग के रूप में देखा जा सकता है। एलएसपी इस बारे में कोई उल्लेख नहीं करता है।
वंशानुक्रम के बजाय मिक्सिन्स के साथ विस्तार
उपवर्ग के अलावा OCP करने के अन्य तरीके भी हैं। एक तरह से अपनी कक्षाओं के उपयोग के माध्यम विस्तार के लिए खुला रखने के लिए है mixins । यह उन भाषाओं में उपयोगी है, जो कक्षा-आधारित के बजाय प्रोटोटाइप-आधारित हैं। विचार एक गतिशील वस्तु को अधिक विधियों या विशेषताओं के साथ आवश्यकतानुसार संशोधित करने का है, दूसरे शब्दों में वस्तुओं को जो अन्य वस्तुओं के साथ मिश्रित या "मिश्रण" करता है।
यहाँ एक मिश्रण का एक जावास्क्रिप्ट उदाहरण दिया गया है जो एंकर के लिए एक सरल HTML टेम्पलेट प्रस्तुत करता है:
// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
render: function() {
return '<a href="' + this.link +'">'
+ this.content
+ '</a>;
}
}
// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
this.content = content;
this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
setLink: function(youtubeid) {
this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
}
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);
// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");
console.log(ytLink.render());
// will output:
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>
विचार वस्तुओं को गतिशील रूप से विस्तारित करना है और इसका लाभ यह है कि ऑब्जेक्ट पूरी तरह से अलग डोमेन में होने पर भी तरीकों को साझा कर सकते हैं। उपरोक्त मामले में आप आसानी से अपने विशिष्ट कार्यान्वयन को बढ़ाकर अन्य प्रकार के HTML एंकर बना सकते हैं LinkMixin
।
OCP के संदर्भ में, "मिश्रण" एक्सटेंशन हैं। ऊपर दिए गए उदाहरण YoutubeLink
में हमारी सॉफ्टवेयर इकाई है जो संशोधन के लिए बंद है, लेकिन मिश्रण के उपयोग के माध्यम से एक्सटेंशन के लिए खुला है। ऑब्जेक्ट पदानुक्रम को चपटा किया जाता है जिससे प्रकारों की जांच करना असंभव हो जाता है। हालाँकि यह वास्तव में एक बुरी बात नहीं है, और मैं आगे बताऊंगा कि प्रकारों की जाँच करना आमतौर पर एक बुरा विचार है और बहुरूपता के साथ विचार को तोड़ देता है।
ध्यान दें कि इस पद्धति के साथ कई उत्तराधिकार करना संभव है क्योंकि अधिकांश extend
कार्यान्वयन कई वस्तुओं में मिश्रण कर सकते हैं:
_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);
केवल एक चीज जो आपको ध्यान में रखने की ज़रूरत है, वह है नामों का टकराव न करना, यानी मिक्सिन्स कुछ विशेषताओं या विधियों के समान नाम को परिभाषित करने के लिए होता है क्योंकि उन्हें ओवरराइड किया जाएगा। मेरे विनम्र अनुभव में यह एक गैर-मुद्दा है और अगर ऐसा होता है तो यह त्रुटिपूर्ण डिजाइन का संकेत है।
लिस्कोव के प्रतिस्थापन सिद्धांत (एलएसपी)
अंकल बॉब इसे बस द्वारा परिभाषित करता है:
व्युत्पन्न वर्गों को उनके आधार वर्गों के लिए प्रतिस्थापित किया जाना चाहिए।
यह सिद्धांत पुराना है, वास्तव में अंकल बॉब की परिभाषा सिद्धांतों को अलग नहीं करती है क्योंकि एलएसपी अभी भी ओसीपी से निकटता से संबंधित है, इस तथ्य से कि, ऊपर की रणनीति उदाहरण में, एक ही सुपरपाइप का उपयोग किया जाता है ( IBehavior
)। तो इसे बारबरा लिस्कॉव की मूल परिभाषा पर गौर करें और देखें कि क्या हम इस सिद्धांत के बारे में कुछ और जान सकते हैं जो गणितीय सिद्धांत की तरह दिखता है:
यहाँ क्या तलाश है निम्नलिखित प्रतिस्थापन संपत्ति की तरह कुछ है: प्रत्येक वस्तु के लिए तो o1
प्रकार के S
एक वस्तु है o2
प्रकार का T
ऐसा है कि सभी कार्यक्रमों के लिए P
के रूप में परिभाषित T
, के व्यवहार P
में कोई बदलाव नहीं है, जब o1
दिया जाता है o2
तो S
की एक उप-प्रकार है T
।
इस पर थोड़ी देर के लिए सिकुड़ जाएं, ध्यान दें क्योंकि यह कक्षाओं का उल्लेख नहीं करता है। जावास्क्रिप्ट में आप वास्तव में एलएसपी का पालन कर सकते हैं, भले ही यह स्पष्ट रूप से वर्ग-आधारित न हो। यदि आपके प्रोग्राम में कम से कम कुछ जावास्क्रिप्ट ऑब्जेक्ट्स की सूची है जो:
- उसी तरह से गणना करने की जरूरत है,
- एक ही व्यवहार है, और
- अन्यथा किसी तरह पूरी तरह से अलग हैं
... तो वस्तुओं को एक ही "प्रकार" के रूप में माना जाता है और यह वास्तव में कार्यक्रम के लिए मायने नहीं रखता है। यह मूलत: बहुरूपता है । सामान्य अर्थों में; यदि आप इसका उपयोग कर रहे हैं, तो आपको वास्तविक उपप्रकार जानने की आवश्यकता नहीं है। OCP इस बारे में कुछ भी स्पष्ट नहीं कहता है। यह वास्तव में एक डिजाइन गलती को इंगित करता है जो अधिकांश नौसिखिए प्रोग्रामर करते हैं:
जब भी आप किसी वस्तु के उपप्रकार की जाँच करने का आग्रह कर रहे हैं, तो आप सबसे अधिक संभावना है कि आप इसे गलत कर रहे हैं।
ठीक है, इसलिए यह हर समय गलत नहीं हो सकता है, लेकिन अगर आपके पास कुछ प्रकार की जाँच करने की इच्छा है instanceof
या आप इसके साथ काम कर रहे हैं, तो आप इस कार्यक्रम को अपने लिए थोड़ा और जटिल बना सकते हैं, जितना कि यह आवश्यक है। पर यह मामला हमेशा नहीं होता; काम करने के लिए त्वरित और गंदे हैक्स मेरे दिमाग में बनाने के लिए एक ठीक रियायत है यदि समाधान काफी छोटा है, और यदि आप निर्दयतापूर्ण रीफ़ैक्टरिंग का अभ्यास करते हैं , तो इसे बदलने की मांग करने के बाद इसमें सुधार हो सकता है।
वास्तविक समस्या के आधार पर, इस "डिज़ाइन गलती" के आसपास के तरीके हैं:
- सुपर क्लास पूर्वापेक्षाओं को नहीं बुला रहा है, कॉल करने वाले को ऐसा करने के लिए मजबूर कर रहा है।
- सुपर क्लास को एक सामान्य विधि याद आ रही है जिसे कॉल करने वाले को चाहिए।
ये दोनों सामान्य कोड डिज़ाइन "गलतियाँ" हैं। ऐसे कई रिफ्लेक्टर हैं जिन्हें आप कर सकते हैं, जैसे कि पुल-अप विधि या रिफ्लेक्टर जैसे विस्कोस पैटर्न ।
मैं वास्तव में विजिटर पैटर्न को बहुत पसंद करता हूं क्योंकि यह बड़े अगर-स्टेटमेंट स्पेगेटी की देखभाल कर सकता है और मौजूदा कोड पर आपके विचार से इसे लागू करना सरल है। कहें कि हमारे पास निम्नलिखित संदर्भ हैं:
public class Context {
public void doStuff(string query) {
// outcome no. 1
if (query.Equals("Hello")) {
System.out.println("Hello world!");
}
// outcome no. 2
else if (query.Equals("Bye")) {
System.out.println("Good bye cruel world!");
}
// a change request may require another outcome...
}
}
// usage:
Context c = new Context();
c.doStuff("Hello");
// prints "Hello world"
c.doStuff("Bye");
// prints "Bye"
इफ-स्टेटमेंट के परिणामों को अपने स्वयं के आगंतुकों में अनुवाद किया जा सकता है क्योंकि प्रत्येक को कुछ निर्णय और चलाने के लिए कुछ कोड पर निर्भर करता है। हम इन्हें इस तरह से निकाल सकते हैं:
public interface IVisitor {
public bool canDo(string query);
public void doStuff();
}
// outcome 1
public class HelloVisitor implements IVisitor {
public bool canDo(string query) {
return query.Equals("Hello");
}
public void doStuff() {
System.out.println("Hello World");
}
}
// outcome 2
public class ByeVisitor implements IVisitor {
public bool canDo(string query) {
return query.Equals("Bye");
}
public void doStuff() {
System.out.println("Good bye cruel world");
}
}
इस बिंदु पर, यदि प्रोग्रामर को विज़िटर पैटर्न के बारे में पता नहीं था, तो वह यह जांचने के लिए कि यह किसी निश्चित प्रकार का है, तो वह संदर्भ वर्ग को लागू करेगा। क्योंकि विज़िटर वर्गों के पास बूलियन canDo
पद्धति है, इसलिए यह निर्धारित करने के लिए कि क्या यह कार्य करने के लिए सही ऑब्जेक्ट है, तो कार्यान्वयनकर्ता उस विधि कॉल का उपयोग कर सकता है। संदर्भ वर्ग इस तरह से सभी आगंतुकों का उपयोग कर सकता है (और नए जोड़ सकता है):
public class Context {
private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();
public Context() {
visitors.add(new HelloVisitor());
visitors.add(new ByeVisitor());
}
// instead of if-statements, go through all visitors
// and use the canDo method to determine if the
// visitor object is the right one to "visit"
public void doStuff(string query) {
for(IVisitor visitor : visitors) {
if (visitor.canDo(query)) {
visitor.doStuff();
break;
// or return... it depends if you have logic
// after this foreach loop
}
}
}
// dynamically adds new visitors
public void addVisitor(IVisitor visitor) {
if (visitor != null)
visitors.add(visitor);
}
}
दोनों पैटर्न OCP और LSP का अनुसरण करते हैं, हालांकि वे दोनों उनके बारे में अलग-अलग बातें बता रहे हैं। तो कोड कैसे दिखता है अगर यह सिद्धांतों में से एक का उल्लंघन करता है?
एक सिद्धांत का उल्लंघन करना लेकिन दूसरे का अनुसरण करना
सिद्धांतों में से एक को तोड़ने के तरीके हैं लेकिन अभी भी दूसरे का पालन किया जा रहा है। नीचे दिए गए उदाहरण अच्छे कारण के लिए विरोधाभासी प्रतीत होते हैं, लेकिन मैंने वास्तव में इन्हें उत्पादन कोड में (और यहां तक कि वर्जन) पॉपिंग करते देखा है:
OCP का अनुसरण करता है लेकिन LSP का नहीं
कहते हैं कि हमारे पास दिए गए कोड हैं:
public interface IPerson {}
public class Boss implements IPerson {
public void doBossStuff() { ... }
}
public class Peon implements IPerson {
public void doPeonStuff() { ... }
}
public class Context {
public Collection<IPerson> getPersons() { ... }
}
कोड का यह टुकड़ा खुले-बंद सिद्धांत का पालन करता है। यदि हम संदर्भ GetPersons
विधि को कॉल कर रहे हैं , तो हम सभी व्यक्तियों को अपने स्वयं के कार्यान्वयन के साथ एक गुच्छा देंगे। इसका मतलब है कि IPerson संशोधन के लिए बंद है, लेकिन विस्तार के लिए खुला है। हालाँकि जब हमें इसका उपयोग करना होता है तो चीजें एक अंधेरा मोड़ लेती हैं:
// in some routine that needs to do stuff with
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
// now we have to check the type... :-P
if (person instanceof Boss) {
((Boss) person).doBossStuff();
}
else if (person instanceof Peon) {
((Peon) person).doPeonStuff();
}
}
आपको टाइप चेकिंग और टाइप कन्वर्सेशन करना है! याद रखें कि मैंने ऊपर कैसे उल्लेख किया है कि किस प्रकार की जाँच एक बुरी बात है ? अरे नहीं! लेकिन डर नहीं, जैसा कि ऊपर बताया गया है कि या तो कुछ पुल-अप रिफैक्टरिंग करते हैं या विज़िटर पैटर्न को लागू करते हैं। इस मामले में हम एक सामान्य विधि को जोड़ने के बाद बस एक पुल अप रिफैक्टरिंग कर सकते हैं:
public class Boss implements IPerson {
// we're adding this general method
public void doStuff() {
// that does the call instead
this.doBossStuff();
}
public void doBossStuff() { ... }
}
public interface IPerson {
// pulled up method from Boss
public void doStuff();
}
// do the same for Peon
लाभ यह है कि आपको एलएसपी के बाद अब सटीक प्रकार जानने की जरूरत नहीं है:
// in some routine that needs to do stuff with
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
// yay, no type checking!
person.doStuff();
}
एलएसपी का पालन करता है लेकिन ओसीपी का नहीं
कुछ कोड को देखते हैं जो एलएसपी का अनुसरण करते हैं लेकिन ओसीपी का नहीं, यह एक तरह से विरोधाभासी है लेकिन इस पर मेरे साथ सहन करना बहुत ही सूक्ष्म गलती है:
public class LiskovBase {
public void doStuff() {
System.out.println("My name is Liskov");
}
}
public class LiskovSub extends LiskovBase {
public void doStuff() {
System.out.println("I'm a sub Liskov!");
}
}
public class Context {
private LiskovBase base;
// the good stuff
public void doLiskovyStuff() {
base.doStuff();
}
public void setBase(LiskovBase base) { this.base = base }
}
कोड LSP करता है क्योंकि संदर्भ वास्तविक प्रकार को जाने बिना LiskovBase का उपयोग कर सकता है। आपको लगता है कि यह कोड OCP को भी अनुसरण करता है, लेकिन बारीकी से देखें, तो क्या वास्तव में क्लास बंद है ? क्या होगा यदि doStuff
विधि केवल एक लाइन का प्रिंट आउट लेने से अधिक थी?
यदि यह OCP का अनुसरण करता है तो इसका उत्तर केवल: NO है , ऐसा नहीं है क्योंकि इस ऑब्जेक्ट डिज़ाइन में हमें कोड को पूरी तरह से कुछ और के साथ ओवरराइड करना आवश्यक है। यह कट-एंड-पेस्ट वर्म को खोलता है क्योंकि आपको काम करने के लिए बेस क्लास से कोड को कॉपी करना होगा। यह doStuff
सुनिश्चित करने का तरीका विस्तार के लिए खुला है, लेकिन यह संशोधन के लिए पूरी तरह से बंद नहीं था।
हम इस पर टेम्पलेट विधि पैटर्न लागू कर सकते हैं । टेम्पलेट विधि पैटर्न चौखटे में इतना आम है कि आप इसे जाने बिना उपयोग कर रहे होंगे (जैसे जावा स्विंग घटक, सी # फॉर्म और घटक, आदि)। यह है कि doStuff
संशोधन के लिए विधि को बंद करने का एक तरीका और सुनिश्चित करें कि यह जावा के final
कीवर्ड के साथ चिह्नित करके बंद रहता है । वह कीवर्ड किसी को भी क्लास को आगे करने से रोकता है (C # में आप sealed
एक ही काम करने के लिए उपयोग कर सकते हैं )।
public class LiskovBase {
// this is now a template method
// the code that was duplicated
public final void doStuff() {
System.out.println(getStuffString());
}
// extension point, the code that "varies"
// in LiskovBase and it's subclasses
// called by the template method above
// we expect it to be virtual and overridden
public string getStuffString() {
return "My name is Liskov";
}
}
public class LiskovSub extends LiskovBase {
// the extension overridden
// the actual code that varied
public string getStuffString() {
return "I'm sub Liskov!";
}
}
यह उदाहरण OCP का अनुसरण करता है और मूर्खतापूर्ण लगता है, जो कि यह है, लेकिन इस स्केल को संभालने के लिए अधिक कोड के साथ कल्पना करें। मैं उत्पादन में तैनात कोड को देखता रहता हूं, जहां उपवर्ग पूरी तरह से सब कुछ खत्म कर देते हैं और ओवरराइड कोड ज्यादातर कार्यान्वयनों के बीच कट-एन-चिपकाया जाता है। यह काम करता है, लेकिन जैसा कि सभी कोड दोहराव भी रखरखाव बुरे सपने के लिए एक सेट-अप है।
निष्कर्ष
मुझे आशा है कि यह सब OCP और LSP और उनके बीच के अंतर / समानता के बारे में कुछ प्रश्न स्पष्ट करता है। उन्हें समान रूप से खारिज करना आसान है लेकिन ऊपर दिए गए उदाहरणों से पता चलता है कि वे नहीं हैं।
ध्यान दें कि, उपरोक्त नमूना कोड से एकत्रित:
OCP वर्किंग कोड को लॉक करने के बारे में है, लेकिन फिर भी इसे किसी न किसी तरह के एक्सटेंशन पॉइंट्स के साथ खुला रखें।
यह टेम्प्लेट विधि पैटर्न के उदाहरण के साथ बदल जाने वाले कोड को एनकोड करके कोड दोहराव से बचने के लिए है। यह तेजी से विफल होने की भी अनुमति देता है क्योंकि ब्रेकिंग परिवर्तन दर्दनाक होते हैं (यानी एक स्थान को बदल दें, इसे हर जगह तोड़ दें)। रखरखाव के लिए एन्कैप्सुलेटिंग परिवर्तन की अवधारणा एक अच्छी बात है, क्योंकि परिवर्तन हमेशा होते हैं।
एलएसपी उपयोगकर्ता को विभिन्न वस्तुओं को संभालने देने के बारे में है जो सुपरटेप को लागू किए बिना यह जांचते हैं कि वे वास्तविक प्रकार क्या हैं। यह स्वाभाविक रूप से बहुरूपता है।
यह सिद्धांत टाइप-चेकिंग और टाइप-कनवर्ज़न करने का एक विकल्प प्रदान करता है, जो कि प्रकार की संख्या बढ़ने पर हाथ से बाहर निकल सकता है, और विज़िटर जैसे पुल-अप रिफैक्टरिंग या पैटर्न लागू करने के माध्यम से प्राप्त किया जा सकता है।