- गलत abstraction की तुलना में code duplication कहीं सस्ता होता है, और जल्दबाज़ी में की गई commonization लंबे समय की maintenance cost बढ़ाती है
- शुरुआत में उचित लगा extraction भी जब requirements थोड़ा-थोड़ा बदलती हैं, तो parameters और conditionals जुड़ते जाते हैं और मूल intent धुंधला हो जाता है
- जब कोई shared abstraction कई अलग-अलग ideas का बोझ उठाने लगता है, तो code condition-केंद्रित procedure में बदल जाता है, और नई functionality जोड़ने पर वह और आसानी से टूटने लगता है
- मौजूदा code पर लगे प्रयास को बचाए रखने वाली sunk cost fallacy से सावधान रहना चाहिए, और ज़रूरत पड़ने पर abstraction को फिर से call sites पर inline करके सिर्फ वास्तव में ज़रूरी code ही छोड़ना चाहिए
- अगर गलत abstraction सामने आ गई है, तो duplication को फिर से लाकर मौजूदा requirements की समानताओं को नए सिरे से देखना, और उसके बाद दोबारा extraction करना अधिक तेज़ होता है
गलत abstraction बनने की प्रक्रिया
- “duplication is far cheaper than the wrong abstraction” वाक्य RailsConf 2014 प्रस्तुति का हिस्सा था, लेकिन उसके बाद भी लगातार उद्धृत किया जाता रहा
- आम failure path कुछ इस प्रकार है
- developer A को duplication दिखती है
- वह duplication को method या class में extract करके, उसे नाम देकर एक नई abstraction बनाता है
- call sites पर मौजूद दोहराए गए code को नई abstraction call से बदल दिया जाता है
- समय बीतने पर एक नई requirement आती है जो लगभग मिलती-जुलती है, पर पूरी तरह समान नहीं होती
- developer B मौजूदा abstraction को बनाए रखने के लिए parameters जोड़ता है, और values के अनुसार अलग path लेने वाले conditionals डालता है
- इसके बाद हर नई requirement के साथ parameters और conditionals बढ़ते जाते हैं, और code को समझना लगातार कठिन होता जाता है
- एक बार लिखा गया code अक्सर संरक्षित किए जाने योग्य निवेश जैसा दिखने लगता है
- पहले से किए गए प्रयास को व्यर्थ न जाने देने वाली मनोवृत्ति काम करती है
- code जितना जटिल और समझने में कठिन होता है, उतना ही वह महत्वपूर्ण और समय-साध्य लगा हुआ महसूस होता है, इसलिए उसे हटाना कठिन हो जाता है
- यह sunk cost fallacy से जुड़ा है
duplication पर लौटकर फिर से extract करना
- गलत abstraction के ऊपर नई requirements लागू करते रहने से shared code conditionals-केंद्रित हो जाता है, और functionality बढ़ाने के साथ वह और अस्थिर बनता जाता है
- ऐसे समय में तेज़ रास्ता और आगे धकेलना नहीं, बल्कि पीछे लौटना होता है
- abstracted code को फिर से हर call site पर inline करके duplication को वापस लाया जाता है
- हर call site पर दिए जा रहे parameters के आधार पर देखा जाता है कि वास्तव में कौन-सा code execute हो रहा है
- उस call site के लिए अनावश्यक code को हटा दिया जाता है
- inline करने की प्रक्रिया abstraction और conditionals दोनों को हटाती है, और हर call site को सिर्फ अपने आवश्यक code के साथ छोड़ती है
- जो code बाहर से एक ही abstraction को call करता दिखता था, संभव है कि वास्तव में हर call site काफी अद्वितीय code path चला रही थी
- पुरानी abstraction को पूरी तरह हटाने के बाद ही duplication को फिर से देखा जा सकता है, और मौजूदा requirements के अनुरूप नई abstraction extract की जा सकती है
- अगर parameters और conditional paths shared code में लगातार जोड़े जा रहे हैं, तो संभव है कि वह abstraction अब उपयुक्त नहीं रही हो
- शुरुआत में वह सही abstraction रही हो सकती है
- requirements बदलने के साथ उसे उसी रूप में बनाए रखना अब कठिन हो गया हो सकता है
- गलत abstraction में duplication को फिर से लाना पीछे हटना नहीं, बल्कि बेहतर प्रगति बन जाता है
5 टिप्पणियां
मुझे पक्का नहीं है कि यह ऐसा विषय है जिसे द्विआधारी तरीके से समझना ज़रूरी है।
ओह, इससे पूरी तरह सहमत हूँ.
जो चीज़ें व्यवस्थित नहीं हैं, उन्हें व्यवस्थित किया जा सकता है, लेकिन
जो पहले से व्यवस्थित हैं, उन्हें उलटने में कहीं ज़्यादा लागत लगती है.
ponytail ने यह पोस्ट किया है, और तुरंत ऐसा कमेंट भी आ गया haha
हमेशा टकराव ही रहता है।
Hacker News की राय
मेरा मानना है कि single source of truth सिद्धांत का हमेशा पालन होना चाहिए
अगर duplicated code ऐसा है जो अलग होते ही bug बन जाएगा, तो उसे refactor करना चाहिए। नहीं तो long-range coupling बन जाती है, जिसे future developer के लिए bug फूटने से पहले पहचानना मुश्किल होता है
लेकिन अगर यह सिद्धांत नहीं टूट रहा, तो abstraction सिर्फ सुविधा है, और अगर वह असुविधाजनक होने लगे तो वह अपना काम नहीं कर रही, इसलिए उसे इस्तेमाल करने का कारण नहीं है। अगर किसी function को customized behavior के लिए कई flags चाहिए, तो बहुत संभावना है कि वह गलत abstraction है या single responsibility principle का उल्लंघन है
अगर वाकई बहुत customization चाहिए, तो अक्सर function/functor को argument के रूप में लेना बेहतर होता है। उदाहरण के लिए
solve(f:double -> double, max_iters = 99, x_abs_tol = 1e-15, x_rel_tol = 1e-15, ...)की जगहsolve(f:double -> double, stopping_criteria: StoppingCriteriaClass)जैसा बनाया जा सकता हैयह स्पष्ट नहीं होता कि code के दो हिस्से एक ही algorithm इस्तेमाल कर रहे हैं, या उसके थोड़ा अलग versions, और उससे भी ज़्यादा महत्वपूर्ण यह कि क्या वे एक ही कारण से बदलेंगे
शीर्षक की कहावत कहती है कि अलग चीज़ों को ज़बरदस्ती एक जैसा बनाना, एक जैसी चीज़ों को duplicate कर देने और बाद में अलग बनाने से ज़्यादा दर्दनाक है, और मैं इससे सहमत हूँ। दूसरे मामले में वही बदलाव दो बार करना पड़ता है या abstraction लाने वाला refactoring करना पड़ता है, लेकिन पहले मामले में abstraction पर लगातार पैबंद लगाने या उसे वापस हटाने की नौबत आती है
खासकर यह locality को तोड़ देता है, और बदलाव करते समय वास्तव में यही सबसे महत्वपूर्ण गुण है। मैं बस यह बदलाव करना चाहता हूँ और सिस्टम के unrelated हिस्सों में side effects की चिंता नहीं करना चाहता
उदाहरण के तौर पर pyproject.toml / requirements.txt sync रखना वास्तव में कभी-कभी सबसे अच्छा विकल्प होता है, और यह बात और व्यापक रूप से भी लागू हो सकती है। शर्त बस यह है कि हालात पहले ही इतने बिगड़ चुके हों कि single source of truth संभव न रह गया हो; यह इलाज से ज़्यादा नुकसान कम करने जैसा है
मैंने कई बार देखा है कि किसी समय code के दो टुकड़े मिलते-जुलते लगे, इसलिए उन्हें over-abstract कर दिया, लेकिन बाद में वे अलग दिशाओं में चले गए
खासकर junior developers कभी-कभी duplication को हर बुराई की जड़ मान लेते हैं
मैं कभी-कभी इस समस्या के बारे में सोचता हूँ। हाल ही में एक personal project में RTS units के लिए 2D sprites संभालते समय इससे सामना हुआ। unit sprites sprite sheet में एक consistent तरीके से रखे गए थे: 8 दिशाओं के लिए 5 sprites, जिनमें से 3 दिशाएँ mirrored थीं, और क्रम stand, move, attack, die था
इसलिए मैंने एक loader बनाया जो action + direction लेकर चलाने के लिए sprite array देता था
लेकिन फिर directionless explosion sprites आए, 4 दिशाओं और सिर्फ 2 mirrored variants वाले corpse sprites आए, और ऊपर से पहले चार को छोड़कर orc और human ज़्यादातर चीज़ें साझा भी कर रहे थे
मैंने थोड़ी देर सोचा कि इन सबका common abstraction आखिर है क्या, लेकिन अंत में loading code का सिर्फ कुछ हिस्सा अलग किया और UnitLoader, CorpseLoader, EffectLoader बना दिए। ये तीनों loaders थोड़ा-थोड़ा समान चीज़ें संभालते हैं, इसलिए बेहतर abstraction हो सकती है, लेकिन वह बाद में भी मिल सकती है। अभी हर case को संभालने वाला कोई जटिल EverythingLoader बनाने की बजाय बाद में उसी समय duplication हटाना ज़्यादा आसान है
programming में generalization के ज़रिए code को simple बनाने की एक सहज प्रवृत्ति होती है, लेकिन वास्तविक दुनिया गड़बड़ होती है, इसलिए हम अक्सर ज़रूरत से ज़्यादा simplify कर देते हैं। जैसे मूल लेख में कहा गया है, समय बीतने पर जब नई requirements आती हैं, तब पता चलता है कि यह बहुत जल्दी की गई simplification थी
“premature abstraction is the root of much evil” भी एक अच्छी कहावत हो सकती है
उसके ऊपर वाले स्तर पर, यानी sprite sheet layout की व्याख्या और playback mode handling में कई variations हैं, और हो सकता है कि हर मामले पर फिट बैठने वाला कोई common abstraction हो ही नहीं
मुझे अदृश्य abstraction को ज़बरदस्ती गढ़ने या incomplete abstraction में सब कुछ फिट करने की कोशिश से बेहतर आपका मौजूदा तरीका लगता है। जब तक abstraction पूरी तरह साफ़ न हो जाए और उसकी ज़रूरत स्पष्ट न हो जाए, तब तक इंतज़ार करना अच्छी बात है
DRY के उलट एक antidote WET भी है। यानी चीज़ों को दो बार/तीन बार लिखो। इससे भी महत्वपूर्ण बात यह है कि abstraction सिर्फ उन्हीं use cases पर करनी चाहिए जो वास्तव में साबित हो चुके हों, और आम तौर पर पहले duplication के रूप में सामने आए हों। भविष्य के उन use cases के लिए लिखा गया code जो अभी मौजूद ही नहीं हैं, अक्सर हमारे पास मौजूद चीज़ों को abstract करने में बाधा बन जाता है, और जब भी ऐसा होता है, वह थोड़ा मज़ेदार भी लगता है
मुश्किल और उबाऊ काम project के आख़िरी 10% तक पहुँचने पर भी किया जा सकता है
और कभी-कभी duplication से बना “bug” वही मज़ेदार feature बन जाता है जिसे players पसंद करते हैं
जब मैं OOP इस्तेमाल करता था, तब abstractions की वजह से बहुत परेशान होता था, लेकिन लगभग pure functional approach पर जाने के बाद code duplication बहुत कम हो गया
बस एक function बनाओ और उसे दो जगह call कर लो। मुख्य abstraction issue data structures में होता है, लेकिन TypeScript interfaces मूल रूप से duck typing हैं, इसलिए यहाँ भी समस्या बहुत ज़्यादा नहीं होती
इसलिए abstraction की समस्या से पैदा होने वाला code duplication दुर्लभ है। developers के silo हो जाने से पैदा होने वाला duplication कहीं ज़्यादा आम है
आज की ज़्यादातर modern languages functional programming theory के ऊपर आसानी से खड़ी हो सकती हैं, और इसके लिए Haskell जानना ज़रूरी नहीं है। हर व्यक्ति का दिमाग़ अलग तरह से काम करता है, लेकिन मेरे लिए यह विचार बहुत फिट बैठता है कि छोटे, simple, और कभी-कभी flexible parts मिलकर पूरा सिस्टम बनाते हैं
यह बड़े, जटिल, सब कुछ करने वाले shape-transforming machine के ठीक उलट है
जब team का आकार एक सीमा पार कर लेता है और हर व्यक्ति के लिए यह जानना असंभव हो जाता है कि बाकी लोग क्या कर रहे हैं, तब code duplication काफ़ी हद तक अपरिहार्य हो जाता है। सब लोग functional style में भी लिखें, तब भी यही होगा
वास्तव में पिछले महीने मेरी company में ऐसा हुआ। मैंने एक नया pure helper function लिखा और उसे file के शुरुआत में रखा, लेकिन एक हफ़्ते बाद एक सहकर्मी ने बताया कि लगभग वही काम करने वाला, बस अलग signature वाला मिलता-जुलता helper function उसी file के अंत में पहले से मौजूद है
मूल पोस्ट के संदर्भ में, जिसने दोनों का अनुभव किया है वह इससे सहमत होगा। कम-डिज़ाइन किया गया codebase ज़रूरत से ज़्यादा डिज़ाइन किए गए codebase की तुलना में संभालना कहीं आसान होता है
जिन codebases का मुझे maintenance करना पड़ा, उनमें सबसे खराब वे थे जो DRY का पालन करने की कोशिश करते थे। लेकिन उन्होंने उस principle के मूल इरादे को समझने की कोशिश नहीं की थी
उस अराजकता से बाहर निकलने का एकमात्र तरीका था बड़े पैमाने पर code duplication को फिर से लाना
यहाँ मुझे दो talks याद आती हैं: Mike Acton की Data-Oriented Design and C++ [1] और Brian Cantrill की The Complexity of Simplicity [2]
Mike की talk कहती है कि code solution को वास्तविक दुनिया को model करने की ज़रूरत नहीं है, अलग-अलग data अलग-अलग समस्याएँ बनाते हैं, और इसलिए अलग-अलग solutions चाहिए। इस talk को पूरी तरह न्याय देकर समझाना मुश्किल है, लेकिन इसने मुझ पर बहुत असर डाला
Brian की talk abstraction के व्यापक विचार और “सही” abstraction ढूँढना कितना कठिन है, इस पर है
कई साल पहले, जब मुझे स्कूल छोड़े ज़्यादा समय नहीं हुआ था, मैं Rust में एक connection pool implement कर रहा था, और सबसे उचित implementation यह थी कि connection object pool का weak reference रखे ताकि
dropहोने पर वह अपने-आप वापस लौट जाएमेरे manager, जो बहुत अनुभवी थे, इस विचार को इसलिए पसंद नहीं करते थे क्योंकि “लाइब्रेरी किताब को रखती है, किताब लाइब्रेरी को नहीं रखती”। मुझे यह design बदलने लायक पर्याप्त तर्क नहीं लगा, लेकिन वह इस समस्या को उस रूपक के lens के बाहर से देखने को तैयार नहीं थे
आख़िरकार गतिरोध तब टूटा जब एक दूसरे manager ने कहा, “लाइब्रेरी की किताब में लाइब्रेरी शामिल नहीं होती, लेकिन उसके पीछे return location बताने वाली लाइब्रेरी की मुहर होती है।” लगता है उस manager को यह विस्तारित उपमा उचित लगी
अगर उस समय मैं ज़्यादा अनुभवी होता, तो शायद बिना मूल बात छोड़े उसी रूपक के भीतर बात करने का तरीका खोज लेता, लेकिन आज भी मुझे यह पूरी तरह विचित्र लगता है कि code abstraction और library use experience के परिणामों को तौलने के बजाय उस रूपक पर ही standard frame के रूप में अड़े रहे
कोई सुनना ही नहीं चाहता। सच में, कोई भी नहीं सुनता। 90% कंपनियों में ऐसे तथाकथित senior developers होते हैं जो नई abstraction बनाते समय रोमांचित हो जाते हैं
Overengineering, abstraction, और premature optimization engineering की तीन बड़ी आपदाएँ हैं
साथ ही, यह भी अच्छा है कि इनके कारण हमेशा काम मिलता रहेगा
इसी तरह, कुछ developers को लगता है कि inline strings या numeric constants पूरी तरह बुराई हैं। मैंने एक PR में यह देखा था
HTTPS_SCHEME = 'https'DOMAIN = 'www.example.com'url = HTTPS_SCHEME + '://' + DOMAIN“constants को hardcode मत करो” जैसी बात को cargo cult की तरह मानने के अलावा, मुझे समझ नहीं आता कि इससे क्या हासिल होता है। ऊपर से constant definitions फ़ाइल के शीर्ष पर थीं और URL बनाने वाला code सैकड़ों lines दूर था
regex को भी फ़ाइल के शीर्ष पर रखने की ज़रूरत नहीं, जहाँ इस्तेमाल हो वहीं रखो। language इतनी समझदार होती है कि शायद वह पहचान लेगी कि यह constant है
अगर function बहुत छोटा है, तो बस lambda इस्तेमाल कर लो। मैं नहीं चाहता कि एक-दो बार इस्तेमाल होने वाले one-line functions कहीं बहुत दूर बनाए जाएँ
अगर test या staging में https की जगह http करना पड़े, तो scheme और domain को अलग करना और constants को ऊपर या अलग फ़ाइल में रखना उचित है। यह भी मायने रखता है कि
urlकई जगह बन रहा है या सिर्फ़ एक ही जगहफ़ाइल के शीर्ष पर named constants रखना बहुत आम style है, और कभी-कभी team coding standard का हिस्सा भी होता है
इसके और भी कारण हो सकते हैं, इसलिए Chesterton’s Fence याद रखना अच्छा है। किसी चीज़ को सीधे cargo cult कहना अच्छा विचार नहीं है। कोई और यही बात inline literals के बारे में भी कह सकता है। अगर कुछ अजीब लगे, तो पूछ लो; हो सकता है उसके पीछे अच्छा कारण हो, या किसी ने ध्यान ही न दिया हो और refactor करके constants को inline करना सबको ठीक लगे
अगर उसे constant में निकाल दो, तो फिर project दर project खोलकर find usages करना पड़ता है
microservices इस्तेमाल करो तो दोनों किए जा सकते हैं
अगर आप एक service के maintainer हैं, तो दूसरी service के code की चिंता करने का कोई कारण नहीं है। वह किसी दूसरी team का code है, तो आपको उससे क्या लेना-देना? कई बार तो यह जानने की भी ज़रूरत नहीं कि वह team मौजूद है। बड़े systems में सभी applications के अस्तित्व के बारे में जानना व्यावहारिक भी नहीं होता
सिर्फ़ $19.95 में हम एक single point of failure को कई single points of failure में बदल देंगे!
service-oriented architecture रखो, लेकिन बस monolith deploy करो तो बेहतर है। testing आसान होती है और serialization/deserialization की अतिरिक्त layer से भी बचा जा सकता है
ज़्यादातर senior लोग जानते हैं कि DRY का अंधाधुंध पालन नहीं करना चाहिए। फिर भी हममें से कई लोगों को duplicate code source कई जगह बनाए रखने का विचार असहज करता है
इसे समझने के लिए उस सरल मॉडल को ध्यान से देखना चाहिए जिसमें दो callers किसी common code पर निर्भर होते हैं। अगर common code को सिर्फ एक caller की ज़रूरत के कारण बदलना पड़े, तो वह code वास्तव में common नहीं है
DRY का गलत लक्ष्य ऐसी चीज़ को हल करना है जिसे encapsulation से सुलझाने की कोशिश की जाती है। Encapsulation refactoring का काम callers से common code की तरफ़ ले जाता है। लेकिन common code को अपडेट करने का असर callers की तुलना में कहीं बड़ा होता है, इसलिए यह वांछित दिशा नहीं है
Encapsulation से बचते हुए भी DRY का पालन किया जा सकता है। कई पतली abstractions रखना बेहतर है जिनके बारे में callers को पता होना चाहिए। OOP में इसके लिए SRP और IoC सीखे जाते हैं, और procedural programming में यह स्वाभाविक रूप से helper functions की एक शृंखला को कॉल करने के रूप में दिखाई देता है