কম্পিউটার টিউটোরিয়াল

লিগেসি রুবি রিফ্যাক্টরিং: স্প্রাউট ক্লাসের সাথে

কিছু লিগেসি অ্যাপ্লিকেশনের সাথে কাজ করার সবচেয়ে জটিল চ্যালেঞ্জগুলোতে একটি হলো কোডটি টেস্টযোগ্য হতে লেখা হয়নি। তাই অর্থপূর্ণ টেস্ট লিখা কঠিন বা অসম্ভব।

এটা একটি চিকেন-অ্যান্ড-এগ সমস্যা: লিগেসি অ্যাপ্লিকেশনের জন্য টেস্ট লিখতে হলে আপনাকে কোড পরিবর্তন করতে হয়, কিন্তু আপনি কোড পরিবর্তন করতে পারবেন না আত্মবিশ্বাসের সাথে যদি আপনি প্রথমে সেটার জন্য টেস্ট না লিখেন!

এই প্যারাডক্সের সাথে কীভাবে ডিল করবেন?

এটা মাইকেল ফেথার্সের চমৎকার বই Working Effectively with Legacy Code-এ কভার করা অনেক বিষয়ের মধ্যে একটি। আজ আমি বই থেকে একটি নির্দিষ্ট টেকনিকের ওপর জুম ইন করব যাকে বলা হয় স্প্রাউট ক্লাস (Sprout Class)।

লিগেসি কোডের জন্য तैयार হন!

এই পুরনো ActiveRecord ক্লাস Appointment-কে দেখুন। এটা খুব বড়, এবং রিয়েল লাইফে এটা ১০০-রও বেশি লাইন বড়।

class Appointment < ActiveRecord::Base
  has_many :appointment_services, :dependent => :destroy
  has_many :services, :through => :appointment_services
  has_many :appointment_products, :dependent => :destroy
  has_many :products, :through => :appointment_products
  has_many :payments, :dependent => :destroy
  has_many :transaction_items
  belongs_to :client
  belongs_to :stylist
  belongs_to :time_block_type

  def record_transactions
    transaction_items.destroy_all if paid_for?
    save_service_transaction_items
    save_product_transaction_items
    save_tip_transaction_item
  end

  def save_service_transaction_items
    appointment_services.reload.each { |s| s.save_transaction_item(self.id) }
  end

  def save_product_transaction_items
    appointment_products.reload.each { |p| p.save_transaction_item(self.id) }
  end

  def save_tip_transaction_item
    TransactionItem.create!(
      :appointment_id => self.id,
      :stylist_id => self.stylist_id,
      :label => "Tip",
      :price => self.tip,
      :transaction_item_type_id => TransactionItemType.find_or_create_by_code("TIP").id
    )
  end
end

কিছু ফিচার যোগ করুন

আমাদের যদি ট্রানজ্যাকশন-রিপোর্টিং এরিয়াতে কিছু নতুন ফাংশন্যালিটি যোগ করতে বলা হয়, কিন্তু Appointment ক্লাসের ততগুলো ডিপেন্ডেন্সি আছে যে সেটা অনেক রিফ্যাক্টরিং ছাড়া টেস্টযোগ্য নয়, তবে আমরা কীভাবে এগিয়ে যাব?

একটি অপশন হলো শুধু পরিবর্তন করুন:

def record_transactions
  transaction_items.destroy_all if paid_for?
  save_service_transaction_items
  save_product_transaction_items
  save_tip_transaction_item
  send_thank_you_email_to_client # নতুন কোড
end

def send_thank_you_email_to_client
  ThankYouMailer.thank_you_email(self).deliver
end

এটা খারাপ লাগে

উপরের কোডের সাথে দুটি সমস্যা আছে:

  1. Appointment-এর অনেক আলাদা দায়িত্ব আছে (সিঙ্গেল রেসপন্সিবিলিটি প্রিন্সিপালের লঙ্ঘন), যেগুলোর মধ্যে একটি হলো ট্রানজ্যাকশন রেকর্ড করা। Appointment ক্লাসে আরও ট্রানজ্যাকশন-রিলেটেড কোড যোগ করে, আমরা কোডটাকে একটু আরো খারাপ করছি।

  2. আমরা হয়তো একটি নতুন ইন্টিগ্রেশন টেস্ট লিখতে পারি এবং চেক করতে পারি ইমেইল পাঠিয়েছে কিনা, কিন্তু যেহেতু আমাদের Appointment ক্লাস টেস্টযোগ্য স্টেটে নয়, আমরা কোনো ইউনিট টেস্ট যোগ করতে পারব না। আমরা আরও অ-টেস্টেড কোড যোগ করব, যা অবশ্যই খারাপ। (মাইকেল ফেথার্স, ফ্যাক্টে, লিগেসি কোড-কে "কোড উইথআউট টেস্ট" হিসেবে ডিফাইন করেছেন, তাই আমরা আরও লিগেসি কোড লিগেসি কোডে যোগ করব।)

চাঙ্কিং ইট আপ ইজ बेटার

সিম্পলি নতুন কোড ইনলাইন যোগ করার চেয়ে ভালো সমাধান হলো ট্রানজ্যাকশন-রেকর্ডিং বিহেভিয়রকে তার নিজস্ব ক্লাসে এক্সট্রাক্ট করা। আমরা এটাকে বলব, ধরা যাক, TransactionRecorder:

class TransactionRecorder
  def initialize(options)
    @appointment_id = options[:appointment_id]
    @appointment_services = options[:appointment_services]
    @appointment_products = options[:appointment_products]
    @stylist_id = options[:stylist_id]
    @tip = options[:tip]
  end

  def run
    save_service_transaction_items(@appointment_services)
    save_product_transaction_items(@appointment_products)
    save_tip_transaction_item(@appointment_id, @stylist_id, @tip_amount)
  end

  def save_service_transaction_items(appointment_services)
    appointment_services.each { |s| s.save_transaction_item(appointment_id) }
  end

  def save_product_transaction_items(appointment_products)
    appointment_products.each { |p| p.save_transaction_item(appointment_id) }
  end

  def save_tip_transaction_item(appointment_id, stylist_id, tip)
    TransactionItem.create!(
      appointment_id: appointment_id,
      stylist_id: stylist_id,
      label: "Tip",
      price: tip,
      transaction_item_type_id: TransactionItemType.find_or_create_by_code("TIP").id
    )
  end
end

দ্যা পে-অফ

তারপর Appointment ছোট হয়ে যাবে শুধু এটাতে:

class Appointment < ActiveRecord::Base
  has_many :appointment_services, :dependent => :destroy
  has_many :services, :through => :appointment_services
  has_many :appointment_products, :dependent => :destroy
  has_many :products, :through => :appointment_products
  has_many :payments, :dependent => :destroy
  has_many :transaction_items
  belongs_to :client
  belongs_to :stylist
  belongs_to :time_block_type

  def record_transactions
    transaction_items.destroy_all if paid_for?
    TransactionRecorder.new(
      appointment_id: id,
      appointment_services: appointment_services,
      appointment_products: appointment_products,
      stylist_id: stylist_id,
      tip: tip
    ).run
  end
end

আমরা এখনও Appointment-এ কোড মডিফাই করছি, যেটা টেস্ট করা যায় না, কিন্তু এখন আমরা TransactionRecorder-এ সবকিছু টেস্ট করতে পারি, এবং যেহেতু আমরা প্রতিটি ফাংশন আর্গুমেন্ট এক্সেপ্ট করার জন্য পরিবর্তন করেছি ইনস্ট্যান্স ভেরিয়েবল ব্যবহার করার বদলে, আমরা প্রতিটি ফাংশন আলাদা আলাদা টেস্ট করতে পারি। সুতরাং আমরা আমরা শুরু করেছিলাম থেকে অনেক ভালো পজিশনে আছি।

  1. RuboCop দিয়ে রুবি কোড কোয়ালিটি উন্নত: লিন্টিং ও অটো-ফরম্যাটিং

  2. রুবিতে ল্যাম্বডা মাস্টার করুন: বেসিক থেকে অ্যাডভান্সড টেকনিক

  3. রুবি এক্সসেপশন দক্ষতা TracePoint দিয়ে: জটিল ত্রুটি পরিস্থিতি ডায়াগনোসिस

  4. রুবিতে স্ট্যাটিক অ্যানালাইসিস মাস্টার করুন: Parser Gem দিয়ে মেথড ও প্যারামিটার এক্সট্রাক্ট করুন