[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 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
    
                        <a href="https://gist.githubusercontent.com/namtx/89adb5f3b8bed95abd10ca6fa373bcb1/raw/87f3ac14c8406872874a42f1ec8b67a1595a1b5d/question.rb" target="_blank">https://gist.githubusercontent.com/namtx/89adb5f3b8bed95abd10ca6fa373bcb1/raw/87f3ac14c8406872874a42f1ec8b67a1595a1b5d/question.rb</a>
    
    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...

    Nguồn: Viblo



Có vẻ như bạn đã mất kết nối tới LaptrinhX, vui lòng đợi một lúc để chúng tôi thử kết nối lại.