Computer >> Máy Tính >  >> Lập trình >> Ruby

Cấu trúc lại Ruby bằng Sprout Classes

Một trong những thách thức lớn nhất khi làm việc với một số ứng dụng cũ là mã không được viết để có thể kiểm tra được. Vì vậy, việc viết các bài kiểm tra có ý nghĩa là khó hoặc không thể .

Đó là một vấn đề khó khăn:để viết các bài kiểm tra cho ứng dụng cũ, bạn phải thay đổi mã, nhưng bạn không thể tự tin thay đổi mã mà không viết các bài kiểm tra trước cho nó!

Bạn đối phó với nghịch lý này như thế nào?

Đây là một trong nhiều chủ đề nằm trong Làm việc hiệu quả với Mã kế thừa xuất sắc của Michael Feathers . Hôm nay tôi sẽ phóng to một kỹ thuật cụ thể từ cuốn sách có tên Lớp mầm .

Hãy sẵn sàng cho một số MÃ PHÁP LUẬT!

Chúng ta hãy xem xét lớp ActiveRecord cũ này có tên là Appointment . Nó khá dài và trong đời thực, nó dài hơn 100 dòng.

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

Thêm một số tính năng

Nếu chúng tôi được yêu cầu thêm một số chức năng mới vào khu vực báo cáo giao dịch, nhưng Appointment lớp có quá nhiều phụ thuộc để có thể kiểm tra được mà không cần cấu trúc lại nhiều, chúng ta tiến hành như thế nào?

Một tùy chọn là chỉ thực hiện thay đổi:

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

Thật là tệ

Có hai vấn đề với đoạn mã trên:

  1. Appointment có nhiều trách nhiệm khác nhau (vi phạm Nguyên tắc Trách nhiệm Đơn lẻ), một trong những trách nhiệm này là * ghi lại các giao dịch . Bằng cách thêm nhiều mã liên quan đến giao dịch vào Appointment lớp, ** chúng tôi đang làm cho mã tệ hơn một chút *.

  2. chúng tôi có thể viết một thử nghiệm tích hợp mới và kiểm tra xem email có vượt qua được hay không, nhưng vì chúng tôi sẽ không có Appointment lớp ở trạng thái có thể kiểm tra, chúng tôi không thể thêm bất kỳ bài kiểm tra đơn vị nào. Chúng tôi sẽ thêm nhiều mã chưa được kiểm tra hơn , điều đó tất nhiên là xấu. (Trên thực tế, Michael Feathers định nghĩa mã kế thừa là "mã không có kiểm tra", vì vậy chúng tôi sẽ thêm_more_ mã kế thừa vào mã kế thừa.)

Nâng cao thì tốt hơn

Một giải pháp tốt hơn là chỉ cần thêm nội tuyến mã mới là trích xuất hành vi ghi giao dịch vào lớp riêng của nó. Chúng ta sẽ gọi nó là 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

Phần thưởng

Sau đó, Appointment có thể giảm xuống chỉ sau:

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

Chúng tôi vẫn đang sửa đổi mã trong Appointment , không thể kiểm tra, nhưng bây giờ we_can_ kiểm tra mọi thứ trong TransactionRecorder và vì chúng tôi đã thay đổi từng hàm để chấp nhận các đối số thay vì sử dụng các biến phiên bản, chúng tôi thậm chí có thể kiểm tra từng hàm một cách riêng biệt. Vì vậy, hiện tại chúng tôi đang ở một vị trí tốt hơn nhiều so với khi chúng tôi bắt đầu.