Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

Refactoring Ruby โดยใช้ Sprout Classes

หนึ่งในความท้าทายที่ยากที่สุดในการทำงานกับแอปพลิเคชันรุ่นเก่าบางตัวคือโค้ดไม่ได้ถูกเขียนขึ้นเพื่อให้ทดสอบได้ ดังนั้น การเขียนแบบทดสอบที่มีความหมายจึงเป็นเรื่องยากหรือเป็นไปไม่ได้ .

มันเป็นปัญหาที่เกิดขึ้นระหว่างไก่กับไข่ ในการเขียนการทดสอบสำหรับแอปพลิเคชันรุ่นเก่า คุณต้องเปลี่ยนรหัส แต่คุณไม่สามารถเปลี่ยนรหัสได้อย่างมั่นใจโดยไม่เขียนการทดสอบก่อน!

คุณจัดการกับความขัดแย้งนี้อย่างไร

นี่เป็นหนึ่งในหลาย ๆ วิชาที่ครอบคลุมใน การทำงานอย่างมีประสิทธิภาพด้วยรหัสดั้งเดิม ที่ยอดเยี่ยมของ Michael Feathers . วันนี้ผมจะขยายความเทคนิคเฉพาะจากหนังสือชื่อ Sprout Class .

เตรียมพร้อมสำหรับรหัส LEGACY!

มาดูคลาส ActiveRecord เก่านี้ที่ชื่อว่า Appointment . มันค่อนข้างยาว และในชีวิตจริงมันยาวกว่า 100 บรรทัด

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
  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 class มีการพึ่งพามากเกินไปที่จะทดสอบได้โดยไม่ต้องมีการจัดโครงสร้างใหม่ เราจะดำเนินการอย่างไร

ทางเลือกหนึ่งคือทำการเปลี่ยนแปลง:

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 # New code
  end
end

def send_thank_you_email_to_client
  ThankYouMailer.thank_you_email(self).deliver
end

แย่จังเลย

โค้ดด้านบนมีปัญหาสองประการ:

  1. Appointment มีความรับผิดชอบที่แตกต่างกันมากมาย (การละเมิดหลักการความรับผิดชอบเดียว) หนึ่งในความรับผิดชอบเหล่านี้คือ*การบันทึกธุรกรรม . โดยเพิ่มโค้ดที่เกี่ยวข้องกับธุรกรรมเพิ่มเติมใน Appointment class **เรากำลังทำให้โค้ดแย่ลงอีกนิด *.

  2. เราอาจเขียนการทดสอบการรวมระบบใหม่และตรวจสอบเพื่อดูว่าอีเมลส่งผ่านหรือไม่ แต่เนื่องจากเราจะไม่มี Appointment คลาสในสถานะทดสอบได้ เราไม่สามารถเพิ่มการทดสอบหน่วยใดๆ เราจะเพิ่มโค้ดที่ยังไม่ได้ทดสอบเพิ่มเติม ซึ่งแน่นอนว่าแย่ (อันที่จริง Michael Feathers กำหนด รหัสเดิม เป็น "รหัสที่ไม่มีการทดสอบ" ดังนั้นเราจะเพิ่มรหัสดั้งเดิม _more_ ลงในรหัสดั้งเดิม)

เลิกกันจะดีกว่า

ทางออกที่ดีกว่าการเพิ่มโค้ดใหม่แบบอินไลน์คือการแยกพฤติกรรมการบันทึกธุรกรรมออกเป็นคลาสของตัวเอง เราจะเรียกมันว่า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
end

เรายังคงแก้ไขโค้ดใน Appointment ซึ่งไม่สามารถทดสอบได้ แต่ตอนนี้ we_can_ ทดสอบทุกอย่างใน TransactionRecorder และเนื่องจากเราได้เปลี่ยนแต่ละฟังก์ชันให้ยอมรับอาร์กิวเมนต์แทนที่จะใช้ตัวแปรอินสแตนซ์ เราจึงสามารถทดสอบแต่ละฟังก์ชันแบบแยกส่วนได้ ตอนนี้เราอยู่ในจุดที่ดีกว่าตอนเริ่มต้นมาก