開發 App 時,「只要加一個復原按鈕」這類需求出現的頻率,往往比想像中更高。
實際加上 undo 後,程式碼很容易變成義大利麵。因為只為了一個按鈕,就得把復原邏輯散落在刪除、移動與修改程式碼的各處。
整理這些問題的正是Swift 命令模式。
本文會說明命令模式為何能成為復原功能的骨架,並帶你掌握實際使用的最小程式碼結構。重點不是複雜理論,而是「能立即套用到專案中的形式」。
什麼是命令模式?一句話總結
先用一句話整理。
命令模式會將「執行某件事」這個動作本身封裝成物件,把執行與復原綁在同一個單位中。
按下按鈕時不直接執行邏輯,而是建立一個寫著「執行這個動作」的命令(Command 物件)。
命令中同時包含執行方式(execute)與復原方式(undo)。
整理如下:
- 將動作建立成物件——刪除、移動、輸入各自是一個命令
- 每個命令成對擁有 execute() 與 undo()
- 將執行過的命令堆疊到 stack
- 從 stack 取出一個命令並呼叫 undo(),復原就完成
這種結構能讓復原邏輯集中在各命令中,而不是散落在程式碼各處。
從 Swift 的命令協定開始實作
首先定義所有命令都必須遵守的約定,也就是協定。
以下是強制執行與復原的最小骨架。
protocol Command {
func execute() // 執行動作
func undo() // 復原執行
}
只要這兩個方法就夠了。
接著建立一個包含實際動作的具體命令。例如,新增文字的命令。
以下程式碼會「加上文字,復原時再將它移除」。
final class AddTextCommand: Command {
private let document: Document
private let text: String
init(document: Document, text: String) {
self.document = document
self.text = text
}
func execute() { document.content += text }
func undo() { document.content.removeLast(text.count) }
}
execute 會加上文字,undo 則移除新增的部分。執行與復原集中在同一個物件中,就是這個模式的全部。
復原的關鍵是堆疊
建立命令後,還需要一個管理它們的「管理者」,通常稱為 Invoker。
管理者會將執行過的命令依序堆疊到 stack 中。
按下復原時,取出最後加入的命令並呼叫 undo()。先復原最後執行的動作,正是復原的自然順序。
以下是管理者的骨架。
final class CommandManager {
private var undoStack: [Command] = []
func run(_ command: Command) {
command.execute()
undoStack.append(command) // 執行後儲存至 stack
}
func undo() {
guard let last = undoStack.popLast() else { return }
last.undo() // 從最後一個開始復原
}
}
現在的使用方式變得非常簡單。
在 buttonTapped 呼叫 manager.run(command),在復原按鈕中只要呼叫 manager.undo() 即可。
復原順序與 stack 管理由 CommandManager 自動處理,因此畫面程式碼能維持整潔。
如果還需要重做(redo),只要將復原過的命令另外放進 redoStack 即可。由於結構對稱,擴充並不困難。
命令模式何時適合使用,何時該避免?
命令模式也不是萬能解。以下整理了適合與過度使用的情況。
| 情況 | 命令模式 |
|---|---|
| 需要復原與重做 | 非常適合 |
| 必須保留操作記錄(歷史) | 適合 |
| 將動作放入佇列稍後執行 | 適合 |
| 不需要復原的單純按鈕 | 過度設計,直接用函式 |
命令模式在需要「復原」或「記錄」時最能發揮價值。繪圖軟體、文字編輯器、付款取消,以及遊戲中的復原功能,都是代表性案例。
相反地,若只是替畫面導覽的單一按鈕套用命令模式,反而只會增加程式碼。
把所有按鈕都做成命令,很容易增加大量檔案,因此只應套用在需要復原的動作上。
總結
復原功能乍看難以加入,但只要先建立命令模式這個骨架,就能比想像中更整潔地運作。
先將今天整理的協定、具體命令與管理器三個元件套用到小型畫面。熟悉結構後,重做與歷史記錄等擴充也會自然完成。現在就放心地為你的 App 加上一個復原按鈕吧。

