iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 9
2
Software Development

什麼?又是/不只是 Design Patterns!?系列 第 9

[Design Pattern] Template 模板模式

今天要介紹的 Pattern 是 Template Pattern。個人覺得在 Design Patterns 中,Template Pattern 大概是數一數二常用卻不自知的 Pattern 了。

在軟體開發中,很常出現一個情形,今天開發了一個功能 A,下次被要求開發一個很類似的功能 B。例如,你今天做了一個報表輸出成 JSON 格式的功能,可是明天被要求支援輸出成 HTML 格式。這時候有多種選擇,可以簡單地複製功能 A 的程式碼,然後修改成功能 B,或是使用今天要介紹的 Template Pattern。

template UML

Template pattern的概念是把一個功能拆成多個小步驟,每個小步驟就是一個method,然後用一個template method把這些小步驟組合起來變成一個功能。其中的小步驟可以是沒有具體的內容,也可以是已經有預設行為的。然後把這些小步驟跟template method放到一個base class,然後再去創造sub class來繼承base class並改寫需要的小步驟。

因此 Template Pattern 特別適合用在多個相似的類別(class),彼此之間有共同的架構與邏輯,但卻有微小的差異。

簡化的例子

使用 Ruby,以報表輸出為例,我們原本的程式碼可能像這樣。

require 'json'

class JsonReport
  def initialize(title, body, footnote)
    @title = title
    @body = body
    @footnote = footnote
  end

  def print_report
    puts "This report is printed on #{Time.now.strftime('%Y-%m-%d')}"
    puts JSON.dump({title: @title,
                    body: @body,
                    footnote: @footnote})
  end
end

現在我們被要求加入支援 HTML 格式的功能,如果要使用 Template Pattern 的方式,我們會把 JsonReportHtmlReport 共通的部分獨立出來變成 BaseReport,然後 JsonReportHtmlReport 繼承 BaseReport ,再把彼此差異的地方,放在 JsonReportHtmlReport 中。

template UML

require 'json'

class BaseReport
  def initialize(title, body, footnote)
    @title = title
    @body = body
    @footnote = footnote
  end

  def print_report
    print_timestamp()
    print_author()
    print_contet()
  end

  private

  def print_timestamp
    puts "This report is printed on #{Time.now.strftime('%Y-%m-%d')}."
  end

  def print_author
    puts "This report is made by PicCollage."
  end

  def print_contet
    puts content
  end

  def content
    raise NotImplementedError
  end
end

class JsonReport < BaseReport
  private

  def content
    JSON.dump({title: @title,
               body: @body,
               footnote: @footnote})
  end
end

class HtmlReport < BaseReport
  private

  def content
    <<~HTML_CONTENT
      <!DOCTYPE html>
      <html>
        <head>
          <title>#{@title}</title>
        </head>
        <body>
          <p>#{@body}</p>
          <footer>
            <p>#{@footnote}</p>
          </footer>
        </body>
      </html>
    HTML_CONTENT
  end
end

Template Pattern 的真實案例

pronto 是一個自動化 code review 的 Ruby 套件,為了要支援在不同的平台(GitHub, Gitlab, Bitbucket)都可以留下 code review 評論, pronto 就使用 Template Pattern 的概念來處理。

以下是 pronto 的部份的程式碼,format() 就是一個 Template Method,裡面的 client_module()pretty_name() 則是由相對應的 GithubFormatterGitlabFormatter,與 BitbucketFormatter 來處理。

module Pronto
  module Formatter
    class GitFormatter < Base
      def format(messages, repo, patches)
        client = client_module.new(repo)
        existing = existing_comments(messages, client, repo)
        comments = new_comments(messages, patches)
        additions = remove_duplicate_comments(existing, comments)
        submit_comments(client, additions)

        approve_pull_request(comments.count, additions.count, client) if defined?(self.approve_pull_request)

        "#{additions.count} Pronto messages posted to #{pretty_name}"
      end

      def client_module
        raise NotImplementedError
      end

      def pretty_name
        raise NotImplementedError
      end

      #....
    end
  end
end
module Pronto
  module Formatter
    class GithubFormatter < CommitFormatter
      def client_module
        Github
      end

      def pretty_name
        'GitHub'
      end

      def line_number(message, _)
        message.line.commit_line.position if message.line
      end
    end
  end
end
module Pronto
  module Formatter
    class GitlabFormatter < CommitFormatter
      def client_module
        Gitlab
      end

      def pretty_name
        'GitLab'
      end

      def line_number(message, _)
        message.line.commit_line.new_lineno if message.line
      end
    end
  end
end
module Pronto
  module Formatter
    class BitbucketFormatter < CommitFormatter
      def client_module
        Bitbucket
      end

      def pretty_name
        'BitBucket'
      end

      def line_number(message, _)
        message.line.new_lineno if message.line
      end
    end
  end
end

Template Pattern 的優缺點

Template Pattern 可以讓你的程式碼比較 DRY (Don't Repeat Yourself),另外因為一個大功能已經被拆成許多小步驟,修改小步驟通常也比直接修改一個大功能來的安全。

但是使用 Template Pattern 也會讓你受制於 base class 所定下的設計,因而失去了一些彈性。另外使用 Template Pattern 的時候,要小心不要為了重用部分的功能,而把不太相關的 class 硬湊在一起,不正確的抽象化常常會讓程式碼變得很難維護。

作者:Maso

(下篇預告:Strategy 策略模式)


上一篇
[Design Pattern] Adapter 配接器模式
下一篇
[Design Pattern] Strategy 策略模式
系列文
什麼?又是/不只是 Design Patterns!?32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言