1. 概要

コマンドパターンはビヘイビアーデザインパターンであり、GoFのデザインパターンの正式なリストの一部です。 簡単に言えば、このパターンは、特定のアクション(コマンド)を実行するために必要なすべてのデータをオブジェクトにカプセル化することを目的としています。これには、呼び出すメソッド、メソッドの引数、メソッドが属するオブジェクトが含まれます。

このモデルでは、コンシューマーからコマンドを生成するオブジェクトを分離することができます。そのため、このパターンは一般に生産者/消費者パターンとして知られています。

このチュートリアルでは、オブジェクト指向とオブジェクト機能の両方のアプローチを使用してJavaでコマンドパターンを実装する方法を学習し、どのようなユースケースで役立つかを確認します。

2. オブジェクト指向の実装

従来の実装では、コマンドパターンには、コマンド、レシーバー、呼び出し元、およびクライアントの4つのコンポーネントを実装する必要があります。

パターンがどのように機能し、各コンポーネントが果たす役割を理解するために、基本的な例を作成しましょう。

テキストファイルアプリケーションを開発したいとします。 このような場合、テキストファイルを開く、書き込む、保存するなど、テキストファイルに関連する操作を実行するために必要なすべての機能を実装する必要があります。

したがって、アプリケーションを上記の4つのコンポーネントに分割する必要があります。

2.1. コマンドクラス

コマンドは、呼び出すメソッド、メソッド引数、メソッドを実装するオブジェクト(レシーバーと呼ばれる)など、アクションの実行に必要なすべての情報を格納する役割を持つオブジェクトです。

コマンドオブジェクトがどのように機能するかをより正確に理解するために、1つのインターフェイスと2つの実装のみを含む単純なコマンドレイヤーの開発を始めましょう。

@FunctionalInterface
public interface TextFileOperation {
    String execute();
}
public class OpenTextFileOperation implements TextFileOperation {

    private TextFile textFile;
    
    // constructors
    
    @Override
    public String execute() {
        return textFile.open();
    }
}
public class SaveTextFileOperation implements TextFileOperation {
    
    // same field and constructor as above
        
    @Override
    public String execute() {
        return textFile.save();
    }
}

この場合、 TextFileOperation インターフェイスはコマンドオブジェクトのAPIを定義し、2つの実装 OpenTextFileOperationSaveTextFileOperation、が具体的なアクションを実行します。 前者はテキストファイルを開き、後者はテキストファイルを保存します。

コマンドオブジェクトの機能を確認することは明らかです。TextFileOperationコマンドは、受信者オブジェクト、呼び出すメソッド、およびテキストファイルを開いて保存するために必要なすべての情報をカプセル化します。引数(この場合、引数は必要ありませんが、必要な場合があります)。

ファイル操作を実行するコンポーネントはレシーバー(TextFileインスタンス)であることを強調する価値があります。

2.2. レシーバークラス

レシーバーは、一連のまとまりのあるアクションを実行するオブジェクトです。 これは、コマンドの execute()メソッドが呼び出されたときに実際のアクションを実行するコンポーネントです。

この場合、TextFileオブジェクトをモデル化する役割を持つレシーバークラスを定義する必要があります。

public class TextFile {
    
    private String name;
    
    // constructor
    
    public String open() {
        return "Opening file " + name;
    }
    
    public String save() {  
        return "Saving file " + name;
    }
    
    // additional text file methods (editing, writing, copying, pasting)
}

2.3. 発動者クラス

呼び出し元は、が特定のコマンドの実行方法を知っているが、コマンドがどのように実装されているかを知らないオブジェクトです。コマンドのインターフェイスのみを知っています。

場合によっては、呼び出し元は、コマンドの実行とは別に、コマンドを保存してキューに入れます。 これは、マクロ記録や元に戻すおよびやり直し機能など、いくつかの追加機能を実装する場合に役立ちます。

この例では、コマンドオブジェクトを呼び出し、コマンドの execute()メソッドを介してそれらを実行するための追加のコンポーネントが必要であることが明らかになります。 これはまさに呼び出し側クラスが機能する場所です

呼び出し元の基本的な実装を見てみましょう。

public class TextFileOperationExecutor {
    
    private final List<TextFileOperation> textFileOperations
     = new ArrayList<>();
    
    public String executeOperation(TextFileOperation textFileOperation) {
        textFileOperations.add(textFileOperation);
        return textFileOperation.execute();
    }
}

TextFileOperationExecutor クラスは、コマンドオブジェクトをコンシューマーから切り離し、TextFileOperationコマンドオブジェクト内にカプセル化されたメソッドを呼び出す単なる抽象化レイヤーです。

この場合、クラスはコマンドオブジェクトもListに格納します。 もちろん、これは、操作の実行プロセスにさらに制御を追加する必要がない限り、パターンの実装では必須ではありません。

2.4. クライアントクラス

クライアントは、実行するコマンドとプロセスのどの段階でコマンドを実行するかを指定することにより、がコマンド実行プロセスを制御するオブジェクトです。

したがって、パターンの正式な定義でオーソドックスになりたい場合は、一般的なmainメソッドを使用してクライアントクラスを作成する必要があります。

public static void main(String[] args) {
    TextFileOperationExecutor textFileOperationExecutor
      = new TextFileOperationExecutor();
    textFileOperationExecutor.executeOperation(
      new OpenTextFileOperation(new TextFile("file1.txt"))));
    textFileOperationExecutor.executeOperation(
      new SaveTextFileOperation(new TextFile("file2.txt"))));
}

3. オブジェクト機能の実装

これまで、オブジェクト指向のアプローチを使用してコマンドパターンを実装してきましたが、これはすべてうまく機能しています。

Java 8から、ラムダ式とメソッド参照に基づくオブジェクト関数型アプローチを使用して、コードを少しコンパクトにし、冗長性を減らすことができます

3.1. ラムダ式の使用

TextFileOperation インターフェイスは機能インターフェイスであるため、を作成しなくても、ラムダ式の形式でコマンドオブジェクトを呼び出し元に渡すことができます。 TextFileOperation インスタンスを明示的に:

TextFileOperationExecutor textFileOperationExecutor
 = new TextFileOperationExecutor();
textFileOperationExecutor.executeOperation(() -> "Opening file file1.txt");
textFileOperationExecutor.executeOperation(() -> "Saving file file1.txt");

ボイラープレートコードの量を削減したため、実装ははるかに合理化され、簡潔に見えます。

それでも、問題は依然として存在します。このアプローチは、オブジェクト指向のアプローチと比較して優れているのでしょうか。

まあ、それはトリッキーです。 ほとんどの場合、よりコンパクトなコードがより良いコードを意味すると仮定すると、実際にそうです。

経験則として、ラムダ式を使用する場合は、ユースケースごとに評価する必要があります。

3.2. メソッド参照の使用

同様に、コマンドオブジェクトを呼び出し元に渡すためのメソッド参照を使用できます:

TextFileOperationExecutor textFileOperationExecutor
 = new TextFileOperationExecutor();
TextFile textFile = new TextFile("file1.txt");
textFileOperationExecutor.executeOperation(textFile::open);
textFileOperationExecutor.executeOperation(textFile::save);

この場合、 TextFile インスタンスを作成する必要があったため、実装はラムダを使用する実装よりも少し冗長です。

4. 結論

この記事では、コマンドパターンの主要な概念と、オブジェクト指向のアプローチとラムダ式とメソッド参照の組み合わせを使用してJavaでパターンを実装する方法を学びました。

いつものように、このチュートリアルに示されているすべてのコード例は、GitHubからで入手できます。