इस लेख को प्लेबुक थर्टी-नाइन - मिनिमल टूलिंग के साथ इंटरैक्टिव वेब ऐप्स शिप करने के लिए एक गाइड में इसकी मूल उपस्थिति से संशोधित किया गया है, और इस गेस्ट पोस्ट के लिए AppSignal के अनुरूप बनाया गया है।
ऐसी बहुत सी कार्यक्षमता है जिसे आपके ऐप को संभालने की आवश्यकता है, लेकिन वह तर्क आवश्यक रूप से कंट्रोलर या मॉडल में भी नहीं है। कुछ उदाहरणों में कार्ट के साथ चेकआउट करना, साइट के लिए पंजीकरण करना, या सदस्यता शुरू करना शामिल है।
आप इस सभी तर्क को कंट्रोलर में शामिल कर सकते हैं, लेकिन आप खुद को दोहराते रहेंगे, उन सभी स्थानों पर समान तर्क को कॉल करेंगे। आप तर्क को मॉडल में डाल सकते हैं, लेकिन कभी-कभी, आपको उन चीजों तक पहुंच की आवश्यकता होती है जो कंट्रोलर में आसानी से उपलब्ध हैं, जैसे IP पता, या URL में एक पैरामीटर। आपको जो चाहिए वह एक सर्विस ऑब्जेक्ट है।
एक सर्विस ऑब्जेक्ट का काम कार्यक्षमता को एनकैप्सुलेट करना, एक सेवा निष्पादित करना, और विफलता का एकल बिंदु प्रदान करना है। सर्विस ऑब्जेक्ट्स का उपयोग करने से डेवलपर्स को एप्लिकेशन के विभिन्न हिस्सों में उपयोग किए जाने पर बार-बार वही कोड लिखने से रोका जाता है।
एक सर्विस ऑब्जेक्ट बस एक प्लेन ओल्ड रूबी ऑब्जेक्ट ("PORO") है। यह बस एक फ़ाइल है जो एक विशिष्ट डायरेक्टरी के तहत रहती है। यह एक रूबी क्लास है जो एक अनुमानित प्रतिक्रिया लौटाती है। प्रतिक्रिया को अनुमानित बनाने वाली तीन प्रमुख भागों के कारण है। सभी सर्विस ऑब्जेक्ट्स को एक ही पैटर्न का पालन करना चाहिए।
- एक इनिशियलाइज़ेशन मेथड जिसमें पैरामीटर्स आर्ग्यूमेंट हो।
- कॉल नामक एक एकल पब्लिक मेथड हो।
- एक OpenStruct लौटाता है जिसमें success? और या तो पेलोड या एरर हो।
OpenStruct क्या है?
यह एक क्लास और हैश का ब्रेनचाइल्ड जैसा है। आप इसे एक मिनी-क्लास के रूप में सोच सकते हैं जो मनमाने एट्रिब्यूट प्राप्त कर सकता है। हमारे मामले में, हम इसे एक प्रकार की अस्थायी डेटा संरचना के रूप में उपयोग कर रहे हैं जो केवल दो एट्रिब्यूट को संभालती है।
यदि सफलता true है, तो यह डेटा का पेलोड लौटाता है।
OpenStruct.new({success ?:true, payload: 'some-data'})
यदि सफलता false है, तो यह एक एरर लौटाता है।
OpenStruct.new({success ?:false, error: 'some-error'})
यहां एक सर्विस ऑब्जेक्ट का उदाहरण है जो AppSignals के नए API तक पहुंचता है और डेटा हड़पता है, जो वर्तमान में बीटा में है।
module AppServices class AppSignalApiService require 'httparty' def initialize(params) @endpoint = params[:endpoint] || 'markers' end def call result = HTTParty.get("https://appsignal.com/api/#{appsignal_app_id}/#{@endpoint}.json?token=#{appsignal_api_key}") rescue HTTParty::Error => e OpenStruct.new({success?: false, error: e}) else OpenStruct.new({success?: true, payload: result}) end private def appsignal_app_id ENV['APPSIGNAL_APP_ID'] end def appsignal_api_key ENV['APPSIGNAL_API_KEY'] end end endआप ऊपर की फ़ाइल को AppServices::AppSignalApiService.new({endpoint: 'markers'}).call के साथ कॉल करेंगे। मैं अनुमानित प्रतिक्रिया लौटाने के लिए OpenStruct का उदारतापूर्वक उपयोग करता हूं। यह टेस्ट लिखने के लिए वास्तव में मूल्यवान है क्योंकि सभी तर्क के आर्किटेक्चरल पैटर्न समान हैं।
मॉड्यूल क्या है?
मॉड्यूल का उपयोग हमें नेमस्पेसिंग प्रदान करता है और अन्य क्लासेस के साथ टकराव को रोकता है। इसका मतलब है कि आप सभी क्लासेस में समान मेथड नामों का उपयोग कर सकते हैं और वे टकराएंगे नहीं क्योंकि वे एक विशिष्ट नेमस्पेस के तहत हैं।
मॉड्यूल नाम का एक अन्य प्रमुख हिस्सा यह है कि फ़ाइलें हमारे ऐप में कैसे व्यवस्थित हैं। सर्विस ऑब्जेक्ट्स को प्रोजेक्ट में सर्विसेज फ़ोल्डर में रखा जाता है। ऊपर दिए गए सर्विस ऑब्जेक्ट उदाहरण, AppServices मॉड्यूल नाम के साथ, सर्विसेज डायरेक्टरी में AppServices फ़ोल्डर में आता है।
मैं अपने सर्विस डायरेक्टरी को कई फ़ोल्डर्स में व्यवस्थित करता हूं, जिनमें से प्रत्येक में एप्लिकेशन के एक विशिष्ट भाग के लिए कार्यक्षमता होती है।
उदाहरण के लिए, CloudflareServices डायरेक्टरी Cloudflare पर सबडोमेन बनाने और हटाने के लिए विशिष्ट सर्विस ऑब्जेक्ट्स रखती है। Wistia और Zapier सेवाएं अपने संबंधित सर्विस फ़ाइलों को रखती हैं।
अपने सर्विस ऑब्जेक्ट्स को इस तरह व्यवस्थित करने से कार्यान्वयन के मामले में बेहतर पूर्वानुमान मिलता है, और 10k-फुट व्यू से यह देखना आसान होता है कि ऐप क्या कर रहा है।
आइए StripeServices डायरेक्टरी में खोदें। यह डायरेक्टरी Stripe के API के साथ इंटरैक्ट करने के लिए व्यक्तिगत सर्विस ऑब्जेक्ट्स रखती है। फिर, इन फ़ाइलों का एकमात्र काम हमारे एप्लिकेशन से डेटा लेना और इसे Stripe पर भेजना है। यदि आपको कभी भी StripeService ऑब्जेक्ट में API कॉल को अपडेट करने की आवश्यकता है जो सदस्यता बनाता है, तो आपके पास ऐसा करने के लिए केवल एक स्थान है।
हमारे एप्लिकेशन से भेजे जाने वाले डेटा को इकट्ठा करने वाला सभी तर्क AppServices डायरेक्टरी में रहने वाले एक अलग सर्विस ऑब्जेक्ट में किया जाता है। ये फ़ाइलें हमारे एप्लिकेशन से डेटा इकट्ठा करती हैं और इसे बाहरी API के साथ इंटरफेस करने के लिए संबंधित सर्विस डायरेक्टरी में भेजती हैं।
यहां एक दृश्य उदाहरण है: मान लें कि हमारे पास कोई है जो एक नई सदस्यता शुरू कर रहा है। सब कुछ एक कंट्रोलर से शुरू होता है। यहां SubscriptionsController है।
class SubscriptionsController < ApplicationController def create @subscription = Subscription.new(subscription_params) if @subscription.save result = AppServices::SubscriptionService.new({ subscription_params: { subscription: @subscription, coupon: params[:coupon], token: params[:stripeToken] } }).call if result && result.success? sign_in @subscription.user redirect_to subscribe_welcome_path, success: 'सदस्यता सफलतापूर्वक बनाई गई।' else @subscription.destroy redirect_to subscribe_path, danger: "सदस्यता बनाई गई, लेकिन विक्रेता के साथ समस्या थी।" end else redirect_to subscribe_path, danger:"सदस्यता बनाने में त्रुटि।" end end endहम पहले ऐप में सदस्यता बनाएंगे, और यदि यह सफल है, तो हम उसे, stripeToken, और कूपन जैसी चीजों को AppServices::SubscriptionService नामक फ़ाइल में भेजते हैं।
AppServices::SubscriptionService फ़ाइल में, कई चीजें होने की आवश्यकता है। यहां वह ऑब्जेक्ट है, इससे पहले कि हम इसमें क्या हो रहा है, उसमें जाएं:
module AppServices class SubscriptionService def initialize(params) @subscription = params[:subscription_params][:subscription] @token = params[:subscription_params][:token] @plan = @subscription.subscription_plan @user = @subscription.user end def call # ग्राहक बनाएं या ढूंढें customer ||= AppServices::StripeCustomerService.new({customer_params: {customer:@user, token:@token}}).call if customer && customer.success? subscription ||= StripeServices::CreateSubscription.new({subscription_params:{ customer: customer.payload, items:[subscription_items], expand: ['latest_invoice.payment_intent'] }}).call if subscription && subscription.success? @subscription.update_attributes( status: 'active', stripe_id: subscription.payload.id, expiration: Time.at(subscription.payload.current_period_end).to_datetime ) OpenStruct.new({success?: true, payload: subscription.payload}) else handle_error(subscription&.error) end else handle_error(customer&.error) end end private attr_reader :plan def subscription_items base_plan end def base_plan [{ plan: plan.stripe_id }] end def handle_error(error) OpenStruct.new({success?: false, error: error}) end end endउच्च-स्तरीय अवलोकन से, यहां हम क्या देख रहे हैं:
हमें पहले Stripe ग्राहक आईडी प्राप्त करनी होगी ताकि हम इसे Stripe पर सदस्यता बनाने के लिए भेज सकें। यह अपने आप में एक पूरी तरह से अलग सर्विस ऑब्जेक्ट है जो ऐसा करने के लिए कई चीजें करता है।
- हम जांचते हैं कि क्या
stripe_customer_idउपयोगकर्ता की प्रोफ़ाइल पर सहेजा गया है। यदि है, तो हम ग्राहक को Stripe से केवल यह सुनिश्चित करने के लिए पुनर्प्राप्त करते हैं कि ग्राहक वास्तव में मौजूद है, फिर इसे हमारे OpenStruct के पेलोड में लौटाते हैं। - यदि ग्राहक मौजूद नहीं है, तो हम ग्राहक बनाते हैं, stripe_customer_id सहेजते हैं, फिर इसे OpenStruct के पेलोड में लौटाते हैं।
किसी भी तरह से, हमारा CustomerService Stripe ग्राहक आईडी लौटाता है, और यह जो आवश्यक है वह करेगा। यहां वह फ़ाइल है:
module AppServices class CustomerService def initialize(params) @user = params[:customer_params][:customer] @token = params[:customer_params][:token] @account = @user.account end def call if @account.stripe_customer_id.present? OpenStruct.new({success?: true, payload: @account.stripe_customer_id}) else if find_by_email.success? && find_by_email.payload OpenStruct.new({success?: true, payload: @account.stripe_customer_id}) else create_customer end end end private attr_reader :user, :token, :account def find_by_email result ||= StripeServices::RetrieveCustomerByEmail.new({email: user.email}).call handle_result(result) end def create_customer result ||= StripeServices::CreateCustomer.new({customer_params:{email:user.email, source: token}}).call handle_result(result) end def handle_result(result) if result.success? account.update_column(:stripe_customer_id, result.payload.id) OpenStruct.new({success?: true, payload: account.stripe_customer_id}) else OpenStruct.new({success?: false, error: result&.error}) end end end end उम्मीद है, आप यह देखना शुरू कर सकते हैं कि हम अपने तर्क को कई सर्विस ऑब्जेक्ट्स में संरचित क्यों करते हैं। क्या आप एक विशाल बीहमोथ फ़ाइल की कल्पना कर सकते हैं जिसमें यह सभी तर्क हैं? कोई तरीका नहीं!
हमारे AppServices::SubscriptionService फ़ाइल पर वापस। अब हमारे पास एक ग्राहक है जिसे हम Stripe पर भेज सकते हैं, जो Stripe पर सदस्यता बनाने के लिए हमारे पास आवश्यक डेटा को पूरा करता है।
अब हम अंतिम सर्विस ऑब्जेक्ट, StripeServices::CreateSubscription फ़ाइल को कॉल करने के लिए तैयार हैं।
फिर से, StripeServices::CreateSubscription सर्विस ऑब्जेक्ट कभी नहीं बदलता। इसकी एकल जिम्मेदारी है, और वह है डेटा लेना, इसे Stripe पर भेजना, और या तो सफलता लौटाना या ऑब्जेक्ट को पेलोड के रूप में लौटाना।
module StripeServices class CreateSubscription def initialize(params) @subscription_params = params[:subscription_params] end def call subscription = Stripe::Subscription.create(@subscription_params) rescue Stripe::StripeError => e OpenStruct.new({success?: false, error: e}) else OpenStruct.new({success?: true, payload: subscription}) end end endकाफी सरल है ना? लेकिन आप शायद सोच रहे हैं, यह छोटी फ़ाइल ओवरकिल है। आइए ऊपर वाले के समान एक और उदाहरण देखें, लेकिन इस बार हमने इसे Stripe Connect के माध्यम से मल्टी-टेनेंट एप्लिकेशन के उपयोग के लिए संवर्धित किया है।
यहां चीजें दिलचस्प हो जाती हैं। हम यहां Mavenseed का उदाहरण के रूप में उपयोग कर रहे हैं, हालांकि यह समान तर्क SportKeeper पर भी चलता है। हमारा मल्टी-टेनेंट ऐप एक सिंगल मोनोलिथ है, टेबल साझा करता है, एक site_id कॉलम द्वारा अलग किया गया। प्रत्येक टेनेंट Stripe Connect के माध्यम से Stripe से जुड़ता है, और हमें फिर टेनेंट के खाते पर सहेजने के लिए एक Stripe अकाउंट आईडी मिलती है।
हमारे समान Stripe API कॉल का उपयोग करके, हम बस कनेक्टेड अकाउंट के Stripe अकाउंट को पास कर सकते हैं, और Stripe कनेक्टेड अकाउंट की ओर से API कॉल करेगा।
एक तरह से, हमारा StripeService ऑब्जेक्ट मेन एप्लिकेशन और टेनेंट्स दोनों के साथ डबल-ड्यूटी कर रहा है, उसी फ़ाइल को कॉल करने के लिए, लेकिन अलग डेटा भेज रहा है।
module StripeServices class CreateSubscription def initialize(params) @subscription_params = params[:subscription_params] @stripe_account = params[:stripe_account] @stripe_secret_key = params[:stripe_secret_key] ? params[:stripe_secret_key] : (Rails.env.production? ? ENV['STRIPE_LIVE_SECRET_KEY'] : ENV['STRIPE_TEST_SECRET_KEY']) end def call subscription = Stripe::Subscription.create(@subscription_params, account_params) rescue Stripe::StripeError => e OpenStruct.new({success?: false, error: e}) else OpenStruct.new({success?: true, payload: subscription}) end private attr_reader :stripe_account, :stripe_secret_key def account_params { api_key: stripe_secret_key, stripe_account: stripe_account, stripe_version: ENV['STRIPE_API_VERSION'] } end end endइस फ़ाइल पर कुछ तकनीकी नोट्स: मैं एक सरल उदाहरण साझा कर सकता था, लेकिन मुझे वास्तव में लगता है कि आपके लिए यह देखना मूल्यवान है कि एक उचित सर्विस ऑब्जेक्ट कैसे संरचित है, जिसमें इसकी प्रतिक्रियाएं शामिल हैं।
सबसे पहले, "कॉल" मेथड में एक रेस्क्यू और एल्स स्टेटमेंट है। यह निम्न लिखने के समान है:
def call begin rescue Stripe ::StripeError => e else end endलेकिन रूबी मेथड्स स्वचालित रूप से एक ब्लॉक को निहित रूप से शुरू करते हैं, इसलिए begin और end जोड़ने का कोई कारण नहीं है। यह स्टेटमेंट इस तरह पढ़ता है, "सदस्यता बनाएं, यदि कोई त्रुटि है तो लौटाएं, अन्यथा सदस्यता लौटाएं।"
सरल, संक्षिप्त, और सुंदर। रूबी वास्तव में एक सुंदर भाषा है और सर्विस ऑब्जेक्ट्स का उपयोग वास्तव में इसे उजागर करता है।
मुझे आशा है कि आप देख सकते हैं कि सर्विस फ़ाइलें हमारे एप्लिकेशन में क्या मूल्य निभाती हैं। वे हमारे तर्क को व्यवस्थित करने का एक बहुत ही संक्षिप्त तरीका प्रदान करती हैं जो न केवल अनुमानित है बल्कि आसानी से बनाए रखने योग्य है!
P.S. यदि आप रूबी मैजिक पोस्ट को प्रेस से निकलते ही पढ़ना चाहते हैं, तो हमारे रूबी मैजिक न्यूज़लेटर की सदस्यता लें और कभी भी एक भी पोस्ट मिस न करें!
——
मेरी नई किताब प्लेबुक थर्टी-नाइन - मिनिमल टूलिंग के साथ इंटरैक्टिव वेब ऐप्स शिप करने के लिए एक गाइड को चुनकर इस अध्याय और अधिक पढ़ें। इस किताब में, मैं अपने पहले हाथ के अनुभव के आधार पर सामान्य पैटर्न और तकनीकों को कवर करने के लिए एक टॉप-डाउन दृष्टिकोण अपनाता हूं, एक सोलो-डेवलपर के रूप में कई हाई-ट्रैफिक, हाई-रिवेन्यू वेबसाइट एप्लिकेशन बनाने और बनाए रखने के लिए।
कूपन कोड appsignalrocks का उपयोग करें और 30% बचाएं!