কিছু লিগেসি অ্যাপ্লিকেশনের সাথে কাজ করার সবচেয়ে জটিল চ্যালেঞ্জগুলোতে একটি হলো কোডটি টেস্টযোগ্য হতে লেখা হয়নি। তাই অর্থপূর্ণ টেস্ট লিখা কঠিন বা অসম্ভব।
এটা একটি চিকেন-অ্যান্ড-এগ সমস্যা: লিগেসি অ্যাপ্লিকেশনের জন্য টেস্ট লিখতে হলে আপনাকে কোড পরিবর্তন করতে হয়, কিন্তু আপনি কোড পরিবর্তন করতে পারবেন না আত্মবিশ্বাসের সাথে যদি আপনি প্রথমে সেটার জন্য টেস্ট না লিখেন!
এই প্যারাডক্সের সাথে কীভাবে ডিল করবেন?
এটা মাইকেল ফেথার্সের চমৎকার বই 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
এটা খারাপ লাগে
উপরের কোডের সাথে দুটি সমস্যা আছে:
Appointment-এর অনেক আলাদা দায়িত্ব আছে (সিঙ্গেল রেসপন্সিবিলিটি প্রিন্সিপালের লঙ্ঘন), যেগুলোর মধ্যে একটি হলো ট্রানজ্যাকশন রেকর্ড করা।Appointmentক্লাসে আরও ট্রানজ্যাকশন-রিলেটেড কোড যোগ করে, আমরা কোডটাকে একটু আরো খারাপ করছি।আমরা হয়তো একটি নতুন ইন্টিগ্রেশন টেস্ট লিখতে পারি এবং চেক করতে পারি ইমেইল পাঠিয়েছে কিনা, কিন্তু যেহেতু আমাদের
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-এ সবকিছু টেস্ট করতে পারি, এবং যেহেতু আমরা প্রতিটি ফাংশন আর্গুমেন্ট এক্সেপ্ট করার জন্য পরিবর্তন করেছি ইনস্ট্যান্স ভেরিয়েবল ব্যবহার করার বদলে, আমরা প্রতিটি ফাংশন আলাদা আলাদা টেস্ট করতে পারি। সুতরাং আমরা আমরা শুরু করেছিলাম থেকে অনেক ভালো পজিশনে আছি।