今天要介紹的 Pattern 是 Template Pattern。個人覺得在 Design Patterns 中,Template Pattern 大概是數一數二常用卻不自知的 Pattern 了。
在軟體開發中,很常出現一個情形,今天開發了一個功能 A,下次被要求開發一個很類似的功能 B。例如,你今天做了一個報表輸出成 JSON 格式的功能,可是明天被要求支援輸出成 HTML 格式。這時候有多種選擇,可以簡單地複製功能 A 的程式碼,然後修改成功能 B,或是使用今天要介紹的 Template Pattern。
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 的方式,我們會把 JsonReport
跟 HtmlReport
共通的部分獨立出來變成 BaseReport
,然後 JsonReport
和 HtmlReport
繼承 BaseReport
,再把彼此差異的地方,放在 JsonReport
或 HtmlReport
中。
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
pronto
是一個自動化 code review 的 Ruby 套件,為了要支援在不同的平台(GitHub, Gitlab, Bitbucket)都可以留下 code review 評論, pronto
就使用 Template Pattern 的概念來處理。
以下是 pronto
的部份的程式碼,format()
就是一個 Template Method,裡面的 client_module()
和 pretty_name()
則是由相對應的 GithubFormatter
, GitlabFormatter
,與 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 可以讓你的程式碼比較 DRY (Don't Repeat Yourself),另外因為一個大功能已經被拆成許多小步驟,修改小步驟通常也比直接修改一個大功能來的安全。
但是使用 Template Pattern 也會讓你受制於 base class 所定下的設計,因而失去了一些彈性。另外使用 Template Pattern 的時候,要小心不要為了重用部分的功能,而把不太相關的 class 硬湊在一起,不正確的抽象化常常會讓程式碼變得很難維護。
作者:Maso
(下篇預告:Strategy 策略模式)