11/08/2018, 23:29

Template Method Pattern trong Ruby

Hãy tưởng tượng bạn có một đoạn code phức tạp, có thể đó là một thuật toán, hoặc mã hệ thống, hay cũng có thể là đoạn mã đủ khó mà bạn chỉ muốn code 1 lần. Vấn đề ở đây là chỉ có phần chính giữa của đoạn code phức tạp là thay đổi. -> Làm sao để giải quyết mà không phải thay đổi đoạn code quá ...

Hãy tưởng tượng bạn có một đoạn code phức tạp, có thể đó là một thuật toán, hoặc mã hệ thống, hay cũng có thể là đoạn mã đủ khó mà bạn chỉ muốn code 1 lần. Vấn đề ở đây là chỉ có phần chính giữa của đoạn code phức tạp là thay đổi.

-> Làm sao để giải quyết mà không phải thay đổi đoạn code quá nhiều trong tương lai

Để cụ thể hơn, chúng ta sẽ xây dựng một chương trình xuất báo cáo như sau:

class Report
  def initialize
    @title = 'Monthly Report'
    @text = [ 'Things are going', 'really, really well.' ]
  end

  def output_report
    puts('<html>')
    puts(' <head>')
    puts(" <title>#{@title}</title>")
    puts(' </head>')
    puts(' <body>')
    @text.each do |line|
      puts(" <p>#{line}</p>" )
    end
    puts(' </body>')
    puts('</html>')
  end
end

Ở đây báo cáo được xuất theo định dạng HTML với các thẻ được gắn cố định. Đoạn mã này sẽ xử lý tốt nếu chúng ta muốn xuất báo cáo định dạng HTML, tuy nhiên nếu chúng ta muốn thay đổi định dạng xuất báo cáo thì đoạn mã này không thể dùng lại được. Vậy cách xử lý ở đây là gì? Điều đâu tiên chúng ta nghĩ tới và đơn giản nhất là xử lý bằngIF ELSE

class Report
  def initialize
   @title = 'Monthly Report'
   @text = ['Things are going', 'really, really well.']
  end
  def output_report(format)
    if format == :plain
      puts("*** #{@title} ***")
    elsif format == :html
      puts('<html>')
      puts(' <head>')
      puts(" <title>#{@title}</title>")
      puts(' </head>')
      puts(' <body>')
    else
      raise "Unknown format: #{format}"
    end
    @text.each do |line|
      if format == :plain
        puts(line)
      else
        puts(" <p>#{line}</p>" )
      end
    end
    if format == :html
      puts(' </body>')
      puts('</html>')
    end
  end
end

Việc dùng IF ELSE sẽ khiến đoạn code của chúng ta dài dòng và có nguy cơ sửa chữa nhiều khi phải thêm 1 đoạn xử lý cho định dạng report mới. Việc sửa chữa code đã vi phạm quy tắc thiết kế design pattern.

Áp dụng quy tắc đầu tiên Separate the Things That Stay the Same

Để ý đoạn code report, chúng ta có thể thấy dù định dạng xuất khác nhau nhưng về cơ bản format template của report vẫn giống nhau:

  1. header information
  2. title
  3. từng line của report
  4. trailing stuff

Với mỗi 1 phân đoạn của report, chúng ta có thể tách ra thành các function của class abstract như sau:

class Report
  def initialize
    @title = 'Monthly Report'
    @text = ['Things are going', 'really, really well.']
  end
  def output_report
    output_start
    output_head
    output_body_start
    output_body
    output_body_end
    output_end
  end
  def output_body
    @text.each do |line|
      output_line(line)
    end
  end
  def output_start
    raise 'Called abstract method: output_start'
  end
  def output_head
    raise 'Called abstract method: output_head'
  end
  def output_body_start
    raise 'Called abstract method: output_body_start'
  end
  def output_line(line)
    raise 'Called abstract method: output_line'
  end
  def output_body_end
    raise 'Called abstract method: output_body_end'
  end
  def output_end
    raise 'Called abstract method: output_end'
  end
end

Từ đây với mỗi loại định dạng xuất report khác nhau, chúng ta có thể dễ dạng tạo thêm 1 subclass tương ứng với mỗi định dạng từ class abstract.

#HTML FORMAT
class HTMLReport < Report
  def output_start
    puts('<html>')
  end
  def output_head
    puts(' <head>')
    puts(" <title>#{@title}</title>")
    puts(' </head>')
  end
  def output_body_start
    puts('<body>')
  end
  def output_line(line)
    puts(" <p>#{line}</p>")
  end
  def output_body_end
    puts('</body>')
  end
  def output_end
    puts('</html>')
  end
end

#Plain text Format
class PlainTextReport < Report
  def output_start
  end
  def output_head
   puts("**** #{@title} ****")
   puts
  end
  def output_body_start
  end
  def output_line(line)
    puts(line)
  end
  def output_body_end
  end
  def output_end
  end
end

Cách xử lý trên thực chất chính là Template Method Pattern

Ý tưởng của pattern này là xây dựng một class base với 1 method xương sống (còn được gọi là template method). Method này sẽ điều hướng các bộ xử lý bằng cách gọi các methods abtract, các method này sẽ được cài đặt lại ở subclass.

Template Method Pattern

Tham khảo

Github (updating):https://github.com/ducnhat1989/design-patterns-in-ruby

Sách: “DESIGN PATTERNS IN RUBY” của tác giả Russ Olsen

: CÁC NGUYÊN TẮC TRONG DESIGN PATTERN

0