【Java】Command パターン【デザインパターン】
目次
Commandパターンについて
Commandパターンとは
- Commandパターンは、命令を表すクラスのインスタンスを、1つのものとして表現するパターンです
- 命令の履歴を管理したいときは、そのインスタンスの集まりを管理すればいいことになります
- 命令の集まりを保存しておけば、同じ命令を実行したり、複数の命令をまとめて新しい命令として再利用したりできます
サンプルプログラム
- Commandパターンを使って、簡単なお絵かきプログラム例を取り上げます
- マウスをドラッグすれば赤の点線が描画され、clearを押すと、点が消えます
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Command | 命令を表現するインターフェース |
MacroCommand(ConcreteCommand) | 「複数の命令をまとめた命令」を表現するクラス |
DrawCommand(ConcreteCommand) | 「点の描画命令」を表現するクラス |
Drawable | 「描画対象」を表現するインターフェース |
DrawCanvas(Receiver) | 「描画対象」を実装したクラス |
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インターフェース実装クラスを作成すればよく、機能拡張が行いやすくなります
本日もお疲れ様です😊