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:
-
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àoAppointment
lớp, ** chúng tôi đang làm cho mã tệ hơn một chút *. -
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.