রুবি অন রেইলস প্যাটার্নস অ্যান্ড অ্যান্টি-প্যাটার্নস সিরিজের চতুর্থ কিস্তিতে স্বাগতম।
আমরা আগে প্যাটার্ন ও অ্যান্টি-প্যাটার্নস সাধারণভাবে এবং রেইলস মডেল ও ভিউর সংশ্লিষ্ট আলোচনা করেছি। এই পোস্টে, আমরা MVC (Model-View-Controller) ডিজাইন প্যাটার্নের চূড়ান্ত অংশ — কন্ট্রোলার বিশ্লেষণ করব। চলুন ডুব মারি এবং রেইলস কন্ট্রোলার সংশ্লিষ্ট প্যাটার্ন ও অ্যান্টি-প্যাটার্নগুলো যেতে পারি।
সামনের মাঠে (At The Front Lines)
যেহেতু রুবি অন রেইলস একটি ওয়েব ফ্রেমওয়ার্ক, HTTP রিকোয়েস্টগুলো এর একটি حیוניים অংশ। সকল ধরণের ক্লায়েন্ট রিকোয়েস্ট দিয়ে রেইলস ব্যাকএন্ডে পৌঁছায় এবং এখানেই কন্ট্রোলার চমক দেয়। কন্ট্রোলারগুলো রিকোয়েস্ট রিসিভ এবং হ্যান্ডেল করার সামনের মাঠে থাকে। এটা কন্ট্রোলারকে রুবি অন রেইলস ফ্রেমওয়ার্কের একটি মৌলিক অংশ করে তোলে। অবশ্য, কন্ট্রোলারের আগে কোড আছে, কিন্তু কন্ট্রোলার কোড এমন কিছু যা আমাদের অধিকাংশ কন্ট্রোল করতে পারি।
একবার আপনি config/routes.rb-এ রুট ডিফাইন করলে, আপনি সেট করা রুটে সার্ভার হিট করতে পারেন, এবং সংশ্লিষ্ট কন্ট্রোলার বাকি কাজগুলো নিতে পারে। উপরের বাক্যটি পড়লে লাগতে পারে সব কিছু তত সিম্পল। কিন্তু, প্রায়শই, প্রচুর ভার কন্ট্রোলারের কাঁধে পড়ে। অথেন্টিকেশন এবং অথোরাইজেশনের চিন্তা আছে, তারপর ডেটা কীভাবে ফেচ করবেন, সেই সাথে বিজনেস লজিক কোথায় এবং কীভাবে পারফর্ম করবেন সেটার সমস্যা।
এই সব চিন্তা এবং দায়িত্ব যা কন্ট্রোলারের ভিতরে ঘটতে পারে, সেগুলো কিছু অ্যান্টি-প্যাটার্নে নিয়ে যেতে পারে। সবচোয়ে 'প্রসিদ্ধ' একটি হলো "ফ্যাট" (Fat) কন্ট্রোলারের অ্যান্টি-প্যাটার্ন।
ফ্যাট (Obese) কন্ট্রোলার
কন্ট্রোলারে খুব বেশি লজিক রাখার সমস্যা হলো: আপনি সিঙ্গল রেসপন্সিবিলিটি প্রিন্সিপল (SRP) লঙ্ঘন করছেন। এর মানে আমরা কন্ট্রোলারের ভিতরে অনেক বেশি কাজ করছি। প্রায়শই, এতে প্রচুর কোড এবং দায়িত্ব সেখানে জমা হয়ে যায়। এখানে, 'ফ্যাট' বলতে কন্ট্রোলার ফাইলগুলোর এক্সটেনসিভ কোড এবং কন্ট্রোলার যে লজিক সাপোর্ট করে সেটা বোঝায়। এটি প্রায়শই একটি অ্যান্টি-প্যাটার্ন হিসেবে বিবেচিত হয়।
কন্ট্রোলার কী করবে সেটা নিয়ে অনেক মতামত রয়েছে। কন্ট্রোলারের দায়িত্বগুলোর একটি কমন গ্রাউন্ড নিচে দেওয়া হলো:
- অথেন্টিকেশন এবং অথোরাইজেশন — চেক করা যে রিকোয়েস্টের পিছনে যে এন্টিটি (প্রায়শই ইউজার) সে কী বলে সেটা কি সত্যি এবং সেটাকে রিসোর্স এক্সেস বা অ্যাকশন পারফর্ম করার অনুমতি আছে কিনা। প্রায়শই, অথেন্টিকেশন সেশন বা কুকিতে সংরক্ষিত থাকে, কিন্তু কন্ট্রোলারকে এখনও চেক করতে হয় অথেন্টিকেশন ডেটা এখনো বৈধ কিনা।
- ডেটা ফেচিং (Data Fetching) — এটি রিকোয়েস্টের সাথে আসা প্যারামিটার বেসড সঠিক ডেটা খুঁজে বের করার লজিক কল করতে হবে। একটি আদর্শ জগতে, এটি একটি মেথড কল হওয়া উচিত যা সारा কাজ করবে। কন্ট্রোলারকে ভারী কাজ করা উচিত নয়, এটি ডেলিগেট করা উচিত।
- টেমপ্লেট রেন্ডারিং (Template Rendering) — অবশেষে, এটি সঠিক ফরম্যাটে (HTML, JSON, ইত্যাদি) রেজাল্ট রেন্ডার করে সঠিক রেসপন্স রিটার্ন করতে হবে। অথবা, অন্য কোনো পাথ বা URL-এ রিডাইরেক্ট করতে হবে।
এই ধারণাগুলো ফলো করলে আপনি কন্ট্রোলার অ্যাকশন এবং জেনারেলি কন্ট্রোলারে অনেক কিছু হওয়া থেকে বাঁচতে পারেন। কন্ট্রোলার লেভেলে সিম্পল রাখলে আপনি আপনার অ্যাপ্লিকেশনের অন্যান্য এলাকায় কাজ ডেলিগেট করতে পারবেন। দায়িত্বগুলো ডেলিগেট করে এবং সেগুলো একো একো টেস্ট করলে নিশ্চিত হবে আপনি আপনার অ্যাপ রবাস্টভাবে ডেভেলপ করছেন।
নিশ্চয়, আপনি উপরের প্রিন্সিপলগুলো ফলো করতে পারেন, কিন্তু আপনি উদাহরণের জন্য আগ্রহী হয়ে থাকবেন। চলুন ডুব মারি এবং দেখি কী প্যাটার্নগুলো ব্যবহার করে কন্ট্রোলারকে কিছু ভার থেকে মুক্ত করতে পারি।
কোয়েরি অবজেক্ট (Query Objects)
কন্ট্রোলার অ্যাকশনের ভিতরে যে একটি সমস্যা হয় সেটা হলো: অত্যধিক ডেটা কোয়েরি করা। আপনি যদি আমাদের রেইলস মডেল অ্যান্টি-প্যাটার্ন ও প্যাটার্ন ব্লগ পোস্ট ফলো করেন, তবে আমরা একটি সিমিলার সমস্যা যেনে গিয়েছিলাম যেখানে মডেলগুলোর ভিতরে খুব বেশি কোয়েরি লজিক ছিল। কিন্তু, এইবার আমরা Query Object নামে একটি প্যাটার্ন ব্যবহার করব। একটি Query Object হলো একটি টেকনিক যা আপনার জটিল কোয়েরিগুলো একটি একক অবজেক্টে আইসোলেট করে।
অধিকাংশ কেসে, Query Object হলো একটি Plain Old Ruby Object যা একটি ActiveRecord রিলেশন দিয়ে ইনিশিয়ালাইজ করা হয়। একটি টিপিক্যাল Query Object দেখতে পারে এমন:
# 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 রাখলেন, কিন্তু এটাকে আরও এলিগেন্ট করে ফেললেন।
কোয়েরি অবজেক্টের চমৎকার দিক: আপনি সেগুলো আইসোলেশনে টেস্ট করতে পারেন এবং নিশ্চিত করতে পারেন সেগুলো সঠিক কাজ করছে কিনা। আরও, আপনি এই কোয়েরি ক্লাসগুলো এক্সটেন্ড করতে পারেন এবং কন্ট্রোলারের লজিক নিয়ে চিন্তা না করেই সেগুলো টেস্ট করতে পারেন। একটা কথা নোট করুন: আপনার রিকোয়েস্ট প্যারামিটারগুলো অন্য জায়গায় হ্যান্ডেল করা উচিত, এবং কোয়েরি অবজেক্টের ওপর নির্ভর করা উচিত নয়। আপনি কী মনে করেন, কোয়েরি অবজেক্ট একবার ট্রাই করবেন?
সার্ভিসে প্রস্তুত (Ready To Serve)
ঠিক আছে, আমরা ডেটা গ্য더িং এবং ফেচিং Query Objects-এ ডেলিগেট করার পদ্ধতি দেখেছি। ডেটা গ্য더িং এবং সেটা রেন্ডার করার মাঝপথে জমা হওয়া লজিকের কী করব? ভালো প্রশ্ন করলেন, কারণ একটি সমাধান হলো Services ব্যবহার করা। একটি সার্ভিস প্রায়শই একটি PORO (Plain Old Ruby Object) হিসেবে বিবেচিত হয় যা একটি সিঙ্গল (বিজনেস) অ্যাকশন পারফর্ম করে। আমরা এই আইডিয়া একটু এক্সপ্লোর করব।
কল্পনা করুন আমাদের দুটি সার্ভিস আছে। একটি রিসিপ্ট তৈরি করে, অন্যটি ইউজারকে রিসিপ্ট পাঠায়:
# 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-এর Operation চেক আউট করুন।
সংক্ষেপে, নতুন CalculateReceiptTotalService সার্ভিসটি সকল নম্বর কران্চিং ডিল করবে। আমাদের CreateReceiptService রেসপন্সিবল ডাটাবেসে রিসিপ্ট লিখতে। SendReceiptService আছে ইউজারদের তাদের রিসিপ্ট সম্পর্কে ইমেইল ডিসপ্যাচ করার জন্য। এই ছোট এবং ফোকাসড ক্লাসগুলো অন্যান্য ইউজ কেসে কম্বাইন করা সহজ করে তোলে, ফলে মেইনটেইন এবং টেস্ট করা সহজ কোডবেস তৈরি হয়।
সার্ভিসের পেছনের কাহিনী (The Service Backstory)
রুবি বিশ্বে, সার্ভিস ক্লাস ব্যবহার করার পদ্ধতিকে actions, operations, এবং সিমিলার নামেও জানা যায়। এই সব আসলে Command প্যাটার্নে অবলম্বন করে। Command প্যাটার্নের আইডিয়া হলো: একটি অবজেক্ট (বা আমাদের উদাহরণে, একটি ক্লাস) একটি বিজনেস অ্যাকশন পারফর্ম করার বা একটি ইভেন্ট ট্রিগার করার জন্য প্রয়োজনীয় সকল ইনফরমেশন এনক্যাপসুলেট করে। কলারের জানা দরকার:
- কমান্ডের নাম
- কমান্ড অবজেক্ট/ক্লাসে কল করার মেথড নাম
- মেথড প্যারামিটার pasaba করার ভ্যালুগুলো
আমাদের কেসে, কমান্ডের কলার হলো কন্ট্রোলার। পদ্ধতিটি খুব সিমিলার, শুধু রুবিতে নামকরণ 'Service'।
কাজ ভাগ করুন (Split Up The Work)
আপনার কন্ট্রোলার যদি কিছু থার্ড পার্টি সার্ভিস কল করে এবং সেগুলো আপনার রেন্ডারিং ব্লক করে, তবে হয়তো এখন সেই কলগুলো এক্সট্রাক্ট করে আলাদা কন্ট্রোলার অ্যাকশনে রেন্ডার করার সময় এসেছে। একটি উদাহরণ হতে পারে: আপনি একটি বইর ইনফো রেন্ডার করতে চাচ্ছেন এবং এর রেটিং অন্য কোনো সার্ভিস থেকে ফেচ করছেন যেটাকে আপনি কন্ট্রোল করতে পারেন না (যেমন 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 পাথ কল করতে হবে, কিন্তু হে, আপনার show অ্যাকশনে আর কোনো ব্লকার নেই। এছাড়া, আপনার '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>
আপনি যেকোনো অপশন বেছে নিন, আইডিয়া হলো: আপনার মেইন কন্ট্রোলার অ্যাকশন থেকে ভারী বা ফ্লেকি কাজ আলাদা করুন এবং ইউজারকে যত দ্রুত সম্ভব পেজ দেখান।
চূড়ান্ত চিন্তা
আপনি যদি কন্ট্রোলার পাতলা রাখার আইডিয়া পছন্দ করেন এবং সেগুলো শুধু অন্যান্য মেথডের 'কলার' হিসেবে চিত্রিত করেন, তবে আমি মনে করি এই পোস্ট আপনাকে কিছু ইনসাইট দিয়েছে কীভাবে সেগুলো ওই রকম রাখা যায়। আমরা যেসব প্যাটার্ন ও অ্যান্টি-প্যাটার্ন আলোচনা করলাম সেগুলো অবশ্যই একটি এক্সহাস্টিভ লিস্ট নয়। আপনার যদি কোনো ভালো আইডিয়া থাকে কী ভালো বা কী পছন্দ, টুইটারে যোগাযোগ করুন, আমরা আলোচনা করব।
নিশ্চয়ই এই সিরিজে বने रहুন, আমরা কমপক্ষে আর একটি ব্লগ পোস্ট করব যেখানে সিরিজ থেকে কমন রেইলস সমস্যা এবং টেকঅওয়ে সামারাইজ করব।
পরবর্তী বার পর্যন্ত, চিয়ার্স!
P.S. Ruby Magic পোস্টগুলো প্রেস থেকে বের होतेই পেতে চাইলে, আমাদের Ruby Magic নিউজলেটার সাবস্ক্রাইব করুন এবং কখনো একক পোস্ট মিস করবেন না!