রুবি অন রেলস প্যাটার্ন এবং অ্যান্টি-প্যাটার্ন সিরিজের দ্বিতীয় পোস্টে আপনাকে স্বাগতম। গত ব্লগ পোস্টে আমরা প্যাটার্ন এবং অ্যান্টি-প্যাটার্ন সাধারণভাবে কী সেটা নিয়ে আলোচনা করেছিলাম। আমরা রেলস বিশ্বের কিছু সবচেয়ে বিখ্যাত প্যাটার্ন এবং অ্যান্টি-প্যাটার্নও উল্লেখ করেছিলাম। এই ব্লগ পোস্টে, আমরা কয়েকটি রেলস মডেল অ্যান্টি-প্যাটার্ন এবং প্যাটার্নের মধ্য দিয়ে যাব।
আপনি যদি মডেল নিয়ে সংগ্রাম করছেন, তবে এই ব্লগ পোস্ট আপনার জন্য। আমরা দ্রুত আপনার মডেলগুলোর ডায়েট করানোর প্রক্রিয়া নিয়ে যাব এবং মাইগ্রেশন লেখার সময় এড়ানো উচিত কিছু বিষয়ের সাথে শক্তিশালীভাবে শেষ করব। চলুন সরাসরি শুরু করি।
চর্বি ওভারওয়েট মডেল
রেলস অ্যাপ্লিকেশন ডেভেলপ করার সময়, চাহে সেটা ফুল-ব্লোন রেলস ওয়েবসাইট হোক বা API, লোকরা অধিকাংশ লজিক মডেলে রাখতে পছন্দ করে। গত ব্লগ পোস্টে, আমাদের पास Song ক্লাসের একটি উদাহরণ ছিল যা অনেক কাজ করত। মডেলে অনেক কিছু রাখা সিঙ্গল রেসপন্সিবিলিটি প্রিন্সিপল (SRP) লঙ্ঘন করে।
চলুন এক নজর দেই।
class Song < ApplicationRecord belongs_to :album belongs_to :artist belongs_to :publisher has_one :text has_many :downloads validates :artist_id, presence: true validates :publisher_id, presence: true after_update :alert_artist_followers after_update :alert_publisher def alert_artist_followers return if unreleased? artist.followers.each { |follower| follower.notify(self) } end def alert_publisher PublisherMailer.song_email(publisher, self).deliver_now end def includes_profanities? text.scan_for_profanities.any? end def user_downloaded?(user) user.library.has_song?(self) end def find_published_from_artist_with_albums ... end def find_published_with_albums ... end def to_wav ... end def to_mp3 ... end def to_flac ... end end
এই ধরনের মডেলের সমস্যা হল সেগুলো গান 관련된 বিভিন্ন লজিকের ডাম্পিং গ্রাউন্ড হয়ে যায়। মেথডগুলো একে بعد একে করে সময়ের সাথে সাথে জমে ওঠে।
আমি মডেলের ভিতরের কোড ছোট ছোট মডিউলে ভাগ করার পরামর্শ দিয়েছিলাম। কিন্তু এতে আপনি কেবল কোড এক জায়গা থেকে অন্য জায়গায় সরাচ্ছেন। তবুও, কোড সরানো আপনাকে কোড ভালোভাবে সংগঠিত করতে সাহায্য করে এবং পঠনযোগ্যতা বাড়িয়ে চর্বি দিয়ে ভরা মডেল এড়াতে সাহায্য করে।
কিছু লোক রেলস কনসার্নস ব্যবহার করছেন এবং মনে করছেন যে লজিকটি মডেলগুলোর জুড়ে রি-ইউজ করা যাচ্ছে। আমি আগে এর সম্পর্কে লিখেছিলাম এবং কিছু লোক এটিকে পছন্দ করেছিল, অন্যদের পছন্দ ছিল না। যাই হোক, কনসার্নসের কাহিনী মডিউলের মতোই। আপনাকে সচেতন থাকা উচিত যে আপনি কেবল কোড একটি মডিউলে সরাচ্ছেন যেটি কোথাও ইনক্লুড করা যাবে।
একটি বিকল্প হল ছোট ক্লাস তৈরি করা এবং প্রয়োজনে সেগুলো কল করা। উদাহরণস্বরূপ, আমরা গান কনভার্ট করার কোড আলাদা একটি ক্লাসে এক্সট্র্যাক্ট করতে পারি।
class SongConverter attr_reader :song def initialize(song) @song = song end def to_wav ... end def to_mp3 ... end def to_flac ... end end class Song ... def converter SongConverter.new(self) end ... end
এখন আমাদের কাছে SongConverter আছে যেটির উদ্দেশ্য গানকে বিভিন্ন ফরম্যাটে কনভার্ট করা। এর নিজস্ব টেস্ট থাকতে পারে এবং কনভার্ট সংক্রান্ত ভবিষ্যতের লজিকও। এবং, যদি আমরা গানকে MP3-তে কনভার্ট করতে চাই, তবে নিচের মতো করতে পারি:
@song.converter.to_mp3
আমার মতে, এটি মডিউল বা কনসার্ন ব্যবহার করার চেয়ে স্পষ্ট লাগে। হয়তো কারণ আমি ইনহেরিট্যান্সের চেয়ে কম্পোজিশন পছন্দ করি। আমি এটাকে আরও ইনটুইটিভ এবং পঠনযোগ্য मानি। আমি আপনাকে দুটি কেস রিভিউ করার পরামর্শ দিই সিদ্ধান্ত নেওয়ার আগে। অথবা আপনি দুটোই বেছে নিতে পারেন যদি চান, কেউ আপনাকে বাধা দিচ্ছে না।
এসকিউএল পাস্টা পারমিজান
বাস্তব জীবনে কে ভালো পাস্টা পছন্দ করে না? অন্যদিকে, কোড পাস্টা নিয়ে বললে প্রায় কেউ ফ্যান নही। এবং কافي কারণ রয়েছে। রেলস মডেলে, আপনি দ্রুত আপনার অ্যাক্টিভ রেকর্ড ব্যবহারকে স্পাঘেটিতে পরিণত করতে পারেন, কোডবেসের সর্বত্র ঘুরে বেড়াতে পারেন। এটা কীভাবে এড়ানো যায়?
একটু আইডিয়া আছে যা সেই লম্বা কোয়ারিগুলোকে স্পাঘেটি बनने থেকে রোকাতে পারে। প্রথমে দেখি ডেটাবেস-সম্পর্কিত কোড কীভাবে সব জায়গায় থাকতে পারে। আমাদের Song মডেলে ফিরে যাই। সীমাবদ্ধ, যখন আমরা কিছু ফেচ করার চেষ্টা করি।
class SongReportService def gather_songs_from_artist(artist_id) songs = Song.where(status: :published) .where(artist_id: artist_id) .order(:title) ... end end class SongController < ApplicationController def index @songs = Song.where(status: :published) .order(:release_date) ... end end class SongRefreshJob < ApplicationJob def perform songs = Song.where(status: :published) ... end end
উপরের উদাহরণে, Song মডেল কোয়ারি করার তিনটি ইউজ-কেস আছে। SongReporterService-এ যা গান সম্পর্কে ডেটা রিপোর্ট করার জন্য ব্যবহৃত হয়, আমরা একটি নির্দিষ্ট আর্টিস্ট থেকে প্রকাশিত গান পেতে চাই। তারপর, SongController-এ, আমরা প্রকাশিত গান পাই এবং রিলিজ তারিখ দ্বারা সাজাই। এবং অবশেষে, SongRefreshJob-এ আমরা শুধু প্রকাশিত গান পাই এবং তাদের সাথে কিছু করি।
এটা সব ঠিক আছে, কিন্তু হঠাৎ আমরা যদি স্ট্যাটাসের নাম released এ পরিবর্তন করতে চাই বা গান ফেচ করার পদ্ধতিতে অন্য কোনো পরিবর্তন আনতে চাই? তবে আমাদের সব জায়গা আলাদা আলাদা এডিট করতে হবে। এছাড়া, উপরের কোড DRY নয়। এটি অ্যাপ্লিকেশনের জুড়ে নিজে নিজে রিপিট হচ্ছে। এটা আপনাকে নিরুৎসাহিত না করুক। ভাগ্যভরে, এই সমস্যার সমাধান আছে।
আমরা রেলস স্কোপ ব্যবহার করে এই কোডকে DRY করতে পারি। স্কোপিং আপনাকে সাধারণত ব্যবহৃত কোয়ারিগুলো ডিফাইন করতে দেয়, যেগুলো অ্যাসোসিয়েশন এবং অবজেক্টসে কল করা যাবে। এটি আমাদের কোডকে পঠনযোগ্য এবং পরিবর্তন করা সহজ করে তোলে। কিন্তু, হয়তো সবচেয়ে গুরুত্বপূর্ণ বিষয় হল স্কোপ আমাদের এক্সি ব্যবহার করতে দেয় অন্য অ্যাক্টিভ রেকর্ড মেথড যেমন joins, where, ইত্যাদি। চলুন দেখি স্কোপ সহ আমাদের কোড কেমন দেখায়।
class Song < ApplicationRecord ... scope :published, -> { where(published: true) } scope :by_artist, ->(artist_id) { where(artist_id: artist_id) } scope :sorted_by_title, { order(:title) } scope :sorted_by_release_date, { order(:release_date) } ... end class SongReportService def gather_songs_from_artist(artist_id) songs = Song.published.by_artist(artist_id).sorted_by_title ... end end class SongController < ApplicationController def index @songs = Song.published.sorted_by_release_date ... end end class SongRefreshJob < ApplicationJob def perform songs = Song.published ... end end
হয়ে গেলো। আমরা রিপিটিং কোড কাটিয়ে মডেলে রাখলাম। কিন্তু এটি সবসময় ভালো ফল দেয় না, বিশেষ করে যদি আপনি চর্বি দিয়ে ভরা মডেল বা গড অবজেক্টের শিকার হন। মডেলে আরও বেশি মেথড এবং দায়িত্ব যোগ করা ভালো আইডিয়া নही হতে পারে।
এখানে আমার পরামর্শ হল স্কোপ ব্যবহার ন্যূনতম রাখা এবং শুধু সাধারণ কোয়ারিগুলো এক্সট্র্যাক্ট করা। আমাদের কেসে, হয়তো where(published: true) একটি পেরফেক্ট স্কোপ কারণ এটি সব জায়গায় ব্যবহৃত হচ্ছে। অন্যান্য SQL-সম্পর্কিত কোডের জন্য, আপনি রিপোজিটরি প্যাটার্ন নামে কিছু ব্যবহার করতে পারেন। চলুন জানি সেটি কী।
রিপোজিটরি প্যাটার্ন
আমরা যা দেখাতে যাচ্ছি সেটি ডোমেন-ড্রিভেন ডিজাইন বইয়ে ডিফাইন করা রিপোজিটরি প্যাটার্নের ১:১ নয়। আমাদের এবং রেলস রিপোজিটরি প্যাটার্নের পেছনের আইডিয়া হল ডেটাবেস লজিককে বিজনেস লজিক থেকে আলাদা করা। আমরা পুরো ভাবে যেতে পারি এবং একটি রিপোজিটরি ক্লাস তৈরি করতে পারি যা আমাদের জন্য র JDBC কল করবে অ্যাক্টিভ রেকর্ডের বদলে, কিন্তু আমি এমন কিছু রিকমেন্ড করব না যদি না সেটি খুব জরুরি হয়।
আমরা কী করতে পারি হল একটি SongRepository তৈরি করা এবং সেখানে ডেটাবেস লজিক রাখা।
class SongRepository class << self def find(id) Song.find(id) rescue ActiveRecord::RecordNotFound => e raise RecordNotFoundError, e end def destroy(id) find(id).destroy end def recently_published_by_artist(artist_id) Song.where(published: true) .where(artist_id: artist_id) .order(:release_date) end end end class SongReportService def gather_songs_from_artist(artist_id) songs = SongRepository.recently_published_by_artist(artist_id) ... end end class SongController < ApplicationController def destroy ... SongRepository.destroy(params[:id]) ... end end
এখানে আমরা কোয়ারি লজিককে একটি টেস্টযোগ্য ক্লাসে আলাদা করেছি। এছাড়া, মডেলটি আর স্কোপ এবং লজিকের চিন্তা করে না। কন্ট্রোলার এবং মডেল দুটোই পাতলা, এবং সবাই খুশি। ঠিক? ঠিক, এখনো অ্যাক্টিভ রেকর্ড সেখানে সকল কঠোর কাজ করছে। আমাদের দৃশ্যে, আমরা find ব্যবহার করছি, যা নিচের SQL জেনারেট করে:
SELECT "songs".* FROM "songs" WHERE "songs"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
"সঠিক" পদ্ধতি হলে এই সব SongRepository-র ভিতরে ডিফাইন করা উচিত। যেমন আমি বলেছি, আমি সেটি রিকমেন্ড করব না। আপনার সেটি দরকার নেই এবং আপনি পূর্ণ নিয়ন্ত্রণ চান। অ্যাক্টিভ রেকর্ড থেকে দূরে যাওয়ার একটি ইউজ কেস হলে আপনাকে SQL-এ কিছু কমপ্লেক্স ট্রিকের দরকার পড়তে পারে যা অ্যাক্টিভ রেকর্ড সহজে সাপোর্ট করে না।
র JDBC এবং অ্যাক্টিভ রেকর্ড সম্পর্কে কথা বলছি, আমাকে একটা টপিক জানাতে হবে। মাইগ্রেশন এবং সেগুলো কীভাবে সঠিকভাবে করা যায় সেটি সম্পর্কে। চলুন নিমগ্ন হই।
মাইগ্রেশন — কে চিন্তা করে?
মাইগ্রেশন লেখার সময় আমি প্রায়শই একটি যুক্তি শুনি যে সেখানে কোড বাকি অ্যাপ্লিকেশনের মতো ভালো হওয়া দরকার নেই। এবং সেই যুক্তি আমার সাথে খুব মেলে না। লোকরা এই দাবিটি ব্যবহার করে মাইগ্রেশনে গন্ধাক্ত কোড সেট আপ করে কারণ সেটি শুধু একবার চালাবে এবং ভুলে যাবে। হয়তো এটি সত্য যদি আপনি কয়েকজন লোকের সাথে কাজ করেন এবং সবাই সবসময় সিঙ্কে থাকেন।
বাস্তবতা প্রায়শই আলাদা। অ্যাপ্লিকেশনটি অনেক লোক ব্যবহার করতে পারে যারা অ্যাপ্লিকেশনের বিভিন্ন অংশের সম্পর্কে কিছুই জানে না। এবং আপনি যদি সেখানে কিছু সন্দেহজনক ওয়ান-অফ কোড রাখেন, তবে আপনি কাউকে তাদের ডেভেলপমেন্ট এনভায়রনমেন্ট কয়েক ঘণ্টা ভাঙতে পারেন কারণ দূষিত ডেটাবেস স্টেট বা শুধু একটি विचিত্র মাইগ্রেশন। নিশ্চিত নই যে এটি অ্যান্টি-প্যাটার্ন কিনা, কিন্তু আপনাকে এর সম্পর্কে জানা উচিত।
মাইগ্রেশন অন্যদের জন্য কীভাবে আরো সুবিধাজনক করতে পারি? চলুন একটি তালিকা দিয়ে যাই যা মাইগ্রেশন সবার জন্য সহজ করবে।
নিশ্চিত করুন যে আপনি সবসময় ডাউন মেথড প্রদান করেন
আপনি কখন জানেন কিছু রোলব্যাক হবে। যদি আপনার মাইগ্রেশন রিভার্সিবল না হয়, তবে নিশ্চিত করুন ActiveRecord::IrreversibleMigration এক্সেপশন রেইজ করেন:
def down raise ActiveRecord::IrreversibleMigration end
মাইগ্রেশনে অ্যাক্টিভ রেকর্ড এড়াতে চেষ্টা করুন
এখানে আইডিয়া হল এক্সটার্নাল ডিপেন্ডেন্সি ন্যূনতম করা ডেটাবেসের স্টেটের ব্যতিরেকে যখন মাইগ্রেশন এক্সিকিউট হবে। তো কোনো অ্যাক্টিভ রেকর্ড ভ্যালিডেশন নেই যা আপনার দিন বরবাদ (বা হয়তো বাঁচাতে) করবে। আপনি ছাড়া প্লেইন SQL ছেড়ে পাবেন। উদাহরণস্বরূপ, লিখুন একটি মাইগ্রেশন যা একটি নির্দিষ্ট আর্টিস্টের সব গান পাবলিশ করবে।
class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0] def up execute <<-SQL UPDATE songs SET published = true WHERE artist_id = 46 SQL end def down execute <<-SQL UPDATE songs SET published = false WHERE artist_id = 46 SQL end end
আপনার যদি Song মডেলের জন্য বড় প্রয়োজন হয়, একটি পরামর্শ হলে সেটি মাইগ্রেশনের ভিতরে ডিফাইন করা। সেভাবে, আপনি আপনার মাইগ্রেশনকে app/models-এর আসল অ্যাক্টিভ রেকর্ড মডেলের যেকোনো সম্ভাব্য পরিবর্তন থেকে বুলেটপ্রুফ করতে পারেন। কিন্তু, কি সব ঠিক আছে? চলুন আমাদের পরবর্তী পয়েন্টে যাই।
স্কিমা মাইগ্রেশন থেকে ডেটা মাইগ্রেশন আলাদা করুন
রেলস গাইডস অন মাইগ্রেশন পড়লে, আপনি নিচের কথা পাবেন:
মাইগ্রেশন হল অ্যাক্টিভ রেকর্ডের একটি ফিচার যা আপনাকে আপনার ডেটাবেস স্কিমা কালক্রমে উন্নত করতে দেয়। пиュর SQL-এ স্কিমা মডিফিকেশন লেখার বদলে, মাইগ্রেশন আপনাকে একটি রুবি DSL ব্যবহার করতে দেয় আপনার টেবিলগুলোর পরিবর্তন বর্ণনা করার জন্য।
গাইডের সারাংশে, ডেটাবেস টেবিলের আসল ডেটা এডিট করার কোনো জিক্র নেই, শুধু কাঠামো। তো, ফকודה আমরা রেগুলার মাইগ্রেশন ব্যবহার করে গান আপডেট করেছি দुसरे পয়েন্টে সেটি পুরোপুরি সঠিক নয়।
আপনার প্রজেক্টে যদি আপনাকে নিয়মিত একই জিনিস করতে হয়, data_migrate জেম ব্যবহার করার কথা বিবেচনা করুন। এটি ডেটা মাইগ্রেশনকে স্কিমা মাইগ্রেশন থেকে আলাদা করার একটি সুন্দর পদ্ধতি। আমরা সহজে আমাদের আগের উদাহরণটি এর সাথে রি-রাইট করতে পারি। ডেটা মাইগ্রেশন জেনারেট করতে, আমরা নিচের মতো করতে পারি:
bin/rails generate data_migration update_artists_songs_to_published
তারপর সেখানে মাইগ্রেশন লজিক যোগ করুন:
class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0] def up execute <<-SQL UPDATE songs SET published = true WHERE artist_id = 46 SQL end def down execute <<-SQL UPDATE songs SET published = false WHERE artist_id = 46 SQL end end
এইভাবে, আপনি আপনার সকল স্কিমা মাইগ্রেশন db/migrate ডিরেক্টরিতে রাখছেন এবং ডেটা নিয়ে কাজ করা সকল মাইগ্রেশন db/data ডিরেক্টরিতে।
চূড়ান্ত মতামত
মডেলের সাথে কাজ করা এবং রেলসে সেগুলো পঠনযোগ্য রাখা একটি دائماً চলমান সংগ্রাম। আশা করি এই ব্লগ পোস্টে আপনি সম্ভাব্য গর্ত এবং সাধারণ সমস্যার সমাধানগুলো দেখতে পেয়েছেন। এই পোস্টে মডেল অ্যান্টি-প্যাটার্ন এবং প্যাটার্নের তালিকা đầyশো দূরে, কিন্তু এগুলো gần الماضية সবচেয়ে উল্লেখযোগ্য موارد যা আমি খুঁজে পেয়েছি।
আপনি যদি আরো রেলস প্যাটার্ন এবং অ্যান্টি-প্যাটার্নে আগ্রহী হন, তবে সিরিজের পরবর্তী ইনস্টলমেন্টের জন্য থাকুন। আগামী পোস্টগুলোতে, আমরা রেলস MVC-এর ভিউ এবং কন্ট্রোলার দিকের সাধারণ সমস্যা এবং সমাধান নিয়ে যাব।
তখন পর্যন্ত, চেয়ার্স!
P.S. আপনি যদি রুবি ম্যজিক পোস্টগুলো প্রেস থেকে বের होते সাথে সাথে পড়তে চান, আমাদের রুবি ম্যজিক নিউজলেটার সাবস্ক্রাইব করুন এবং কখনো একটি পোস্টও মিস করবেন না!