12/08/2018, 17:02

[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.

To be continued...

0