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