Skip to content

Commandパターンとは:処理をオブジェクトにする設計手法

はじめに

「処理をオブジェクトにする」と聞いて、ピンとこない方も多いのではないでしょうか。正直なところ、私も最初にこの言葉を聞いたとき、何を言っているのかさっぱりでした。

でも考えてみてください。ボタンを押したら何かが実行される。メニューを選んだら処理が走る。こういった「やりたいこと」を、コードのあちこちに直接書いていると、どうなるでしょう。同じような処理が散らばり、取り消し機能を追加しようとしたら大改修が必要になる。そんな経験、ありませんか。

本記事では、GoFデザインパターンのひとつであるCommandパターンについて解説します。難しい言葉は極力避けて、図をたくさん使いながら「結局何がうれしいの?」というところまでお伝えできればと思います。

Commandパターンの基本的な考え方

Commandパターンは、実行したい処理をオブジェクトとして切り出す設計手法です。「振る舞いパターン」と呼ばれるカテゴリに属していて、オブジェクト同士のやり取りをうまく整理するための知恵のひとつです。

まずは、このパターンがない状態を想像してみましょう。


flowchart LR
    A[ボタンA] -->|直接呼び出し| B[処理X]
    C[メニュー項目] -->|直接呼び出し| B
    D[キーボード] -->|直接呼び出し| B

    style B fill:#ff6b6b

ボタンもメニューもキーボードショートカットも、全部が処理Xを直接呼んでいます。これ、一見シンプルですよね。でも処理Xを変更したくなったら、呼び出し元を全部修正しなきゃいけません。

処理を「モノ」として扱うとどうなるか

Commandパターンを使うと、この関係が変わります。


flowchart LR
    A[ボタンA] --> E[コマンド]
    C[メニュー項目] --> E
    D[キーボード] --> E
    E -->|実行| B[処理X]

    style E fill:#51cf66

処理Xを実行するための「コマンド」というオブジェクトを間に挟みます。ボタンもメニューもキーボードも、このコマンドを実行するだけ。処理の詳細は知らなくていいんです。

これが「処理をオブジェクトにする」ということの意味です。処理そのものを、データのように受け渡しできる「モノ」として扱えるようになります。

パターンを構成する登場人物たち

Commandパターンには、いくつかの役割を持ったクラスが登場します。堅苦しい名前がついていますが、やっていることは単純です。


classDiagram
    class Command {
         <>
        +execute()
    }
    class ConcreteCommand {
        -receiver
        +execute()
    }
    class Invoker {
        -command
        +setCommand()
        +executeCommand()
    }
    class Receiver {
        +action()
    }

Command <|.. ConcreteCommand 
ConcreteCommand --> Receiver
Invoker --> Command

それぞれの役割を説明します。

Command(コマンド)は、実行するための入り口を定義するインターフェースです。「execute」というメソッドを持っていて、これを呼べば処理が走ります。

ConcreteCommand(具体的なコマンド)は、Commandの実装クラスです。「ファイルを保存するコマンド」「テキストをコピーするコマンド」のように、具体的な処理内容を知っています。

Invoker(呼び出し側)は、コマンドを実行する側です。ボタンやメニュー項目がこれに当たります。「どんなコマンドかは知らないけど、とにかく実行する」というスタンスです。

Receiver(受け手)は、実際の処理を行うクラスです。コマンドから呼び出されて、本当の仕事をこなします。

実際の処理の流れを追ってみる

言葉だけだとイメージしづらいので、実際にどう動くのか見ていきましょう。テキストエディタでコピー操作をする場面を例にします。


sequenceDiagram
    participant User as ユーザー
    participant Button as コピーボタン
    participant Cmd as CopyCommand
    participant Editor as テキストエディタ

    User->>Button: クリック
    Button->>Cmd: execute()
    Cmd->>Editor: copy()
    Editor-->>Cmd: 完了
    Cmd-->>Button: 完了
    Button-->>User: 反応を返す

ユーザーがコピーボタンをクリックすると、ボタンは持っているコマンドの「execute」を呼びます。コマンドはテキストエディタの「copy」メソッドを呼んで、実際のコピー処理が行われます。

ここでのポイントは、ボタンがテキストエディタを直接知らないことです。ボタンはコマンドを実行するだけ。だから、同じボタンに別のコマンドを渡せば、別の処理を実行できます。

Commandパターンの具体的な実装

ここからは、実際にコードを見ながら理解を深めていきます。シンプルなテキストエディタを題材にしてみましょう。

まずはCommandインターフェースの定義です。


interface Command {
    fun execute()
}

たったこれだけです。「execute」というメソッドを持っているだけ。シンプルですよね。

次に、実際の処理を行うReceiver(テキストエディタ)を作ります。


class TextEditor {
    var text: String = ""
    var clipboard: String = ""

    fun copy() {
        clipboard = text
    }

    fun paste() {
        text += clipboard
    }

    fun clear() {
        text = ""
    }
}

テキストを保持して、コピー・ペースト・クリアができるシンプルなエディタです。

続いて、具体的なコマンドを実装します。


class CopyCommand(private val editor: TextEditor) : Command {
    override fun execute() {
        editor.copy()
    }
}

class PasteCommand(private val editor: TextEditor) : Command {
    override fun execute() {
        editor.paste()
    }
}

class ClearCommand(private val editor: TextEditor) : Command {
    override fun execute() {
        editor.clear()
    }
}

各コマンドは、エディタへの参照を持っていて、executeが呼ばれたら対応するメソッドを呼び出します。

最後に、これらを使う側のコードです。


class Button(private var command: Command) {
    fun click() {
        command.execute()
    }

    fun setCommand(newCommand: Command) {
        command = newCommand
    }
}

ボタンはコマンドを持っていて、クリックされたらexecuteを呼ぶだけ。どんなコマンドかは気にしません。


fun main() {
    val editor = TextEditor()
    editor.text = "Hello, World!"

    val copyButton = Button(CopyCommand(editor))
    val pasteButton = Button(PasteCommand(editor))

    copyButton.click()  // テキストをコピー
    pasteButton.click() // ペースト

    println(editor.text) // "Hello, World!Hello, World!"
}

Commandパターンが真価を発揮する場面

ここまで読んで「わざわざクラスを増やす意味あるの?」と思った方、正直その感覚は正しいです。単純な処理なら、直接呼んだほうが早い。

でも、このパターンが輝く場面があります。

取り消し機能の実装

Undo/Redo機能を実装するとき、Commandパターンは本当に便利です。


flowchart TD
    A[コマンド実行] --> B[履歴スタックに追加]
    B --> C{Undoボタン押下?}
    C -->|Yes| D[スタックから取り出し]
    D --> E[undo実行]
    E --> F[Redoスタックに追加]
    C -->|No| G[通常操作継続]

    style B fill:#4dabf7
    style F fill:#4dabf7

コマンドを実行するたびに履歴として保存しておけば、あとから「元に戻す」ことができます。

Commandインターフェースにundoメソッドを追加してみましょう。


interface Command {
    fun execute()
    fun undo()
}

class TypeCommand(
    private val editor: TextEditor,
    private val textToType: String
) : Command {
    private var previousText: String = ""

    override fun execute() {
        previousText = editor.text
        editor.text += textToType
    }

    override fun undo() {
        editor.text = previousText
    }
}

TypeCommandは、実行前のテキストを覚えておいて、undoが呼ばれたら元に戻します。

履歴を管理するクラスも作ってみます。


class CommandHistory {
    private val history = mutableListOf()
    private var currentIndex = -1

    fun execute(command: Command) {
        // 現在位置より後ろの履歴を削除
        while (history.size > currentIndex + 1) {
            history.removeAt(history.size - 1)
        }
        command.execute()
        history.add(command)
        currentIndex++
    }

    fun undo() {
        if (currentIndex >= 0) {
            history[currentIndex].undo()
            currentIndex--
        }
    }

    fun redo() {
        if (currentIndex < history.size - 1) {
            currentIndex++
            history[currentIndex].execute()
        }
    }
}

これで、ワープロソフトのようなUndo/Redo機能が実現できます。

マクロ機能の実装

複数のコマンドをまとめて実行する「マクロ」も簡単に作れます。


flowchart LR
    A[マクロコマンド] --> B[コマンド1]
    A --> C[コマンド2]
    A --> D[コマンド3]
    B --> E[順番に実行]
    C --> E
    D --> E

    style A fill:#51cf66

class MacroCommand(private val commands: List) : Command {
    override fun execute() {
        commands.forEach { it.execute() }
    }

    override fun undo() {
        commands.reversed().forEach { it.undo() }
    }
}

MacroCommand自体もCommandインターフェースを実装しているので、他のコマンドと同じように扱えます。マクロの中にマクロを入れることだってできます。

処理の遅延実行とキューイング

コマンドはオブジェクトなので、すぐ実行しなくても保存しておけます。


sequenceDiagram
    participant Client as クライアント
    participant Queue as コマンドキュー
    participant Worker as ワーカー
    participant Cmd as コマンド

    Client->>Queue: コマンド追加
    Client->>Queue: コマンド追加
    Client->>Queue: コマンド追加
    Note over Queue: 後で処理
    Worker->>Queue: 取り出し
    Queue-->>Worker: コマンド
    Worker->>Cmd: execute()

バッチ処理やジョブキューの実装に使えるパターンです。ユーザーの操作を一旦キューに溜めて、バックグラウンドで順番に処理するような場面で活躍します。

Commandパターンを使うべきか判断する

万能なパターンはありません。Commandパターンにも向き不向きがあります。


flowchart TD
    A{Commandパターンを検討} --> B{取り消し機能が必要?}
    B -->|Yes| C[採用を強く推奨]
    B -->|No| D{処理の履歴を記録したい?}
    D -->|Yes| C
    D -->|No| E{同じ処理を複数箇所から呼ぶ?}
    E -->|Yes| F[採用を検討]
    E -->|No| G{処理を後で実行したい?}
    G -->|Yes| F
    G -->|No| H[不要かもしれない]

    style C fill:#51cf66
    style F fill:#4dabf7
    style H fill:#ff6b6b

向いている場面

取り消し・やり直し機能が必要なとき、Commandパターンは強力です。テキストエディタ、画像編集ソフト、設定画面などが典型例です。

処理の履歴を記録してログを取りたいときも有効です。コマンドオブジェクトをそのまま保存すれば、「誰が何をいつ実行したか」を追跡できます。

GUIのボタンやメニューなど、同じ処理を複数の場所から呼び出すときも便利です。ボタンとメニューとショートカットキーが同じコマンドを共有できます。

向いていない場面

単純な処理を一度実行するだけなら、わざわざクラスを作る必要はありません。「犬も歩けば棒に当たる」式に何でもパターンを適用すると、かえってコードが複雑になります。

処理の取り消しが原理的に不可能な場合(メール送信、外部APIへのPOSTなど)も、Commandパターンの恩恵を受けにくいです。undoメソッドが意味をなさないからです。

他のパターンとの関係

Commandパターンは、他のデザインパターンと組み合わせて使うことが多いです。


flowchart LR
    A[Command] --- B[Memento]
    A --- C[Composite]
    A --- D[Strategy]

    B -->|状態の保存・復元| E[Undoの実現]
    C -->|複合コマンド| F[マクロの実現]
    D -->|アルゴリズムの切り替え| G[処理の差し替え]

    style A fill:#4dabf7

Mementoパターンと組み合わせると、より高度なUndo機能が実現できます。Compositeパターンと組み合わせると、マクロのようなコマンドの入れ子構造を作れます。

Strategyパターンとは似ているところがありますが、目的が異なります。Strategyはアルゴリズムを差し替えるためのパターンで、Commandは処理をオブジェクト化して操作するためのパターンです。

終わりに

Commandパターンは、「処理をオブジェクトにする」というシンプルなアイデアです。難しそうに見えて、やっていることは単純です。

実行したい処理をクラスとして切り出す。そうすることで、処理の履歴を保存したり、取り消したり、後で実行したりできるようになる。それだけのことです。

最初から完璧なパターン適用を目指す必要はありません。まずは「この処理、後で取り消せたら便利だな」と思ったときに、Commandパターンを思い出してください。きっと役に立つ場面があるはずです。

デザインパターンは道具です。使いどころを見極めて、必要なときに必要なだけ使う。それが大事だと思っています。

GoFデザインパターンについてさらに学びたい方は、以下のリソースをご参照ください。

オブジェクト指向における再利用のためのデザインパターン

オブジェクト指向における再利用のためのデザインパターン(改訂版) | SBクリエイティブ SBクリエイティブ

Refactoring Guru – Command

Command refactoring.guru

SourceMaking – Command Design Pattern

Design Patterns and Refactoring sourcemaking.com

コメントを残す