軟體設計

使用 Swift 命令模式實作復原功能

開發 App 時,「只要加一個復原按鈕」這類需求出現的頻率,往往比想像中更高。

閱讀 4 分鐘
使用 Swift 命令模式實作復原功能 封面圖

開發 App 時,「只要加一個復原按鈕」這類需求出現的頻率,往往比想像中更高。

實際加上 undo 後,程式碼很容易變成義大利麵。因為只為了一個按鈕,就得把復原邏輯散落在刪除、移動與修改程式碼的各處。

整理這些問題的正是Swift 命令模式

本文會說明命令模式為何能成為復原功能的骨架,並帶你掌握實際使用的最小程式碼結構。重點不是複雜理論,而是「能立即套用到專案中的形式」。

什麼是命令模式?一句話總結

先用一句話整理。

命令模式會將「執行某件事」這個動作本身封裝成物件,把執行與復原綁在同一個單位中。

按下按鈕時不直接執行邏輯,而是建立一個寫著「執行這個動作」的命令(Command 物件)。

命令中同時包含執行方式(execute)與復原方式(undo)。

整理如下:

  1. 將動作建立成物件——刪除、移動、輸入各自是一個命令
  2. 每個命令成對擁有 execute() 與 undo()
  3. 將執行過的命令堆疊到 stack
  4. 從 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 則移除新增的部分。執行與復原集中在同一個物件中,就是這個模式的全部。

Command 協定從短短兩行開始
Command 協定從短短兩行開始

復原的關鍵是堆疊

建立命令後,還需要一個管理它們的「管理者」,通常稱為 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 自動處理,因此畫面程式碼能維持整潔。

三個 Command 元件會這樣串接
三個 Command 元件會這樣串接

如果還需要重做(redo),只要將復原過的命令另外放進 redoStack 即可。由於結構對稱,擴充並不困難。


命令模式何時適合使用,何時該避免?

命令模式也不是萬能解。以下整理了適合與過度使用的情況。

情況 命令模式
需要復原與重做 非常適合
必須保留操作記錄(歷史) 適合
將動作放入佇列稍後執行 適合
不需要復原的單純按鈕 過度設計,直接用函式

命令模式在需要「復原」或「記錄」時最能發揮價值。繪圖軟體、文字編輯器、付款取消,以及遊戲中的復原功能,都是代表性案例。

相反地,若只是替畫面導覽的單一按鈕套用命令模式,反而只會增加程式碼。

把所有按鈕都做成命令,很容易增加大量檔案,因此只應套用在需要復原的動作上。

從最上方的卡片開始逐一取出並復原
從最上方的卡片開始逐一取出並復原

總結

復原功能乍看難以加入,但只要先建立命令模式這個骨架,就能比想像中更整潔地運作。

先將今天整理的協定、具體命令與管理器三個元件套用到小型畫面。熟悉結構後,重做與歷史記錄等擴充也會自然完成。現在就放心地為你的 App 加上一個復原按鈕吧。

延伸閱讀