[CleanCode] Replace Conditional with Null Object
Bài đăng này đã không được cập nhật trong 7 năm
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 checknil, khi thay đổi method này thì kéo theo sự thay đổi của nhiều method khác. - Remove
Duplicated Codekhi phải thực hiện checknilnhiều lần - Code dễ đọc hơn
- Replace các đoạn
conditional statementphức tạp bằngsimple command, tuân theo nguyên tắcTell, Don't ask
All rights reserved