[Clean Code] Replace Conditional with Polymorphism
Bài đăng này đã không được cập nhật trong 6 năm
Khi học bất cứ một ngôn ngữ hay là một framework nào đó, Developers chúng ta thường học những cú pháp đầu tiên, một trong những cú pháp mà bất cứ ngôn ngữ hay framework nào cũng có đó là câu điều kiện (Conditional Statement
). Không quá khó để bắt gặp những đoạn code có conditional statement
phức tạp trong bất kì ứng dụng nào đó.
Tuy nhiên, khi mà hiểu rõ ngôn ngữ hay framework đó thì chúng ta sẽ dần dần nhận ra một số điều:
- Nhìn vào các đoạn
conditional statement
làm code loạn cả lên, xấu xí. - Khó dùng lại
- Khó tách các đoạn
conditional statement
, dễ làm code phình to.
Các ngôn ngữ lập trình như Ruby
, chúng ta có thể sử dụng polymorphism
để tránh những đoạn conditional statement
lặp đi lặp lại trong ứng dụng. Thay vì những đoạn if/else
hay case/when
rối rắm thì bạn có thể implement những đoạn code đó trong các class khác nhau, chúng ta thêm hoặc sử dụng lại các class cho từng trường hợp trong conditional statement
.
Việc thay thể conditional statement
bằng Polymorphism
giúp chúng ta move các đoạn xử lý vào những nơi hợp lý nhất trong ứng dụng. Các class này sẽ không cần phải thay đổi trong tương lai khi mà ứng dụng phải thay đổi.
Examples
Chúng ta có một Question
model, giả sử rằng có 3 loại question
khác nhau: Open Question
, MultipleChoice Question
, Scale Question
. Chúng ta sẽ sử dụng một column tên là question_type
để quy định loại của một question
.
rails g model Question title question_type maximum:integer minimum:integer
class Quesion < ApplicationRecord
SUBMITITABLE_TYPES = %w(Open MultipleChoice Scale).freeze
validates :maximum, presence: true, if: :scale?
validates :minimum, presence: true, if: :scale?
validates :question_type, presence: true, inclusion: SUBMITITABLE_TYPES
validates :title, presence: true
def summary
case question_type
when "MultipleChoice"
summarize_multiple_choice_answers
when "Open"
summarize_open_answers
when "Scale"
summarize_scale_answers
end
end
def steps
(minimum..maximum).to_a
end
private
def scale?
question_type == "Scale"
end
def summarize_multiple_choice_answers
"Multiple Choice Answer"
end
def summarize_open_answers
"Open Answer"
end
def summarize_scale_answers
"Scale Answer"
end
end
Chúng ta có thể thấy những issues của method summary
trên:
- Sẽ ra sao nếu chúng ta muốn thêm 1 loại
question
mới? Code trên sẽ bị thay đổi. - Tất cả các logic và data có
summary
để nằm trong 1 class duy nhất:Question
, nó sẽ làm cho class này phình to hơn mức quy định. - Đây là chỉ mới trong model, trong ứng dụng chắc chắn sẽ còn rất nhiều đoạn
conditional statement
khác kiểu này để kiểm tra type củaquestion
. Khi thêm một loạiQuestion
mới thì code sẽ phải thay đổi rất nhiều.
Có rất nhiều cách để refactor
lại class trên bằng Polymorphism, trong bài viết này mình sẽ nói về cách implement bằng sử dụng subclasses
, đây là phương pháp đơn giản nhất.
Replace Type Code with Subclasses
Rails
cung cấp cho chúng ta một công cụ để xử lý trong trường hợp này đó là Single Table Inheritance
, bằng cách này, chúng ta sẽ tạo ra các subclass
của Question
, mặc dù là các class khác nhau, nhưng chúng đều được lưu vào Database
bằng 1 table duy nhất: questions
Migration
Khi thực hiện STI
, Rails
sẽ ngầm định là model đó có attributes type
[1] nên chúng ta sẽ thực hiện rename column question_type
thành type
.
class ChangeColumnQuestionTypeToTypeToQuestions < ActiveRecord::Migration[5.1]
def change
change_column :questions, :question_type, :type
end
end
Với 3 loại Question
trên ta tạo ra 3 classes mới kế thừa từ Question
# models/multiple_choice_question.rb
class MultipleChoice < Question
end
# models/open_question.rb
class Open < Question
end
#models/scale_question.rb
class Scale < Question
end
Refactor lại class Question
như sau:
--- a/app/models/question.rb
+++ b/app/models/question.rb
@@ -7,7 +7,7 @@ class Question < ApplicationRecord
validates :title, presence: true
def summary
- case question_type
+ case type
when "MultipleChoice"
summarize_multiple_choice_answers
when "Open"
@@ -23,7 +23,7 @@ class Question < ApplicationRecord
private
def scale?
- question_type == "Scale"
+ type == "Scale"
end
def summarize_multiple_choice_answers
Bây giờ, khi chúng ta tạo các instance của subclasses
thì Rails
sẽ tự động tạo ra một record ở database với type tương ứng.
2.4.1 :002 > a = MultipleChoice.create title: "Are you happy now?"
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "questions" ("title", "type", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "Are you happy now?"], ["type", "MultipleChoice"], ["created_at", "2018-02-12 07:52:49.018933"], ["updated_at", "2018-02-12 07:52:49.018933"]]
(3.9ms) commit transaction
=> #<MultipleChoice id: 1, title: "Are you happy now?", type: "MultipleChoice", maximum: nil, minimum: nil, created_at: "2018-02-12 07:52:49", updated_at: "2018-02-12 07:52:49">
2.4.1 :003 > a.summary
=> "Multiple Choice Answer"
Tiếp theo, chúng ta move summary
về subclass
:
diff --git a/app/models/question.rb b/app/models/question.rb
index 0aa4791..8c0396c 100644
--- a/app/models/question.rb
+++ b/app/models/question.rb
@@ -6,35 +6,12 @@ class Question < ApplicationRecord
validates :type, presence: true, inclusion: SUBMITITABLE_TYPES
validates :title, presence: true
- def summary
- case question_type
- when "MultipleChoice"
- summarize_multiple_choice_answers
- when "Open"
- summarize_open_answers
- when "Scale"
- summarize_scale_answers
- end
- end
-
def steps
(minimum..maximum).to_a
end
private
def scale?
type == "Scale"
end
-
- def summarize_multiple_choice_answers
- "Multiple Choice Answer"
- end
-
- def summarize_open_answers
- "Open Answer"
- end
-
- def summarize_scale_answers
- "Scale Answer"
- end
end
class Scale < Question
+ def summary
+ "Scale Answer"
+ end
end
class MultipleChoice < Question
+ def summary
+ "Multiple Choice Answer"
+ end
end
class Open < Question
+ def summary
+ "Open Answer"
+ end
end
Như vậy, chúng ta đã remove những conditional statement
phức tạp bằng những subclass đơn giản, không còn những logic dài dòng.
Giờ đây, mỗi khi phải thêm một loại Question
mới, chúng ta chỉ cần tạo mới một class kế thừa từ Question
, implement lại summary
mà không phải thay đổi gì đến những file khác.
To be continued...
All rights reserved