mohuneko’s blog

かんばる駆け出しエンジニアのブログです

【Java】Command パターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Commandパターンについて

Commandパターンとは

  • Commandパターンは、命令を表すクラスのインスタンスを、1つのものとして表現するパターンです
  • 命令の履歴を管理したいときは、そのインスタンスの集まりを管理すればいいことになります
  • 命令の集まりを保存しておけば、同じ命令を実行したり、複数の命令をまとめて新しい命令として再利用したりできます

サンプルプログラム

  • Commandパターンを使って、簡単なお絵かきプログラム例を取り上げます
  • マウスをドラッグすれば赤の点線が描画され、clearを押すと、点が消えます
  • 各クラスの役割は以下のようになっています
名前 役割
Command 命令を表現するインターフェース
MacroCommand(ConcreteCommand) 「複数の命令をまとめた命令」を表現するクラス
DrawCommand(ConcreteCommand) 「点の描画命令」を表現するクラス
Drawable 「描画対象」を表現するインターフェース
DrawCanvas(Receiver) 「描画対象」を実装したクラス

f:id:mohuNeko:20210103121229p:plain

Command インターフェース

  • 命令を表現するインターフェースでexecuteメソッドのみを持ちます
  • 何が起こるかはMacroCommand・DrawCommandクラスが実装します
package command;

public interface Command {
    public abstract void execute();
}

MacroCommandクラス

  • 複数の命令をまとめた命令を表します
  • commandsフィールドに複数のCommand実装クラスのインスタンスを集めておきます
  • appendメソッドで、Commandを実装したクラスのインスタンスを追加します
    • 無限ループにならないよう、ifで自分自身(this)ではないことをチェックします
  • undoメソッドで、commandsの最後の命令を削除します
package command;

import java.util.Stack;
import java.util.Iterator;

public class MacroCommand implements Command {
    // 命令の集合
    private Stack commands = new Stack();
    // 実行
    public void execute() {
        Iterator it = commands.iterator();
        while (it.hasNext()) {
            ((Command)it.next()).execute();
        }
    }
    // 追加
    public void append(Command cmd) {
        if (cmd != this) {
            commands.push(cmd);
        }
    }
    // 最後の命令を削除
    public void undo() {
        if (!commands.empty()) {
            commands.pop();
        }
    }
    // 全部削除
    public void clear() {
        commands.clear();
    }
}

DrawCommandクラス

  • 点の描画命令を表現します
  • Pointはjava.awt.Pointで定められているクラスで、X,Y座標で二次元平面上の位置を表します
  • コンストラクタでDrawableインターフェース実装クラスのインスタンスと、Pointクラスのインスタンスを引数に渡して、フィールドに代入します
    • "この位置に点を描け"という命令を表しています
package drawer;

import command.Command;
import java.awt.Point;

public class DrawCommand implements Command {
    // 描画対象
    protected Drawable drawable;
    // 描画位置
    private Point position;

    // コンストラクタ
    public DrawCommand(Drawable drawable, Point position) {
        this.drawable = drawable;
        this.position = position;
    }
    // 実行
    public void execute() {
        drawable.draw(position.x, position.y);
    }
}

Drawableインターフェース

  • 描画対象を表します
  • drawは描画するメソッドです
package drawer;

public interface Drawable {
    public abstract void draw(int x, int y);
}

DrawCanvasクラス

  • Drawableインターフェース実装クラスで、java.awt.Canvas クラスのサブクラスです
  • 自分が描画すべき命令の集合は、MacroCommand型のhistoryフィールドに保持します
  • 履歴全体を再描画する時は、paintメソッドで history.execute() を呼ぶだけで、保持されている命令の集まりが再実行されます
package drawer;

import command.*;

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class DrawCanvas extends Canvas implements Drawable {
    // 描画色
    private Color color = Color.red;
    // 描画する点の半径
    private int radius = 6;
    // 履歴
    private MacroCommand history;
    // コンストラクタ
    public DrawCanvas(int width, int height, MacroCommand history) {
        setSize(width, height);
        setBackground(Color.white);
        this.history = history;
    }
    // 履歴全体を再描画
    public void paint(Graphics g) {
        history.execute();
    }
    // 描画
    public void draw(int x, int y) {
        Graphics g = getGraphics();
        g.setColor(color);
        g.fillOval(x - radius, y - radius, radius * 2, radius * 2);
    }
}

Mainクラスで動作確認

  • hostoryで描画履歴を保持し、DrawCanvas に渡します
  • 描画履歴はMainとDrawCanvasで共有されているということになります
import command.*;
import drawer.*;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Main extends JFrame implements ActionListener, MouseMotionListener, WindowListener {
    // 描画履歴
    private MacroCommand history = new MacroCommand();
    // 描画領域
    private DrawCanvas canvas = new DrawCanvas(400, 400, history);
    // 消去ボタン
    private JButton clearButton  = new JButton("clear");

    // コンストラクタ
    public Main(String title) {
        super(title);

        this.addWindowListener(this);
        canvas.addMouseMotionListener(this);
        clearButton.addActionListener(this);

        Box buttonBox = new Box(BoxLayout.X_AXIS); //横にボタンを並べるボックス
        buttonBox.add(clearButton);
        Box mainBox = new Box(BoxLayout.Y_AXIS); //縦にボタンを並べるボックス
        mainBox.add(buttonBox); 
        mainBox.add(canvas);
        getContentPane().add(mainBox); ..コンテナにコンポーネントを載せる

        pack();
        show();
    }

    // ActionListener用
    public void actionPerformed(ActionEvent e) { 
        if (e.getSource() == clearButton) {
            history.clear();
            canvas.repaint();
        }
    }

    // MouseMotionListener用
    public void mouseMoved(MouseEvent e) {
    }
    public void mouseDragged(MouseEvent e) {
        Command cmd = new DrawCommand(canvas, e.getPoint());
        history.append(cmd); //実行履歴に追加
        cmd.execute(); //即実行
    }

    // WindowListener用
    public void windowClosing(WindowEvent e) {
        System.exit(0);
    }
    public void windowActivated(WindowEvent e) {}
    public void windowClosed(WindowEvent e) {}
    public void windowDeactivated(WindowEvent e) {}
    public void windowDeiconified(WindowEvent e) {}
    public void windowIconified(WindowEvent e) {}
    public void windowOpened(WindowEvent e) {}

    public static void main(String[] args) {
        new Main("Command Pattern Sample");
    }
}

Commandパターンのメリット

  • 命令をオブジェクトとして表現することで、命令の履歴をとったり、命令の再実行を行うことができます
  • 新しい命令を追加したい場合は、Commandインターフェース実装クラスを作成すればよく、機能拡張が行いやすくなります
  • ConcreteCommand自身が、Receiver(描画対象)も知っていることで、ConcreteCommandを誰が管理してもいつでもexecuteできるようになります

アダプターについて

  • プログラミングを簡略化するために、アダプターというクラス群がjava.awt.eventパッケージに用意されています(Adapterパターンの一例)
    • MouseMotionListenerインターフェースに対して、MouseMotionAdapterクラスなど
    • MouseMotionAdapterクラスでは、MouseMotionListenerインターフェースを実装し、要求するメソッドを全部提供します *しかし、提供されているインターフェースの実装の中身は空になっているので、MouseMotionAdapterクラスのサブクラスを作り、必要なメソッドを実装する事で、目的を達成できます
  • 匿名インナークラス機構を組み合わせてアダプターを使うと、スマートに記述できます
    • MouseMotionAdapterクラスの名前なしサブクラスを作り、そのインスタンスを生成できます
    • オーバーライドしたいメソッドのみ実装すれば、他は書く必要がありません

今日のポイント

  • Commandパターンは、命令を表すクラスのインスタンスを、1つのものとして表現するパターンです
  • 命令をオブジェクトとして表現することで、命令の履歴をとったり、命令の再実行を行うことができます
  • 新しい命令を追加したい場合は、Commandインターフェース実装クラスを作成すればよく、機能拡張が行いやすくなります

 本日もお疲れ様です😊