[Clean Code] Replace Conditional with Polymorphism
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 ...
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
https://gist.githubusercontent.com/namtx/89adb5f3b8bed95abd10ca6fa373bcb1/raw/87f3ac14c8406872874a42f1ec8b67a1595a1b5d/question.rb
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ủa question. Khi thêm một loại Question 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.