[CleanCode] Replace Conditional with Null Object
Tiếp theo trong loạt bài về Clean Code trong Rails, lại nói về vấn đề Conditional Statement, mình đã có 1 bài viết tại đây về nó: Replace Conditional with Polymorphism. Trong bài này, mình xin được viết về một phương pháp khác để tránh những đoạn Conditional Statement dài dòng trong ứng dụng Rails, ...
Tiếp theo trong loạt bài về Clean Code trong Rails, lại nói về vấn đề Conditional Statement, mình đã có 1 bài viết tại đây về nó: Replace Conditional with Polymorphism. Trong bài này, mình xin được viết về một phương pháp khác để tránh những đoạn Conditional Statement dài dòng trong ứng dụng Rails, đó là sử dụng Null Object. Chắc hẳn, ai đã từng sử dụng Ruby đều cảm thấy quen thuộc với nil, và Ruby đã sinh ra rất nhiều method để Developer có thể làm việc với nil như: nil?, present?, try,...Tuy nhiên việc sử dụng quá nhiều có thể dẫn đến việc trùng lặp code, nếu gặp phải vấn đề như vậy, hãy thử thay thế chúng bằng Null Object.
Example
Chúng ta có model Question:
# app/models/question.rb def most_recent_answer_text answers.most_recent_try(:text) || Answer::MISSING_TEXT end
Method most_recent_answer_text sẽ tìm đến associations answers cả question, giá trị trả về là text của answer mới nhất, tuy nhiên, ở đây nó phải kiểm tra là giá trị đó có thực sự tồn tại hay không? vì most_recent có thể trả về nil.
# app/model/answer.rb def self.most_recent order(:created_at).last end
Có một điều phải chú ý là: tất cả các method dùng most_recent sẽ phải check nil như:
# app/models/user.rb def answer_text_for question question.answers.for_user(self).try(:text) || Answer::MISSING_TEXT end
# app/models/anwser.rb def self.for_user user joins(:completion).where(completions: {user_id: user.id}).last end
User#answer_text_for như chúng ta thấy phải thực hiện check nil lại 1 lần nữa, điều này là code bị lặp. Chúng ta sẽ không cần phải duplicate lại code trong model User và Question bằng Null Object:
# app/models/question.rb def most_recent_answer_text answers.most_recent.text end
# app/models/user.rb def answer_text_for question question.answers.for_user(self).text end
Như vậy, chúng ta sẽ assume rằng model Answer sẽ luôn trả về 1 giá trị khác nil và User và Question sẽ không cần phải check nữa.
# app/models/answer.rb class Answer < ActiveRecord::Base belongs_to :completion belongs_to :question: validates :text, presence: true def self.for_user user joins(:completion).where(completions: {user_id: user.id}).last || NullAnswer.new end def self.most_recent order(:created_at).last || NullAnswer.new end end
most_recent và for_user bây giờ sẽ không trả về giá trị nil nữa, chúng ta sẽ chỉ cần implement class NullAnswer đơn giản như sau:
class NullAnswer def text "No response" end end
Tuy nhiên, chúng ta vẫn có thể refactor lại model Answer một chút để tránh lặp code.
# app/models/anwser.rb class Answer < ActiveRecord::Base ... def self.for_user user joins(:completion).where(completions: {user_id: user.id}).last_or_null end def self.most_recent order(:created_at).last_or_null end private def self.last_or_null last || NullAnswer.new end end
Như vậy, chỉ bằng class NullAnswer đơn giản chúng ta đã tránh được các đoạn check nil bị duplicated trong ứng dụng.
Conclusion
Những lợi ích mà Null Object mang lại:
- Remove Shotgun Surgery, khi mà một method method trả về nil, thì các method khác phải thực hiện check nil, khi thay đổi method này thì kéo theo sự thay đổi của nhiều method khác.
- Remove Duplicated Code khi phải thực hiện check nil nhiều lần
- Code dễ đọc hơn
- Replace các đoạn conditional statement phức tạp bằng simple command, tuân theo nguyên tắc Tell, Don't ask