रूबी ऑन रेल्स पैटर्न और एंटी-पैटर्न श्रृंखला की चौथी किस्त में आपका स्वागत है।
पहले, हमने सामान्य रूप से पैटर्न और एंटी-पैटर्न के साथ-साथ रेल मॉडल और दृश्यों के संबंध को कवर किया था। इस पोस्ट में, हम एमवीसी (मॉडल-व्यू-कंट्रोलर) डिज़ाइन पैटर्न के अंतिम भाग - कंट्रोलर का विश्लेषण करने जा रहे हैं। आइए रेल नियंत्रकों से संबंधित पैटर्न और विरोधी पैटर्न में गोता लगाएँ और देखें।
फ्रंट लाइन पर
चूंकि रूबी ऑन रेल्स एक वेब ढांचा है, इसलिए HTTP अनुरोध इसका एक महत्वपूर्ण हिस्सा हैं। सभी प्रकार के ग्राहक अनुरोधों के माध्यम से रेल बैकएंड तक पहुंचते हैं और यहीं पर नियंत्रक चमकते हैं। अनुरोध प्राप्त करने और संभालने के लिए नियंत्रक सबसे आगे हैं। यह उन्हें रूबी ऑन रेल्स फ्रेमवर्क का एक मूलभूत हिस्सा बनाता है। बेशक, एक कोड है जो नियंत्रकों से पहले आता है, लेकिन नियंत्रक कोड कुछ ऐसा है जिसे हम में से अधिकांश नियंत्रित कर सकते हैं।
एक बार जब आप config/routes.rb
. पर मार्ग निर्धारित कर लेते हैं , आप इनसेट रूट पर सर्वर को हिट कर सकते हैं, और संबंधित कंट्रोलर बाकी का ध्यान रखेगा। पिछले वाक्य को पढ़ने से यह आभास हो सकता है कि सब कुछ उतना ही सरल है। लेकिन, अक्सर, बहुत सारा भार नियंत्रक के कंधों पर पड़ता है। प्रमाणीकरण और प्राधिकरण की चिंता है, फिर आवश्यक डेटा कैसे प्राप्त किया जाए, साथ ही साथ व्यावसायिक तर्क को कहां और कैसे निष्पादित किया जाए, इसमें समस्याएं हैं।
ये सभी चिंताएं और जिम्मेदारियां जो नियंत्रक के अंदर हो सकती हैं, कुछ विरोधी पैटर्न का कारण बन सकती हैं। सबसे 'प्रसिद्ध' में से एक "वसा" नियंत्रक का विरोधी पैटर्न है।
वसा (मोटापे से ग्रस्त) नियंत्रक
नियंत्रक में बहुत अधिक तर्क डालने में समस्या यह है कि आप एकल उत्तरदायित्व सिद्धांत (एसआरपी) का उल्लंघन करना शुरू कर रहे हैं। इसका मतलब है कि हम कंट्रोलर के अंदर बहुत ज्यादा काम कर रहे हैं। अक्सर, इससे बहुत सारे कोड और जिम्मेदारियाँ वहाँ जमा हो जाती हैं। यहां, 'वसा' नियंत्रक फ़ाइलों में निहित व्यापक कोड के साथ-साथ नियंत्रक द्वारा समर्थित तर्क को संदर्भित करता है। इसे अक्सर एक विरोधी पैटर्न माना जाता है।
नियंत्रक को क्या करना चाहिए, इस पर बहुत सारी राय है। एक नियंत्रक की जिम्मेदारियों के एक सामान्य आधार में निम्नलिखित शामिल होने चाहिए:
- प्रमाणीकरण और प्राधिकरण - जांच कर रहा है कि अनुरोध के पीछे इकाई (अक्सर, एक उपयोगकर्ता) वह कौन है जो यह कहता है और क्या उसे संसाधन तक पहुंचने या कार्रवाई करने की अनुमति है। अक्सर, प्रमाणीकरण सत्र या कुकी में सहेजा जाता है, लेकिन नियंत्रक को अभी भी जांचना चाहिए कि प्रमाणीकरण डेटा अभी भी मान्य है या नहीं।
- डेटा प्राप्त करना - इसे अनुरोध के साथ आए मापदंडों के आधार पर सही डेटा खोजने के लिए तर्क को कॉल करना चाहिए। परिपूर्ण दुनिया में, यह एक ऐसी विधि का आह्वान करना चाहिए जो सभी काम करती है। नियंत्रक को भारी काम नहीं करना चाहिए, उसे इसे और अधिक सौंपना चाहिए।
- टेम्पलेट प्रतिपादन - अंत में, इसे उचित प्रारूप (एचटीएमएल, जेएसओएन, आदि) के साथ परिणाम प्रस्तुत करके सही प्रतिक्रिया देनी चाहिए। या, इसे किसी अन्य पथ या URL पर रीडायरेक्ट करना चाहिए।
इन विचारों का पालन करने से आप सामान्य रूप से नियंत्रक क्रियाओं और नियंत्रक के अंदर बहुत अधिक होने से बचा सकते हैं। नियंत्रक स्तर पर इसे सरल रखने से आप अपने आवेदन के अन्य क्षेत्रों में कार्य सौंप सकते हैं। जिम्मेदारियों को सौंपना और एक-एक करके उनका परीक्षण करना सुनिश्चित करेगा कि आप अपने ऐप को मजबूत बनाने के लिए विकसित कर रहे हैं।
ज़रूर, आप उपरोक्त सिद्धांतों का पालन कर सकते हैं, लेकिन आपको कुछ उदाहरणों के लिए उत्सुक होना चाहिए। आइए देखें और देखें कि कुछ वजन के नियंत्रकों को राहत देने के लिए हम किन पैटर्नों का उपयोग कर सकते हैं।
क्वेरी ऑब्जेक्ट
नियंत्रक कार्यों के अंदर होने वाली समस्याओं में से एक डेटा की बहुत अधिक पूछताछ है। यदि आपने रेल मॉडल विरोधी पैटर्न और पैटर्न पर हमारे ब्लॉग पोस्ट का अनुसरण किया है, तो हम एक ऐसी ही समस्या से गुज़रे हैं जहाँ मॉडल में बहुत अधिक क्वेरी करने वाले तर्क थे। लेकिन, इस बार हम क्वेरी ऑब्जेक्ट नामक एक पैटर्न का उपयोग करेंगे। एक क्वेरी ऑब्जेक्ट एक ऐसी तकनीक है जो आपके जटिल प्रश्नों को एक ही ऑब्जेक्ट में अलग करती है।
ज्यादातर मामलों में, क्वेरी ऑब्जेक्ट एक सादा पुराना रूबी ऑब्जेक्ट होता है जिसे 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
. पर कॉल कर सकते हैं और हम कर रहे हैं। हम params
. में पास कर सकते हैं artist_id
. के साथ . इसके अलावा, अगर हमें किसी कारण से इसे बदलने की आवश्यकता है तो हम प्रारंभिक दायरे को पारित कर सकते हैं। अगर आप वाकई 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
रखा है , लेकिन आपने इसे और अधिक सुंदर बना दिया है।
क्वेरी ऑब्जेक्ट्स के बारे में सबसे अच्छी बात यह है कि आप उन्हें अलग-थलग कर सकते हैं और सुनिश्चित कर सकते हैं कि वे वही कर रहे हैं जो उन्हें करना चाहिए। इसके अलावा, आप इन क्वेरी क्लासेस का विस्तार कर सकते हैं और नियंत्रक में तर्क के बारे में बहुत अधिक चिंता किए बिना उनका परीक्षण कर सकते हैं। ध्यान देने वाली एक बात यह है कि आपको अपने अनुरोध पैरामीटर को कहीं और संभालना चाहिए, और ऐसा करने के लिए क्वेरी ऑब्जेक्ट पर भरोसा नहीं करना चाहिए। आपको क्या लगता है, क्या आप क्वेरी ऑब्जेक्ट को आज़माने जा रहे हैं?
सेवा के लिए तैयार
ठीक है, इसलिए हमने QueryObjects में डेटा एकत्र करने और लाने के तरीके को संभाला है। डेटा एकत्र करने और उस चरण में जहां हम इसे प्रस्तुत करते हैं, के बीच ढेर किए गए तर्क के साथ हम क्या करते हैं? अच्छा है कि आपने पूछा, क्योंकि समाधानों में से एक का उपयोग करना है जिसे सेवाएँ कहा जाता है। एक सेवा को अक्सर पोरो (सादा पुरानी रूबी ऑब्जेक्ट) के रूप में माना जाता है जो एक एकल (व्यवसाय) क्रिया करता है। हम आगे बढ़ेंगे और इस विचार को थोड़ा नीचे देखेंगे।
कल्पना कीजिए कि हमारे पास दो सेवाएं हैं। एक रसीद बनाता है, दूसरा उपयोगकर्ता को एक रसीद भेजता है जैसे:
# 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
अब आपके पास दो सेवाएं हैं जो सभी काम कर रही हैं, और नियंत्रक उन्हें कॉल करता है। आप इनका अलग से परीक्षण कर सकते हैं, लेकिन समस्या यह है कि सेवाओं के बीच कोई स्पष्ट संबंध नहीं है। हां, सिद्धांत रूप में, ये सभी एकल व्यावसायिक क्रिया करते हैं। लेकिन, अगर हम हितधारकों के दृष्टिकोण से अमूर्त स्तर पर विचार करते हैं - रसीद बनाने की कार्रवाई के बारे में उनके विचार में इसका एक ईमेल भेजना शामिल है। अमूर्तता का स्तर किसका 'दाएं'™️ है?
इस विचार प्रयोग को थोड़ा और जटिल बनाने के लिए, आइए एक आवश्यकता जोड़ें कि रसीद पर कुल राशि की गणना या रसीद के निर्माण के दौरान कहीं से प्राप्त की जानी चाहिए। तो फिर हम क्या करें? कुल योग के योग को संभालने के लिए एक और सेवा लिखें? इसका उत्तर सिंगल रिस्पॉन्सिबिलिटी प्रिंसिपल (एसआरपी) और अमूर्त चीजों का एक दूसरे से दूर पालन करना हो सकता है।
# 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
एसआरपी का पालन करके, हम सुनिश्चित करते हैं कि हमारी सेवाओं को एक साथ बड़े एब्स्ट्रैक्शन में बनाया जा सकता है, जैसे ReceiptCreation
प्रक्रिया। इस 'प्रक्रिया' वर्ग को बनाकर, हम प्रक्रिया को पूरा करने के लिए आवश्यक सभी क्रियाओं को समूहीकृत कर सकते हैं। आप इस आइडिया के बारे में क्या सोचते हैं? यह पहली बार में बहुत अधिक अमूर्तता की तरह लग सकता है, लेकिन यह फायदेमंद साबित हो सकता है यदि आप इन कार्यों को हर जगह बुला रहे हैं। अगर यह आपको अच्छा लगता है, तो ट्रेलब्लेज़र का ऑपरेशन देखें।
संक्षेप में, नया CalculateReceiptTotalService
सेवा सभी नंबर क्रंचिंग से निपट सकती है। हमारी CreateReceiptService
डेटाबेस को रसीद लिखने के लिए जिम्मेदार है। SendReceiptService
उपयोगकर्ताओं को उनकी रसीदों के बारे में ईमेल भेजने के लिए है। इन छोटे और केंद्रित वर्गों के होने से उन्हें अन्य उपयोग के मामलों में जोड़ना आसान हो जाता है, इस प्रकार कोडबेस को बनाए रखना आसान और परीक्षण में आसान हो जाता है।
द सर्विस बैकस्टोरी
रूबी दुनिया में, सेवा वर्गों का उपयोग करने के दृष्टिकोण को क्रिया, संचालन और इसी तरह के रूप में भी जाना जाता है। कमांड पैटर्न के पीछे यह सब क्या है। कमांड पैटर्न के पीछे का विचार यह है कि एक वस्तु (या हमारे उदाहरण में, एक्लास) एक व्यावसायिक कार्रवाई करने या किसी घटना को ट्रिगर करने के लिए आवश्यक सभी सूचनाओं को समाहित कर रही है। कमांड के कॉलर को जो जानकारी पता होनी चाहिए वह है:
- कमांड का नाम
- कमांड ऑब्जेक्ट/क्लास पर कॉल करने की विधि का नाम
- विधि पैरामीटर के लिए पास किए जाने वाले मान
तो, हमारे मामले में, कमांड का कॉलर एक नियंत्रक है। दृष्टिकोण बहुत समान है, बस रूबी में नामकरण 'सेवा' है।
कार्य को विभाजित करें
यदि आपके नियंत्रक कुछ तृतीय पक्ष सेवाओं को कॉल कर रहे हैं और वे आपके प्रतिपादन को अवरुद्ध कर रहे हैं, तो हो सकता है कि इन कॉलों को निकालने और उन्हें किसी अन्य नियंत्रक कार्रवाई के साथ अलग से प्रस्तुत करने का समय हो। इसका एक उदाहरण तब हो सकता है जब आप किसी पुस्तक की जानकारी प्रस्तुत करने का प्रयास करते हैं और किसी अन्य सेवा से उसकी रेटिंग प्राप्त करते हैं जिसे आप वास्तव में प्रभावित नहीं कर सकते (जैसे गुडरीड्स)।
# app/controllers/books_controller.rb
class BooksController < ApplicationController
def show
@book = Book.find(params[:id])
@rating = GoodreadsRatingService.new(book).call
end
end
यदि गुड्रेड्स नीचे है या कुछ इसी तरह का है, तो आपके उपयोगकर्ताओं को गुड्रेड्स सर्वर के अनुरोध को टाइमआउट करने के लिए इंतजार करना होगा। या, अगर उनके सर्वर पर कुछ धीमा है, तो पेज धीरे-धीरे लोड होगा। आप तृतीय पक्ष सेवा की कॉलिंग को किसी अन्य क्रिया में इस तरह से निकाल सकते हैं:
# 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 %>
रेटिंग को book_rating
. में रेंडर करने के लिए HTML निकालें आंशिक, और डाल:
<%= content_for :render_async %>
आपकी लेआउट फ़ाइल के अंदर, रत्न book_rating_path
. को कॉल करेगा एक बार आपका पृष्ठ लोड होने पर AJAX अनुरोध के साथ, और जब रेटिंग प्राप्त की जाती है, तो यह पृष्ठ पर दिखाई देगी। इसमें एक बड़ा फायदा यह है कि आपके यूजर्स को अलग से रेटिंग लोड करके किताब का पेज तेजी से देखने को मिलता है।
या, यदि आप चाहें, तो आप बेसकैंप से टर्बो फ्रेम्स का उपयोग कर सकते हैं। विचार वही है, लेकिन आप केवल <turbo-frame>
का उपयोग करें। आपके मार्कअप में ऐसा तत्व:
<turbo-frame id="rating_1" src="/books/1/rating"> </turbo-frame>
आप जो भी विकल्प चुनते हैं, विचार यह है कि आपके मुख्य नियंत्रक कार्रवाई से भारी या परतदार काम को विभाजित किया जाए और उपयोगकर्ता को जल्द से जल्द पेज दिखाया जाए।
अंतिम विचार
यदि आप नियंत्रकों को पतला रखने का विचार पसंद करते हैं और उन्हें अन्य तरीकों के 'कॉलर्स' के रूप में चित्रित करते हैं, तो मेरा मानना है कि इस पोस्ट ने उन्हें इस तरह से रखने के बारे में कुछ जानकारी दी। कुछ पैटर्न और विरोधी पैटर्न जिनका हमने यहां उल्लेख किया है, निश्चित रूप से एक विस्तृत सूची नहीं है। यदि आपके पास इस बारे में कोई विचार है कि क्या बेहतर है या आप क्या पसंद करते हैं, तो कृपया ट्विटर पर पहुंचें और हम चर्चा कर सकते हैं।
निश्चित रूप से इस श्रृंखला पर बने रहें, हम कम से कम एक और ब्लॉग पोस्ट करने जा रहे हैं जहां हम आम रेल की समस्याओं और श्रृंखला से टेकअवे को सारांशित करते हैं।
अगली बार तक, चीयर्स!
पी.एस. यदि आप रूबी मैजिक की पोस्ट प्रेस से बाहर होते ही पढ़ना चाहते हैं, तो हमारे रूबी मैजिक न्यूजलेटर की सदस्यता लें और एक भी पोस्ट मिस न करें!