Ruby on Rails पैटर्न और एंटी-पैटर्न श्रृंखला की चौथी किस्त में आपका स्वागत है।
पहले, हमने सामान्य रूप से पैटर्न और एंटी-पैटर्न को कवर किया साथ ही Rails मॉडल और व्यू के संबंध में। इस पोस्ट में, हम MVC (मॉडल-व्यू-कंट्रोलर) डिज़ाइन पैटर्न के अंतिम भाग — कंट्रोलर का विश्लेषण करने जा रहे हैं। आइए गोता लगाएं और Rails कंट्रोलर से संबंधित पैटर्न और एंटी-पैटर्न के माध्यम से जाएं।
फ्रंट लाइन्स पर
चूंकि Ruby on Rails एक वेब फ्रेमवर्क है, HTTP अनुरोध इसका एक महत्वपूर्ण हिस्सा हैं। सभी प्रकार के क्लाइंट अनुरोधों के माध्यम से Rails बैकएंड तक पहुंचते हैं और यहीं कंट्रोलर चमकते हैं। कंट्रोलर अनुरोधों को प्राप्त करने और संभालने की अग्रिम पंक्ति में हैं। यह उन्हें Ruby on Rails फ्रेमवर्क का एक मौलिक हिस्सा बनाता है। बेशक, कंट्रोलर से पहले कोड आता है, लेकिन कंट्रोलर कोड कुछ ऐसा है जिसे हम में से अधिकांश नियंत्रित कर सकते हैं।
एक बार जब आप config/routes.rb पर रूट्स को परिभाषित करते हैं, तो आप सेट रूट पर सर्वर को हिट कर सकते हैं, और संबंधित कंट्रोलर बाकी का ख्याल रखेगा। पिछला वाक्य पढ़ने से यह आभास हो सकता है कि सब कुछ उतना ही सरल है। लेकिन, अक्सर, बहुत सारा भार कंट्रोलर के कंधों पर पड़ता है। प्रमाणीकरण और प्राधिकरण की चिंता है, फिर सही डेटा कैसे प्राप्त करें, साथ ही व्यावसायिक तर्क कहां और कैसे करें, इसकी समस्याएं हैं।
इन सभी चिंताओं और जिम्मेदारियों जो कंट्रोलर के अंदर हो सकती हैं, कुछ एंटी-पैटर्न का कारण बन सकती हैं। सबसे 'प्रसिद्ध' में से एक "मोटे" कंट्रोलर का एंटी-पैटर्न है।
मोटे (मोटापे से ग्रस्त) कंट्रोलर
कंट्रोलर में बहुत अधिक तर्क डालने की समस्या यह है कि आप एकल उत्तरदायित्व सिद्धांत (SRP) का उल्लंघन करना शुरू कर रहे हैं। इसका मतलब है कि हम कंट्रोलर के अंदर बहुत अधिक काम कर रहे हैं। अक्सर, इससे कंट्रोलर फ़ाइलों में बहुत सारा कोड और जिम्मेदारियां जमा हो जाती हैं। यहां, 'मोटा' कंट्रोलर फ़ाइलों में निहित व्यापक कोड के साथ-साथ कंट्रोलर द्वारा समर्थित तर्क को संदर्भित करता है। इसे अक्सर एंटी-पैटर्न माना जाता है।
कंट्रोलर को क्या करना चाहिए, इस पर बहुत सारी राय हैं। कंट्रोलर की जिम्मेदारियों का एक सामान्य आधार निम्नलिखित शामिल हैं:
- प्रमाणीकरण और प्राधिकरण — यह जांचना कि अनुरोध के पीछे की इकाई (अक्सर, एक उपयोगकर्ता) वह है जो वह कहता है कि वह है और क्या उसे संसाधन तक पहुंचने या कार्रवाई करने की अनुमति है। अक्सर, प्रमाणीकरण सत्र या कुकी में सहेजा जाता है, लेकिन कंट्रोलर को अभी भी जांचना चाहिए कि प्रमाणीकरण डेटा अभी भी मान्य है या नहीं।
- डेटा लाना — इसे अनुरोध के साथ आए पैरामीटर्स के आधार पर सही डेटा खोजने के लिए तर्क को कॉल करना चाहिए। एक आदर्श दुनिया में, यह एक मेथड को कॉल करना चाहिए जो सारा काम करता है। कंट्रोलर को भारी काम नहीं करना चाहिए, उसे इसे आगे सौंपना चाहिए।
- टेम्पलेट रेंडरिंग — अंत में, इसे उचित प्रारूप (HTML, JSON, आदि) के साथ परिणाम प्रस्तुत करके सही प्रतिक्रिया वापस करनी चाहिए। या, इसे किसी अन्य पथ या URL पर रीडायरेक्ट करना चाहिए।
इन विचारों का पालन करने से आप कंट्रोलर एक्शन और कंट्रोलर में सामान्य रूप से बहुत कुछ होने से बच सकते हैं। कंट्रोलर स्तर पर इसे सरल रखने से आप अपने एप्लिकेशन के अन्य क्षेत्रों में काम सौंप सकते हैं। जिम्मेदारियों को सौंपना और उन्हें एक-एक करके टेस्ट करना यह सुनिश्चित करेगा कि आप अपने ऐप को मजबूत विकसित कर रहे हैं।
ज़रूर, आप उपरोक्त सिद्धांतों का पालन कर सकते हैं, लेकिन आप कुछ उदाहरणों के लिए उत्सुक होंगे। आइए गोता लगाएं और देखें कि हम कंट्रोलर के कुछ वजन को कम करने के लिए किन पैटर्न का उपयोग कर सकते हैं।
क्वेरी ऑब्जेक्ट्स
कंट्रोलर एक्शन के अंदर होने वाली समस्याओं में से एक है डेटा की बहुत अधिक क्वेरी करना। यदि आपने Rails मॉडल एंटी-पैटर्न और पैटर्न पर हमारे ब्लॉग पोस्ट का पालन किया, तो हम एक समान समस्या से गुजरे जहां मॉडल में बहुत अधिक क्वेरी तर्क था। लेकिन, इस बार हम क्वेरी ऑब्जेक्ट नामक एक पैटर्न का उपयोग करेंगे। एक क्वेरी ऑब्जेक्ट एक तकनीक है जो आपकी जटिल क्वेरी को एक ऑब्जेक्ट में अलग करती है।
अधिकांश मामलों में, क्वेरी ऑब्जेक्ट एक प्लेन ओल्ड Ruby ऑब्जेक्ट है जिसे ActiveRecord रिलेशन के साथ इनिशियलाइज़ किया गया है। एक विशिष्ट क्वेरी ऑब्जेक्ट ऐसा दिख सकता है:
# app/queries/all_songs_query.rb class AllSongsQuery def initialize(songs = Song.all) @songs = songs end def call(params, songs = Song.all) songs.where(published: true) .where(artist_id: params[:artist_id]) .order(:title) end end
इसे कंट्रोलर के अंदर ऐसे उपयोग करने के लिए बनाया गया है:
class SongsController < ApplicationController def index @songs = AllSongsQuery.new.call(all_songs_params) end private def all_songs_params params.slice(:artist_id) end end
आप क्वेरी ऑब्जेक्ट का एक और दृष्टिकोण भी आजमा सकते हैं:
# app/queries/all_songs_query.rb class AllSongsQuery attr_reader :songs def initialize(songs = Song.all) @songs = songs end def call(params = {}) scope = published(songs) scope = by_artist_id(scope, params[:artist_id]) scope = order_by_title(scope) end private def published(scope) scope.where(published: true) end def by_artist_id(scope, artist_id) artist_id ? scope.where(artist_id: artist_id) : scope end def order_by_title(scope) scope.order(:title) end end
बाद वाला दृष्टिकोण params को वैकल्पिक बनाकर क्वेरी ऑब्जेक्ट को अधिक मजबूत बनाता है। साथ ही, ध्यान दें कि हम अब AllSongsQuery.new.call कॉल कर सकते हैं। यदि आप इसके बड़े प्रशंसक नहीं हैं, तो आप क्लास मेथड का सहारा ले सकते हैं। यदि आप अपनी क्वेरी क्लास को क्लास मेथड के साथ लिखते हैं, तो यह अब 'ऑब्जेक्ट' नहीं होगा, लेकिन यह व्यक्तिगत पसंद का मामला है। उदाहरण के लिए, आइए देखें कि हम AllSongsQuery को जंगल में कॉल करने के लिए कैसे सरल बना सकते हैं।
# app/queries/all_songs_query.rb class AllSongsQuery class << self def call(params = {}, songs = Song.all) scope = published(songs) scope = by_artist_id(scope, params[:artist_id]) scope = order_by_title(scope) end private def published(scope) scope.where(published: true) end def by_artist_id(scope, artist_id) artist_id ? scope.where(artist_id: artist_id) : scope end def order_by_title(scope) scope.order(:title) end end end
अब, हम AllSongsQuery.call कॉल कर सकते हैं और हमारा काम हो गया। हम artist_id के साथ params पास कर सकते हैं। साथ ही, हम किसी कारण से इसे बदलने की आवश्यकता होने पर प्रारंभिक स्कोप पास कर सकते हैं। यदि आप वास्तव में क्वेरी क्लास पर new कॉल करने से बचना चाहते हैं, तो इस 'ट्रिक' को आजमाएं:
# app/queries/application_query.rb class ApplicationQuery def self.call(*params) new(*params).call end end
आप ApplicationQuery बना सकते हैं और फिर अन्य क्वेरी क्लास में इससे इनहेरिट कर सकते हैं:
# app/queries/all_songs_query.rb class AllSongsQuery < ApplicationQuery ... end
आपने अभी भी AllSongsQuery.call रखा, लेकिन आपने इसे अधिक सुंदर बना दिया।
क्वेरी ऑब्जेक्ट्स के बारे में अच्छी बात यह है कि आप उन्हें अलगाव में टेस्ट कर सकते हैं और यह सुनिश्चित कर सकते हैं कि वे वही कर रहे हैं जो उन्हें करना चाहिए। इसके अलावा, आप इन क्वेरी क्लास को बढ़ा सकते हैं और कंट्रोलर में तर्क के बारे में ज्यादा चिंता किए बिना उन्हें टेस्ट कर सकते हैं। एक बात ध्यान देने योग्य है कि आपको अपने अनुरोध पैरामीटर्स को कहीं और संभालना चाहिए, और क्वेरी ऑब्जेक्ट पर भरोसा नहीं करना चाहिए। आप क्या सोचते हैं, क्या आप क्वेरी ऑब्जेक्ट को आजमाने जा रहे हैं?
सेवा के लिए तैयार
ठीक है, तो हमने डेटा एकत्र करने और लाने के तरीकों को क्वेरी ऑब्जेक्ट्स में सौंपने के तरीकों को संभाला है। हम डेटा एकत्र करने और उस चरण के बीच जमा हुए तर्क के साथ क्या करते हैं जहां हम इसे प्रस्तुत करते हैं? अच्छा है कि आपने पूछा, क्योंकि समाधानों में से एक है जिसे सेवाएं कहा जाता है। एक सेवा को अक्सर एक PORO (प्लेन ओल्ड Ruby ऑब्जेक्ट) माना जाता है जो एकल (व्यावसायिक) कार्रवाई करता है। हम आगे बढ़ेंगे और इस विचार का थोड़ा पता लगाएंगे।
कल्पना करें कि हमारे पास दो सेवाएं हैं। एक रसीद बनाता है, दूसरा उपयोगकर्ता को रसीद भेजता है जैसे:
# app/services/create_receipt_service.rb class CreateReceiptService def self.call(total, user_id) Receipt.create!(total: total, user_id: user_id) end end # app/services/send_receipt_service.rb class SendReceiptService def self.call(receipt) UserMailer.send_receipt(receipt).deliver_later end end
तब, हमारे कंट्रोलर में हम SendReceiptService को इस तरह कॉल करेंगे:
# app/controllers/receipts_controller.rb class ReceiptsController < ApplicationController def create receipt = CreateReceiptService.call(total: receipt_params[:total], user_id: receipt_params[:user_id]) SendReceiptService.call(receipt) end end
अब आपके पास दो सेवाएं सारा काम कर रही हैं, और कंट्रोलर सिर्फ उन्हें कॉल करता है। आप इन्हें अलग-अलग टेस्ट कर सकते हैं, लेकिन समस्या यह है कि सेवाओं के बीच कोई स्पष्ट संबंध नहीं है। हां, सिद्धांत रूप में, उनमें से प्रत्येक एकल व्यावसायिक क्रिया करता है। लेकिन, यदि हम हितधारकों के परिप्रेक्ष्य से अमूर्त स्तर पर विचार करते हैं — रसीद बनाने की उनकी कार्रवाई में उसका ईमेल भेजना शामिल है। अमूर्तता का कौन सा स्तर 'सही'™️ है?
इस विचार प्रयोग को थोड़ा और जटिल बनाने के लिए, आइए एक आवश्यकता जोड़ें कि रसीद पर कुल राशि की गणना की जानी चाहिए या रसीद के निर्माण के दौरान कहीं से प्राप्त की जानी चाहिए। हम तब क्या करते हैं? कुल राशि के योग को संभालने के लिए एक और सेवा लिखें? उत्तर एकल उत्तरदायित्व सिद्धांत (SRP) का पालन करना और चीजों को एक दूसरे से अलग करना हो सकता है।
# app/services/create_receipt_service.rb class CreateReceiptService ... end # app/services/send_receipt_service.rb class SendReceiptService ... end # app/services/calculate_receipt_total_service.rb class CalculateReceiptTotalService ... end # app/controllers/receipts_controller.rb class ReceiptsController < ApplicationController def create total = CalculateReceiptTotalService.call(user_id: receipts_controller[:user_id]) receipt = CreateReceiptService.call(total: total, user_id: receipt_params[:user_id]) SendReceiptService.call(receipt) end end
SRP का पालन करके, हम सुनिश्चित करते हैं कि हमारी सेवाओं को बड़े अमूर्तताओं में संयोजित किया जा सकता है, जैसे ReceiptCreation प्रक्रिया। इस 'प्रक्रिया' क्लास को बनाकर, हम उस प्रक्रिया को पूरा करने के लिए आवश्यक सभी कार्यों को समूहित कर सकते हैं। आप इस विचार के बारे में क्या सोचते हैं? यह पहली बार में बहुत अधिक अमूर्तता की तरह लग सकता है, लेकिन अगर आप इन कार्रवाइयों को हर जगह कॉल कर रहे हैं तो यह फायदेमंद साबित हो सकता है। यदि यह अच्छा लगता है, तो Trailblazer के ऑपरेशन को देखें।
संक्षेप में, नई CalculateReceiptTotalService सेवा सभी नंबर क्रंचिंग से निपट सकती है। हमारा CreateReceiptService डेटाबेस में रसीद लिखने के लिए जिम्मेदार है। SendReceiptService उपयोगकर्ताओं को उनकी रसीदों के बारे में ईमेल भेजने के लिए है। ये छोटी और केंद्रित क्लास होने से उन्हें अन्य उपयोग के मामलों में संयोजित करना आसान हो जाता है, जिससे कोडबेस को बनाए रखना और टेस्ट करना आसान हो जाता है।
सेवा की पृष्ठभूमि
Ruby दुनिया में, सर्विस क्लास का उपयोग करने का दृष्टिकोण एक्शन, ऑपरेशन और इसी तरह के नाम से भी जाना जाता है। ये सभी कमांड पैटर्न पर उबलते हैं। कमांड पैटर्न के पीछे का विचार यह है कि एक ऑब्जेक्ट (या हमारे उदाहरण में, एक क्लास) एक व्यावसायिक कार्रवाई करने या घटना को ट्रिगर करने के लिए आवश्यक सभी जानकारी को एनकैप्सुलेट करता है। कमांड के कॉलर को पता होना चाहिए:
- कमांड का नाम
- कमांड ऑब्जेक्ट/क्लास पर कॉल करने के लिए मेथड नाम
- मेथड पैरामीटर्स के लिए पास किए जाने वाले मान
तो, हमारे मामले में, कमांड का कॉलर एक कंट्रोलर है। दृष्टिकोण बहुत समान है, बस Ruby में नामकरण 'सर्विस' है।
काम को विभाजित करें
यदि आपके कंट्रोलर कुछ तृतीय-पक्ष सेवाओं को कॉल कर रहे हैं और वे आपके रेंडरिंग को ब्लॉक कर रहे हैं, तो शायद इन कॉल्स को निकालने और उन्हें दूसरे कंट्रोलर एक्शन के साथ अलग से रेंडर करने का समय आ गया है। इसका एक उदाहरण तब हो सकता है जब आप किसी किताब की जानकारी रेंडर करने का प्रयास करते हैं और उसकी रेटिंग किसी अन्य सेवा से प्राप्त करते हैं जिसे आप वास्तव में प्रभावित नहीं कर सकते (जैसे Goodreads)।
# app/controllers/books_controller.rb class BooksController < ApplicationController def show @book = Book.find(params[:id]) @rating = GoodreadsRatingService.new(book).call end end
यदि Goodreads डाउन है या ऐसा कुछ, तो आपके उपयोगकर्ताओं को Goodreads सर्वर के अनुरोध के टाइमआउट होने तक इंतजार करना होगा। या, यदि उनके सर्वर पर कुछ धीमा है, तो पेज धीरे-धीरे लोड होगा। आप तृतीय-पक्ष सेवा को कॉल करने को दूसरे एक्शन में निकाल सकते हैं जैसे:
# app/controllers/books_controller.rb class BooksController < ApplicationController ... def show @book = Book.find(params[:id]) end def rating @rating = GoodreadsRatingService.new(@book).call render partial: 'book_rating' end ... end
तब, आपको अपने व्यू से rating पथ को कॉल करना होगा, लेकिन हे, आपके शो एक्शन में अब कोई ब्लॉकर नहीं है। साथ ही, आपको 'book_rating' पार्शियल की आवश्यकता है। इसे अधिक आसानी से करने के लिए, आप render_async जेम का उपयोग कर सकते हैं। आपको बस निम्नलिखित कथन डालना होगा जहां आप अपनी किताब की रेटिंग रेंडर करते हैं:
<%= render_async book_rating_path %>
रेटिंग रेंडर करने के लिए HTML को book_rating पार्शियल में निकालें, और डालें:
<%= content_for :render_async %>
अपने लेआउट फ़ाइल के अंदर, जेम पेज लोड होने के बाद book_rating_path को AJAX अनुरोध के साथ कॉल करेगा, और जब रेटिंग प्राप्त हो जाएगी, तो यह इसे पेज पर दिखाएगा। इसमें एक बड़ा लाभ यह है कि आपके उपयोगकर्ता रेटिंग को अलग से लोड करके किताब पेज को तेजी से देख पाते हैं।
या, यदि आप चाहें, तो आप Basecamp से Turbo Frames का उपयोग कर सकते हैं। विचार वही है, लेकिन आप बस अपने मार्कअप में <turbo-frame> एलिमेंट का उपयोग करते हैं जैसे:
<turbo-frame id="rating_1" src="/books/1/rating"> </turbo-frame>
आप जो भी विकल्प चुनते हैं, विचार यह है कि भारी या अस्थिर काम को अपने मुख्य कंट्रोलर एक्शन से विभाजित करें और उपयोगकर्ता को पेज जितनी जल्दी हो सके दिखाएं।
अंतिम विचार
यदि आप कंट्रोलर को पतला रखने के विचार को पसंद करते हैं और उन्हें अन्य मेथड के केवल 'कॉलर' के रूप में चित्रित करते हैं, तो मेरा मानना है कि यह पोस्ट इस बारे में कुछ अंतर्दृष्टि लाया कि उन्हें इस तरह कैसे रखा जाए। यहां उल्लिखित कुछ पैटर्न और एंटी-पैटर्न, निश्चित रूप से, एक संपूर्ण सूची नहीं हैं। यदि आपके पास इस बारे में कोई विचार है कि क्या बेहतर है या आप क्या पसंद करते हैं, तो कृपया ट्विटर पर संपर्क करें और हम चर्चा कर सकते हैं।
निश्चित रूप से इस श्रृंखला पर बने रहें, हम कम से कम एक और ब्लॉग पोस्ट करने जा रहे हैं जहां हम सामान्य Rails समस्याओं और श्रृंखला से निष्कर्षों को सारांशित करते हैं।
अगली बार तक, चीयर्स!
P.S. यदि आप Ruby Magic पोस्ट को प्रेस से निकलते ही पढ़ना चाहते हैं, तो हमारे Ruby Magic न्यूज़लेटर की सदस्यता लें और कभी भी एक भी पोस्ट मिस न करें!