এই আর্টিকেলটি এর মূল উপস্থিতি থেকে সংশোধিত হয়েছে Playbook Thirty-nine - A Guide to Shipping Interactive Web Apps with Minimal Tooling-এ, এবং এই গেস্ট পোস্টের জন্য অ্যাপসিগনেলে ফিট করার জন্য টেইলর করা হয়েছে।
আপনার অ্যাপ যে অনেক ফাংশনালিটি হ্যান্ডেল করতে হবে, কিন্তু সেই লজিকটি কন্ট্রোলারে বা এমনকি মডেলেও 소속 নয়। কিছু উদাহরণ হল কার্ট দিয়ে চেক আউট করা, সাইটে রেজিস্টার করা, বা সাবস্ক্রিপশন শুরু করা।
আপনি এই সব লজিক কন্ট্রোলারে রাখতে পারেন, কিন্তু আপনি বারবার নিজেকে রিপিট করবেন, একই লজিক সব জায়গায় কল করবেন। আপনি লজিকটি মডেলে রাখতে পারেন, কিন্তু কখনো কখনো, আপনাকে অ্যাক্সেস দরকার পড়ে এমন জিনিসগুলো যা কন্ট্রোলারে সহজে উপলব্ধ, যেমন একটি IP ঠিকানা, বা একটি URL-এ প্যারামিটার। আপনার যা দরকার সেটি হল একটি সার্ভিস অবজেক্ট।
একটি সার্ভিস অবজেক্টের কাজ হল ফাংশনালিটি এনক্যাপসুলেট করা, একটি সার্ভিস এক্সিকিউট করা, এবং একটি সিঙ্গেল পয়েন্ট অফ ফেইলার প্রদান করা। সার্ভিস অবজেক্ট ব্যবহার করাও ডেভেলপারদের একই কোড বারবার লিখতে হয় না যখন সেটি অ্যাপ্লিকেশনের বিভিন্ন অংশে ব্যবহৃত হয়।
একটি সার্ভিস অবজেক্ট হল শুধু একটি Plain Old Ruby Object ("PORO")। এটি শুধু একটি ফাইল যা একটি নির্দিষ্ট ডিরেক্টরির অধীনে থাকে। এটি একটি রুবি ক্লাস যা একটি প্রেডিক্টেবল রেসপন্স রিটার্ন করে। কী রেসপন্সটিকে প্রেডিক্টেবল করে সেটি তিনটি কী অংশের কারণে। সব সার্ভিস অবজেক্ট একই প্যাটার্ন ফলো করা উচিত।
- একটি ইনিশিয়ালাইজেশন মেথড থাকে params আর্গুমেন্টের সাথে।
- একটি একক পাবলিক মেথড থাকে call নামের।
- একটি OpenStruct রিটার্ন করে success? এবং(payload অথবা error) এর সাথে।
OpenStruct কী?
এটি একটি ক্লাস এবং একটি হ্যাশের ব্রেইনচাইল্ডের মতো। আপনি এটিকে একটি মিনি ক্লাস হিসেবে চিন্তা করতে পারেন যেটি আর্বিট্রারি অ্যাট্রিবিউট গ্রহণ করতে পারে। আমাদের কেসে, আমরা এটিকে একটি সর্ট অফ টেম্পরারি ডেটা স্ট্রাকচার হিসেবে ব্যবহার করছি যেটি শুধু দুটি অ্যাট্রিবিউট হ্যান্ডেল করে।
যদি সাক্সেস true হয়, এটি একটি পেলোড অফ ডেটা রিটার্ন করে।
OpenStruct.new({success?:true, payload: 'some-data'})
যদি সাক্সেস false হয়, এটি একটি এরর রিটার্ন করে।
OpenStruct.new({success?:false, error: 'some-error'})
এখানে একটি সার্ভিস অবজেক্টের উদাহরণ যা আউট রিচ করে এবং অ্যাপসিগনেলের নতুন 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-এর উদার ব্যবহার করি একটি প্রেডিক্টেবল রেসপন্স রিটার্ন করতে। এটি সত্যিই মূল্যবান যখন টেস্ট লিখার কথা আসে কারণ লজিকের সব আর্কিটেকচারাল প্যাটার্ন একই।
মডিউল কী?
মডিউল ব্যবহার করলে আমাদের নেমস্পেসিং मिलে এবং অন্যান্য ক্লাসের সাথে কলিশন এড়ায়। এর মানে আপনি সব ক্লাসে একই মেথড নাম ব্যবহার করতে পারেন এবং সেগুলো ক্ল্যাশ করবে না কারণ সেগুলো একটি নির্দিষ্ট নেমস্পেসের অধীনে।
মডিউল নামের আরেকটি কী অংশ হল কীভাবে ফাইলগুলো আমাদের অ্যাপে সংগঠিত থাকে। সার্ভিস অবজেক্টগুলো একটি services ফোল্ডারে রাখা হয় প্রজেক্টে। উপরের সার্ভিস অবজেক্ট উদাহরণটি, AppServices মডিউল নামের সাথে, services ডিরেক্টরিতে AppServices ফোল্ডারে পড়ে।
আমি আমার সার্ভিস ডিরেক্টরি একাধিক ফোল্ডারে সংগঠিত করি, প্রতিটি অ্যাপ্লিকেশনের নির্দিষ্ট অংশের ফাংশনালিটি ধারণ করে।
উদাহরণস্বরূপ, CloudflareServices ডিরেক্টরি ক্লাউডফ্লেয়ারে সাবডোমেন তৈরি এবং মুছে ফেলার জন্য নির্দিষ্ট সার্ভিস অবজেক্ট ধারণ করে। Wistia এবং Zapier সার্ভিসগুলো তাদের রেসপেক্টিভ সার্ভিস ফাইল ধারণ করে।
আপনার সার্ভিস অবজেক্টগুলো এইভাবে সংগঠিত করলে ইমপ্লিমেন্টেশনের zaman আরও ভালো প্রেডিক্টেবিলিটি দেয়, এবং ১০k-ফুট ভিউ থেকে অ্যাপ কী করছে সেটি এক নজরে বুঝতে সহজ হয়।
চলুন StripeServices ডিরেক্টরিতে খোঁজা করি। এই ডিরেক্টরি স্ট্রাইপের API-এর সাথে ইন্টারঅ্যাক্ট করার জন্য আন্ডিভিজুয়াল সার্ভিস অবজেক্ট ধারণ করে। আবার, এই ফাইলগুলোর একমাত্র কাজ হল আমাদের অ্যাপ্লিকেশন থেকে ডেটা নেওয়া এবং স্ট্রাইপে পাঠানো। আপনি যদি কখনো 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: 'Subscription was successfully created.' else @subscription.destroy redirect_to subscribe_path, danger: "Subscription was created, but there was a problem with the vendor." end else redirect_to subscribe_path, danger:"Error creating subscription.' 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_customer_idইউজারের প্রোফাইলে সেভ করা আছে কিনা। যদি থাকে, আমরা কাস্টমার রিট্রিভ করি স্ট্রাইপ থেকে শুধু নিশ্চিত করতে যে কাস্টমার আসলে আছে, তারপর রিটার্ন করি আমাদের OpenStruct-এর পেলোডে। - যদি কাস্টমার এক্সিস্ট না করে, আমরা কাস্টমার তৈরি করি, stripe_customer_id সেভ করি, তারপর রিটার্ন করি OpenStruct-এর পেলোডে।
যে যাবে, আমাদের CustomerService স্ট্রাইপ কাস্টমার আইডি রিটার্ন করে, এবং সেটি করবে যা কিছু দরকার সেটি হয়ে তোলার জন্য। এখানে সেটি ফাইল:
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 ফাইলে ফিরে যাই। আমাদের এখন একটি কাস্টমার আছে যাকে আমরা স্ট্রাইপে পাঠাতে পারি, যা কমপ্লিট করে স্ট্রাইপে সাবস্ক্রিপশন তৈরি করার জন্য আমাদের প্রয়োজনীয় ডেটা।
আমরা এখন কল করার জন্য প্রস্তুত আশিক সার্ভিস অবজেক্ট, StripeServices::CreateSubscription ফাইল।
আবার, StripeServices::CreateSubscription সার্ভিস অবজেক্ট কখনো পরিবর্তন হয় না। এর একক দায়িত্ব, এবং সেটি হল ডেটা নেওয়া, স্ট্রাইপে পাঠানো, এবং অথবা সাক্সেস রিটার্ন করা অথবা অবজেক্টকে পেলোড হিসেবে রিটার্ন করা।
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
খুব সিম্পল ঠিক? কিন্তু আপনি احتمالত চিন্তা করছেন, এই ছোট ফাইল অওভারকিল। আসুন একটি অন্য উদাহরণ দেখি সেমিলার ফাইলের মতে উপরের মত, কিন্তু এইবার আমরা এটিকে অগমেন্ট করেছি মাল্টি-টেন্যান্ট অ্যাপ্লিকেশনের জন্য স্ট্রাইপ কানেক্টের মাধ্যমে।
এখানে জিনিসগুলো আকর্ষণীয় হয়। আমরা Mavenseed কে উদাহরণ হিসেবে ব্যবহার করছি, যদিও একই লজিক SportKeeper-এও চলে। আমাদের মাল্টি-টেন্যান্ট অ্যাপ একটি একক মনোলিথ, শেয়ারড টেবিল, একটি site_id কলাম দ্বারা পৃথক। প্রতিটি টেন্যান্ট স্ট্রাইপ কানেক্টের মাধ্যমে স্ট্রাইপের সাথে কানেক্ট হয়, এবং আমরা স্ট্রাইপ অ্যাকাউন্ট আইডি পাই টেন্যান্টের অ্যাকাউন্টে সেভ করার জন্য।
আমাদের একই স্ট্রাইপ API কল ব্যবহার করে, আমরা সিম্পলি স্ট্রাইপ অ্যাকাউন্ট পাস করতে পারি কানেক্টেড অ্যাকাউন্টের, এবং স্ট্রাইপ 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
এই ফাইলের কিছু টেকনিক্যাল নোট: আমি একটি সিম্পলার উদাহরণ শেয়ার করতে পারতাম, কিন্তু আমি সত্যিই মনে করি আপনার জন্য এটি মূল্যবান একটি প্রপার সার্ভিস অবজেক্ট কীভাবে স্ট্রাকচার করা হয় সেটি দেখতে, এর রেসপন্স সহ।
প্রথমে, "call" মেথডে একটি rescue এবং else স্টেটমেন্ট আছে। এটি একই যা নিচের লিখতে:
def call begin rescue Stripe ::StripeError => e else end end
কিন্তু রুবি মেথড অটোমেটিক্যালি একটি ব্লক ইমপ্লিসিটলি শুরু করে, তাই begin এবং end যোগ করার কোনো কারণ নেই। এই স্টেটমেন্টটি পড়া যায়, "সাবস্ক্রিপশন তৈরি করুন, এরর থাকলে রিটার্ন করুন, অন্যথায় সাবস্ক্রিপশন রিটার্ন করুন।"
সিম্পল, সংক্ষিপ্ত, এবং ইলিগ্যান্ট। রুবি সত্যিই একটি সুন্দর ভাষা এবং সার্ভিস অবজেক্টের ব্যবহার এটিকে হাইলাইট করে।
আশা করি আপনি সার্ভিস ফাইলগুলোর মূল্য বুঝতে পেরেছেন আমাদের অ্যাপ্লিকেশনে। সেগুলো আমাদের লজিক সংগঠন করার একটি খুব সংক্ষিপ্ত পদ্ধতি প্রদান করে যা শুধু প্রেডিক্টেবল নয় বরং সহজে মেইনটেইনযোগ্য!
P.S. আপনি যদি রুবি ম্যজিক পোস্টগুলো প্রেস থেকে বের होते সাথে সাথে পড়তে চান, আমাদের রুবি ম্যজিক নিউজলেটার সাবস্ক্রাইব করুন এবং কখনো একটি পোস্টও মিস করবেন না!
——
এই চ্যাপ্টার এবং আরও অনেক কিছু পড়ুন আমার নতুন বই Playbook Thirty-nine - A Guide to Shipping Interactive Web Apps with Minimal Tooling কিনে। এই বইতে, আমি একটি টপ-ডাউন অ্যাপ্রোচ নেওয়া সাধারণ প্যাটার্ন এবং টেকনিক কভার করেছি, একমাত্র আমার ফার্স্ট-হ্যান্ড অভিজ্ঞতা ভিত্তিক করে একজন সোলো-ডেভেলপার হিসেবে একাধিক হাই-ট্রাফিক, হাই-রেভিনিউ ওয়েবসাইট অ্যাপ্লিকেশন বানানো এবং মেইনটেইন করা।
কুপন কোড appsignalrocks ব্যবহার করুন এবং ৩০% বাঁচান!