# 13設計模式
### 適配器模式
### 問題
想象你去國外旅行,一旦你意識到你的電源線插座與酒店房間墻上的插座不兼容時,幸運的是你記得帶你的電源適配器。它將一邊連接你的電源線插座另一邊連接墻壁插座,允許它們之間進行通信。
同樣的情況也可能會出現在代碼中,當兩個 ( 或更多 ) 實例 ( 類、模塊等 ) 想跟對方通信,但其通信協議 ( 例如,他們所使用的語言交流 ) 不同。在這種情況下,[Adapter](http://en.wikipedia.org/wiki/Adapter_pattern) 模式更方便。它會充當翻譯,從一邊到另一邊。
### 解決方案
~~~
# a fragment of 3-rd party grid component
?
class AwesomeGrid
constructor: (@datasource)->
@sort_order = 'ASC'
@sorter = new NullSorter # in this place we use NullObject pattern (another useful pattern)
setCustomSorter: (@customSorter) ->
@sorter = customSorter
sort: () ->
@datasource = @sorter.sort @datasource, @sort_order
# don't forget to change sort order
?
?
class NullSorter
sort: (data, order) -> # do nothing; it is just a stub
?
class RandomSorter
sort: (data)->
for i in [data.length-1..1] #let's shuffle the data a bit
j = Math.floor Math.random() * (i + 1)
[data[i], data[j]] = [data[j], data[i]]
return data
?
class RandomSorterAdapter
constructor: (@sorter) ->
sort: (data, order) ->
@sorter.sort data
?
agrid = new AwesomeGrid ['a','b','c','d','e','f']
agrid.setCustomSorter new RandomSorterAdapter(new RandomSorter)
agrid.sort() # sort data with custom sorter through adapter
~~~
### 討論
當你要組織兩個具有不同接口的對象之間的交互時,適配器是有用的。它可以當你使用第三方庫或者使用遺留代碼時使用。在任何情況下小心使用適配器:它可以是有用的,但它也可以導致設計錯誤。
### 橋接模式
### 問題
你需要為代碼保持一個可靠的接口,可以經常變化或者在多種實現間轉換。
### 解決方案
使用橋接模式作為不同的實現和剩余代碼的中間體。
假設你開發了一個瀏覽器的文本編輯器保存到云。然而,現在你需要通過獨立客戶端的端口將其在本地保存。
~~~
class TextSaver
constructor: (@filename, @options) ->
save: (data) ->
?
class CloudSaver extends TextSaver
constructor: (@filename, @options) ->
super @filename, @options
save: (data) ->
# Assuming jQuery
# Note the fat arrows
$( =>
$.post "#{@options.url}/#{@filename}", data, =>
alert "Saved '#{data}' to #{@filename} at #{@options.url}."
)
?
class FileSaver extends TextSaver
constructor: (@filename, @options) ->
super @filename, @options
@fs = require 'fs'
save: (data) ->
@fs.writeFile @filename, data, (err) => # Note the fat arrow
if err? then console.log err
else console.log "Saved '#{data}' to #{@filename} in #{@options.directory}."
?
filename = "temp.txt"
data = "Example data"
?
saver = if window?
new CloudSaver filename, url: 'http://localhost' # => Saved "Example data" to temp.txt at http://localhost
else if root?
new FileSaver filename, directory: './' # => Saved "Example data" to temp.txt in ./
?
saver.save data
~~~
### 討論
橋接模式可以幫助你將特定實現的代碼置于看不見的地方,這樣你就可以專注于你的程序中的具體代碼。在上面的示例中,應用程序的其余部分可以稱為 saver.save data ,不考慮文件的最終結束。
### 生成器模式
### 問題
你需要準備一個復雜的、多部分的對象,你希望操作不止一次或有不同的配置。
### 解決方案
創建一個生成器封裝對象的產生過程。
[Todo.txt](http://todotxt.com/) 格式提供了一個先進的但還是純文本的方法來維護待辦事項列表。手工輸入每個項目有損耗且容易出錯,然而 TodoTxtBuilder 類可以解決我們的麻煩:
~~~
class TodoTxtBuilder
constructor: (defaultParameters={ }) ->
@date = new Date(defaultParameters.date) or new Date
@contexts = defaultParameters.contexts or [ ]
@projects = defaultParameters.projects or [ ]
@priority = defaultParameters.priority or undefined
newTodo: (description, parameters={ }) ->
date = (parameters.date and new Date(parameters.date)) or @date
contexts = @contexts.concat(parameters.contexts or [ ])
projects = @projects.concat(parameters.projects or [ ])
priorityLevel = parameters.priority or @priority
createdAt = [date.getFullYear(), date.getMonth()+1, date.getDate()].join("-")
contextNames = ("@#{context}" for context in contexts when context).join(" ")
projectNames = ("+#{project}" for project in projects when project).join(" ")
priority = if priorityLevel then "(#{priorityLevel})" else ""
todoParts = [priority, createdAt, description, contextNames, projectNames]
(part for part in todoParts when part.length > 0).join " "
?
builder = new TodoTxtBuilder(date: "10/13/2011")
?
builder.newTodo "Wash laundry"
?
# => '2011-10-13 Wash laundry'
?
?
workBuilder = new TodoTxtBuilder(date: "10/13/2011", contexts: ["work"])
?
workBuilder.newTodo "Show the new design pattern to Lucy", contexts: ["desk", "xpSession"]
?
# => '2011-10-13 Show the new design pattern to Lucy @work @desk @xpSession'
?
?
workBuilder.newTodo "Remind Sean about the failing unit tests", contexts: ["meeting"], projects: ["compilerRefactor"], priority: 'A'
?
# => '(A) 2011-10-13 Remind Sean about the failing unit tests @work @meeting +compilerRefactor'
~~~
### 討論
TodoTxtBuilder 類負責所有文本的生成,讓程序員關注每個工作項的獨特元素。此外,命令行工具或 GUI 可以插入這個代碼且之后仍然保持支持,提供輕松、更高版本的格式。
### 前期建設
并不是每次創建一個新的實例所需的對象都要從頭開始,我們將負擔轉移到一個單獨的對象,可以在對象創建過程中進行調整。
~~~
builder = new TodoTxtBuilder(date: "10/13/2011")
?
builder.newTodo "Order new netbook"
?
# => '2011-10-13 Order new netbook'
?
?
builder.projects.push "summerVacation"
?
builder.newTodo "Buy suntan lotion"
?
# => '2011-10-13 Buy suntan lotion +summerVacation'
?
?
builder.contexts.push "phone"
?
builder.newTodo "Order tickets"
?
# => '2011-10-13 Order tickets @phone +summerVacation'
?
?
delete builder.contexts[0]
?
builder.newTodo "Fill gas tank"
?
# => '2011-10-13 Fill gas tank +summerVacation'
~~~
### 練習
-
擴大 project- 和 context-tag 生成代碼來過濾掉重復的條目。
-
一些 Todo.txt 用戶喜歡在任務描述中插入項目和上下文的標簽。添加代碼來識別這些標簽和過濾器的結束標記。
### 命令模式
### 問題
你需要讓另一個對象處理你自己的可執行的代碼。
### 解決方案
使用 [Command pattern](http://en.wikipedia.org/wiki/Command_pattern) 傳遞函數的引用。
~~~
# Using a private variable to simulate external scripts or modules
?
incrementers = (() ->
privateVar = 0
?
singleIncrementer = () ->
privateVar += 1
?
doubleIncrementer = () ->
privateVar += 2
?
commands =
single: singleIncrementer
double: doubleIncrementer
value: -> privateVar
)()
?
class RunsAll
constructor: (@commands...) ->
run: -> command() for command in @commands
?
runner = new RunsAll(incrementers.single, incrementers.double, incrementers.single, incrementers.double)
runner.run()
incrementers.value() # => 6
~~~
### 討論
以函數作為一級的對象且從 Javascript 函數的變量范圍中繼承,CoffeeScript 使語言模式幾乎看不出來。事實上,任何函數傳遞回調函數可以作為一個*命令*。
jqXHR 對象返回 jQuery AJAX 方法使用此模式。
~~~
jqxhr = $.ajax
url: "/"
?
logMessages = ""
?
jqxhr.success -> logMessages += "Success!\n"
jqxhr.error -> logMessages += "Error!\n"
jqxhr.complete -> logMessages += "Completed!\n"
?
# On a valid AJAX request:
?
# logMessages == "Success!\nCompleted!\n"
~~~
### 修飾模式
### 問題
你有一組數據,需要在多個過程、可能變換的方式下處理。
### 解決方案
使用修飾模式來構造如何更改應用。
~~~
miniMarkdown = (line) ->
if match = line.match /^(#+)\s*(.*)$/
headerLevel = match[1].length
headerText = match[2]
"<h#{headerLevel}>#{headerText}</h#{headerLevel}>"
else
if line.length > 0
"<p>#{line}</p>"
else
''
?
stripComments = (line) ->
line.replace /\s*\/\/.*$/, '' # Removes one-line, double-slash C-style comments
?
class TextProcessor
constructor: (@processors) ->
?
reducer: (existing, processor) ->
if processor
processor(existing or '')
else
existing
processLine: (text) ->
@processors.reduce @reducer, text
processString: (text) ->
(@processLine(line) for line in text.split("\n")).join("\n")
?
exampleText = '''
# A level 1 header
A regular line
// a comment
## A level 2 header
A line // with a comment
'''
?
processor = new TextProcessor [stripComments, miniMarkdown]
?
processor.processString exampleText
?
# => "<h1>A level 1 header</h1>\n<p>A regular line</p>\n\n<h2>A level 2 header</h2>\n<p>A line</p>"
~~~
### 結果
~~~
<h1>A level 1 header</h1>
<p>A regular line</p>
?
<h2>A level 1 header</h2>
<p>A line</p>
~~~
### 討論
TextProcessor 服務有修飾的作用,可將個人、專業文本處理器綁定在一起。這使 miniMarkdown 和 stripComments 組件只專注于處理一行文本。未來的開發人員只需要編寫函數返回一個字符串,并將它添加到陣列的處理器即可。
我們甚至可以修改現有的修飾對象動態:
~~~
smilies =
':)' : "smile"
':D' : "huge_grin"
':(' : "frown"
';)' : "wink"
?
smilieExpander = (line) ->
if line
(line = line.replace symbol, "<img src='#{text}.png' alt='#{text}' />") for symbol, text of smilies
line
?
processor.processors.unshift smilieExpander
?
processor.processString "# A header that makes you :) // you may even laugh"
?
# => "<h1>A header that makes you <img src='smile.png' alt='smile' /></h1>"
?
?
processor.processors.shift()
?
# => "<h1>A header that makes you :)</h1>"
~~~
### 工廠方法模式
### 問題
直到開始運行你才知道需要的是什么種類的對象。
### 解決方案
使用 [工廠方法(Factory Method)](http://en.wikipedia.org/wiki/Factory_method_pattern) 模式和選擇對象都是動態生成的。
你需要將一個文件加載到編輯器,但是直到用戶選擇文件時你才知道它的格式。一個類使用[工廠方法 ( Factory Method )](http://en.wikipedia.org/wiki/Factory_method_pattern) 模式可以根據文件的擴展名提供不同的解析器。
~~~
class HTMLParser
constructor: ->
@type = "HTML parser"
class MarkdownParser
constructor: ->
@type = "Markdown parser"
class JSONParser
constructor: ->
@type = "JSON parser"
?
class ParserFactory
makeParser: (filename) ->
matches = filename.match /\.(\w*)$/
extension = matches[1]
switch extension
when "html" then new HTMLParser
when "htm" then new HTMLParser
when "markdown" then new MarkdownParser
when "md" then new MarkdownParser
when "json" then new JSONParser
?
factory = new ParserFactory
?
factory.makeParser("example.html").type # => "HTML parser"
?
factory.makeParser("example.md").type # => "Markdown parser"
?
factory.makeParser("example.json").type # => "JSON parser"
~~~
### 討論
在這個示例中,你可以關注解析的內容,忽略細節文件的格式。更先進的工廠方法,例如,搜索版本控制文件中的數據本身,然后返回一個更精確的解析器(例如,返回一個 HTML5 解析器而不是 HTML v4 解析器)。
### 解釋器模式
### 問題
其他人需要以控制方式運行你的一部分代碼。相對地,你選擇的語言不能以一種簡潔的方式表達問題域。
### 解決方案
使用解釋器模式來創建一個你翻譯為特定代碼的領域特異性語言( domain-specific language )。
我們來做個假設,例如用戶希望在你的應用程序中執行數學運算。你可以讓他們正向運行代碼來演算指令(eval)但這會讓他們運行任意代碼。相反,你可以提供一個小型的“堆棧計算器(stack calculator)”語言,用來做單獨分析,以便只運行數學運算,同時報告更有用的錯誤信息。
~~~
class StackCalculator
parseString: (string) ->
@stack = [ ]
for token in string.split /\s+/
@parseToken token
?
if @stack.length > 1
throw "Not enough operators: numbers left over"
else
@stack[0]
?
parseToken: (token, lastNumber) ->
if isNaN parseFloat(token) # Assume that anything other than a number is an operator
@parseOperator token
else
@stack.push parseFloat(token)
?
parseOperator: (operator) ->
if @stack.length < 2
throw "Can't operate on a stack without at least 2 items"
?
right = @stack.pop()
left = @stack.pop()
?
result = switch operator
when "+" then left + right
when "-" then left - right
when "*" then left * right
when "/"
if right is 0
throw "Can't divide by 0"
else
left / right
else
throw "Unrecognized operator: #{operator}"
?
@stack.push result
?
calc = new StackCalculator
?
calc.parseString "5 5 +" # => { result: 10 }
?
calc.parseString "4.0 5.5 +" # => { result: 9.5 }
?
calc.parseString "5 5 + 5 5 + *" # => { result: 100 }
?
try
calc.parseString "5 0 /"
catch error
error # => "Can't divide by 0"
?
try
calc.parseString "5 -"
catch error
error # => "Can't operate on a stack without at least 2 items"
?
try
calc.parseString "5 5 5 -"
catch error
error # => "Not enough operators: numbers left over"
?
try
calc.parseString "5 5 5 foo"
catch error
error # => "Unrecognized operator: foo"
~~~
### 討論
作為一種替代編寫我們自己的解釋器的選擇,你可以將現有的 CoffeeScript 解釋器與更自然的(更容易理解的)表達自己的算法的正常方式相結合。
~~~
class Sandwich
constructor: (@customer, @bread='white', @toppings=[], @toasted=false)->
?
white = (sw) ->
sw.bread = 'white'
sw
?
wheat = (sw) ->
sw.bread = 'wheat'
sw
?
turkey = (sw) ->
sw.toppings.push 'turkey'
sw
?
ham = (sw) ->
sw.toppings.push 'ham'
sw
?
swiss = (sw) ->
sw.toppings.push 'swiss'
sw
?
mayo = (sw) ->
sw.toppings.push 'mayo'
sw
?
toasted = (sw) ->
sw.toasted = true
sw
?
sandwich = (customer) ->
new Sandwich customer
?
to = (customer) ->
customer
?
send = (sw) ->
toastedState = sw.toasted and 'a toasted' or 'an untoasted'
?
toppingState = ''
if sw.toppings.length > 0
if sw.toppings.length > 1
toppingState = " with #{sw.toppings[0..sw.toppings.length-2].join ', '} and #{sw.toppings[sw.toppings.length-1]}"
else
toppingState = " with #{sw.toppings[0]}"
"#{sw.customer} requested #{toastedState}, #{sw.bread} bread sandwich#{toppingState}"
?
send sandwich to 'Charlie' # => "Charlie requested an untoasted, white bread sandwich"
send turkey sandwich to 'Judy' # => "Judy requested an untoasted, white bread sandwich with turkey"
send toasted ham turkey sandwich to 'Rachel' # => "Rachel requested a toasted, white bread sandwich with turkey and ham"
send toasted turkey ham swiss sandwich to 'Matt' # => "Matt requested a toasted, white bread sandwich with swiss, ham and turkey"
~~~
這個實例可以允許功能層實現返回修改后的對象,從而外函數可以依次修改它。示例通過借用動詞和介詞的用法,把自然語法提供給結構,當被正確使用時,會像自然語句一樣結束。這樣,利用 CoffeeScript 語言技能和你現有的語言技能可以幫助你關于捕捉代碼的問題。
### 備忘錄模式
### 問題
你想預測對一個對象做出改變后的反應。
### 解決方案
使用備忘錄模式[(Memento Pattern)](http://en.wikipedia.org/wiki/Memento_pattern)來跟蹤一個對象的變化。使用這個模式的類會輸出一個存儲在其他地方的備忘錄對象。
如果你的應用程序可以讓用戶編輯文本文件,例如,他們可能想要撤銷上一個動作。你可以在用戶改變文件之前保存文件現有的狀態,然后回滾到上一個位置。
~~~
class PreserveableText
class Memento
constructor: (@text) ->
?
constructor: (@text) ->
save: (newText) ->
memento = new Memento @text
@text = newText
memento
restore: (memento) ->
@text = memento.text
?
pt = new PreserveableText "The original string"
pt.text # => "The original string"
?
memento = pt.save "A new string"
pt.text # => "A new string"
?
pt.save "Yet another string"
pt.text # => "Yet another string"
?
pt.restore memento
pt.text # => "The original string"
~~~
### 討論
備忘錄對象由 PreserveableText#save 返回,為了安全保護,分別地存儲著重要的狀態信息。你可以序列化備忘錄以便來保證硬盤中的“撤銷”緩沖或者是那些被編輯的圖片等數據密集型對象。
### 觀察者模式
### 問題
當一個事件發生時你不得不向一些對象發布公告。
### 解決方案
使用觀察者模式[(Observer Pattern)](http://en.wikipedia.org/wiki/Observer_pattern)。
~~~
class PostOffice
constructor: () ->
@subscribers = []
notifyNewItemReleased: (item) ->
subscriber.callback(item) for subscriber in @subscribers when subscriber.item is item
subscribe: (to, onNewItemReleased) ->
@subscribers.push {'item':to, 'callback':onNewItemReleased}
?
class MagazineSubscriber
onNewMagazine: (item) ->
alert "I've got new "+item
?
class NewspaperSubscriber
onNewNewspaper: (item) ->
alert "I've got new "+item
?
postOffice = new PostOffice()
sub1 = new MagazineSubscriber()
sub2 = new NewspaperSubscriber()
postOffice.subscribe "Mens Health", sub1.onNewMagazine
postOffice.subscribe "Times", sub2.onNewNewspaper
postOffice.notifyNewItemReleased "Times"
postOffice.notifyNewItemReleased "Mens Health"
~~~
### 討論
這里你有一個觀察者對象( PostOffice )和可觀察對象( MagazineSubscriber, NewspaperSubscriber )。為了通報發布新的周期性可觀察對象的事件,應該對 PostOffice 進行訂閱。每一個被訂閱的對象都存儲在 PostOffice 的內部訂閱數組中。當新的實體周期發布時每一個訂閱者都會收到通知。
### 單件模式
### 問題
許多時候你想要一個,并且只要一個類的實例。比如,你可能需要一個創建服務器資源的類,并且你想要保證使用一個對象就可以控制這些資源。但是使用時要小心,因為單件模式可以很容易被濫用來模擬不必要的全局變量。
### 解決方案
公有類只包含獲得一個實例的方法。實例被保存在該公共對象的閉包中,并且總是有返回值。
這很奏效因為 CoffeeScript 允許你在一個類的聲明中定義可執行的狀態。但是,因為大多數 CoffeeScript 編譯成一個 [IIFE](http://benalman.com/news/2010/11/immediately-invoked-function-expression/) 包,如果這個方式適合你,你就不需要在類的聲明中放置私有的類。之后的內容可能對開發模塊化代碼有所幫助,例如 [CommonJS](http://www.commonjs.org/)(Node.js)或 [Require.js](http://requirejs.org/) 中可見(見實例討論)。
~~~
class Singleton
# You can add statements inside the class definition
# which helps establish private scope (due to closures)
# instance is defined as null to force correct scope
instance = null
# Create a private class that we can initialize however
# defined inside this scope to force the use of the
# singleton class.
class PrivateClass
constructor: (@message) ->
echo: -> @message
# This is a static method used to either retrieve the
# instance or create a new one.
@get: (message) ->
instance ?= new PrivateClass(message)
?
a = Singleton.get "Hello A"
a.echo() # => "Hello A"
?
b = Singleton.get "Hello B"
b.echo() # => "Hello A"
?
Singleton.instance # => undefined
a.instance # => undefined
Singleton.PrivateClass # => undefined
~~~
### 討論
通過上面的實例我們可以看到,所有的實例是如何從同一個 Singleton 類的實例中輸出的。你也可以看到,私有類和實例變量都無法在 Singleton class 外被訪問到。 Singleton class 的本質是提供一個靜態方法得到只返回一個私有類的實例。它也對外界也隱藏私有類,因此你無法創建一個自己的私有類。
隱藏或使私有類在內部運作的想法是更受偏愛的。尤其是由于缺省的 CoffeeScript 將編譯的代碼封裝在自己的 IIFE(閉包)中,你可以定義類而無須擔心會被文件外部訪問到。在這個實例中,注意,用慣用的模塊導出特點來強調模塊中可被公共訪問的部分。(請看 “[導出到全局命名空間](http://stackoverflow.com/questions/4214731/coffeescript-global-variables)” 中對此理解更深入的討論)。
~~~
root = exports ? this
?
# Create a private class that we can initialize however
?
# defined inside the wrapper scope.
?
class ProtectedClass
constructor: (@message) ->
echo: -> @message
?
class Singleton
# You can add statements inside the class definition
# which helps establish private scope (due to closures)
# instance is defined as null to force correct scope
instance = null
# This is a static method used to either retrieve the
# instance or create a new one.
@get: (message) ->
instance ?= new ProtectedClass(message)
?
# Export Singleton as a module
?
root.Singleton = Singleton
~~~
我們可以注意到 coffeescript 是如此簡單地實現這個設計模式。為了更好地參考和討論 JavaScript 的實現,請看[初學者必備 JavaScript 設計模式](http://addyosmani.com/resources/essentialjsdesignpatterns/book/)。
### 策略模式
### 問題
解決問題的方式有多種,但是你需要在程序運行時選擇(或是轉換)這些方法。
### 解決方案
在策略對象(Strategy objects)中封裝你的算法。
例如,給定一個未排序的列表,我們可以在不同情況下改變排序算法。
#### 基類
~~~
StringSorter = (algorithm) ->
sort: (list) -> algorithm list
~~~
#### 策略
~~~
bubbleSort = (list) ->
anySwaps = false
swapPass = ->
for r in [0..list.length-2]
if list[r] > list[r+1]
anySwaps = true
[list[r], list[r+1]] = [list[r+1], list[r]]
?
swapPass()
while anySwaps
anySwaps = false
swapPass()
list
?
reverseBubbleSort = (list) ->
anySwaps = false
swapPass = ->
for r in [list.length-1..1]
if list[r] < list[r-1]
anySwaps = true
[list[r], list[r-1]] = [list[r-1], list[r]]
?
swapPass()
while anySwaps
anySwaps = false
swapPass()
list
~~~
#### 使用策略
~~~
sorter = new StringSorter bubbleSort
?
unsortedList = ['e', 'b', 'd', 'c', 'x', 'a']
?
sorter.sort unsortedList
?
# => ['a', 'b', 'c', 'd', 'e', 'x']
?
?
unsortedList.push 'w'
?
# => ['a', 'b', 'c', 'd', 'e', 'x', 'w']
?
?
sorter.algorithm = reverseBubbleSort
?
sorter.sort unsortedList
?
# => ['a', 'b', 'c', 'd', 'e', 'w', 'x']
~~~
### 討論
“沒有作戰計劃在第一次接觸敵人時便能存活下來。” 用戶如是,但是我們可以運用從變化的情況中獲得的知識來做出適應改變。在示例末尾,例如,數組中的最新項是亂序排列的,知道了這個細節,我們便可以通過切換算法來加速排序,只要簡單地重賦值就可以了。
### 練習
- 將 StringSorter 擴展為 AlwaysSortedArray 類來實現規則序列的所有功能,但是要基于插入方法自動分類新的項(例如 push 對比 shift)。
### 模板方法模式
### 問題
定義一個算法的結構,作為一系列的高層次的步驟,使每一個步驟的行為可以指定,使屬于一個族的算法都具有相同的結構但是有不同的行為。
### 解決方案
使用模板方法( Template Method )在父類中描述算法的結構,再授權一個或多個具體子類來具體地進行實現。
例如,想象你希望模擬各種類型的文件的生成,并且每個文件要包含一個標題和正文。
~~~
class Document
produceDocument: ->
@produceHeader()
@produceBody()
?
produceHeader: ->
produceBody: ->
?
class DocWithHeader extends Document
produceHeader: ->
console.log "Producing header for DocWithHeader"
?
produceBody: ->
console.log "Producing body for DocWithHeader"
?
class DocWithoutHeader extends Document
produceBody: ->
console.log "Producing body for DocWithoutHeader"
?
docs = [new DocWithHeader, new DocWithoutHeader]
doc.produceDocument() for doc in docs
~~~
### 討論
在這個實例中,算法用兩個步驟來描述文件的生成:其一是產生文件的標題,另一步是生成文件的正文。父類中是實現每一個步驟的空的方法,多態性使得每一個具體的子類可以通過重寫一步步的方法來實現對方法不同的利用。在本實例中,DocWithHeader 實現了正文和標題的步驟, DocWithoutHeader 只是實現了正文的步驟。
不同類型文件的生成就是簡單的將文檔對象存儲在一個數組中,簡單的遍歷每個文檔對象并調用其 produceDocument 方法的問題。