デザインパターンを学ぶ~その21:Commandパターン(1)~
デザインパターンを学ぶ~その22:Commandパターン(2)~
と続いたCommandパターンの最後は、Undo機能を実装してみましょう。手順は次の通り。
- Do、UndoメソッドをもつIUndoableCommandインターフェイスを定義します。
public interface IUndoableCommand
{
void Do();
void Undo();
}
- Receiverを定義します。今回はサンプルとしてCounterクラスを考えます。
public class Counter
{
public Counter(int initialValue)
{
Count = initialValue;
}
public void Up()
{
Count++;
}
public void Down()
{
Count--;
}
public int Count
{
get;
private set;
}
}
- Counterクラスに対し、カウントアップ、ダウンを行うCommandクラスを定義します。
public class CountUpCommand : IUndoableCommand
{
private Counter counter;
public CountUpCommand(Counter counter)
{
this.counter = counter;
}
public void Do()
{
counter.Up();
}
public void Undo()
{
counter.Down();
}
}
public class CountDownCommand : IUndoableCommand
{
private Counter counter;
public CountDownCommand(Counter counter)
{
this.counter = counter;
}
public void Do()
{
counter.Down();
}
public void Undo()
{
counter.Up();
}
}
- Undoに備えて何もしないNullCommandクラスも定義します。
public class NullCommand : IUndoableCommand
{
public void Do()
{
}
public void Undo()
{
}
}
- Invokerとして、CountManagerクラスを定義します。
public class CountManager
{
private IUndoableCommand upCommand; // カウントアップ用Command
private IUndoableCommand downCommand; // カウントダウン用Command
private IUndoableCommand undoCommand; // Undo用Command
public CountManager(IUndoableCommand upCommand, IUndoableCommand downCommand)
{
// 引数で渡されたCommandをフィールド設定
this.upCommand = upCommand;
this.downCommand = downCommand;
// 最初はUndo操作をしても何もしないので、
// NullCommandをundoCommandに設定
this.undoCommand = new NullCommand();
}
public void DoUp()
{
// カウントアップ実行
upCommand.Do();
// 実行したCommandをundoCommandに設定
undoCommand = upCommand;
}
public void DoDown()
{
// カウントダウン実行
downCommand.Do();
// 実行したCommandをundoCommandに設定
undoCommand = downCommand;
}
public void Undo()
{
// Undo実行
undoCommand.Undo();
// Undoを連続して行えないよう、
// NullCommandを設定
undoCommand = new NullCommand();
}
}
では、実行してみましょう。実行用コードは以下の通りです。
class Program
{
static void Main(string[] args)
{
// 新規カウンター(Receiver)作成
Counter counter = new Counter(10);
Console.WriteLine(counter.Count);
// カウントアップ、ダウン用Commandクラスのインスタンス作成
CountUpCommand cuc = new CountUpCommand(counter);
CountDownCommand cdc = new CountDownCommand(counter);
// CountManager(Invoker)を作成
CountManager cm = new CountManager(cuc, cdc);
// カウントアップ
cm.DoUp();
Console.WriteLine(counter.Count);
// カウントダウン
cm.DoDown();
Console.WriteLine(counter.Count);
// Undo
cm.Undo();
Console.WriteLine(counter.Count);
// Undo
cm.Undo();
Console.WriteLine(counter.Count);
}
}
実行結果は以下の通り。
一度目のUndoでカウンターが元に戻り、二度目のUndoではカウンターに変化がないことが確認できます。
さて、ここまでのやり方は一度しかUndoが出来ないものでした。これを複数回Undo可能にするにはどうすればいいのでしょうか?
実はかなり簡単です。Invokerの中にStackとして実行したCommandを保持して、PopしながらUndoを行えばいいだけです。以下は、複数回のUndoを可能にしたCountManagerMultiUndoableです。
public class CountManagerMultiUndoable
{
private IUndoableCommand upCommand; // カウントアップ用Command
private IUndoableCommand downCommand; // カウントダウン用Command
private Stack<IUndoableCommand> undoCommands; // Undo用CommandのStack
public CountManagerMultiUndoable(IUndoableCommand upCommand, IUndoableCommand downCommand)
{
// 引数で渡されたCommandをフィールド設定
this.upCommand = upCommand;
this.downCommand = downCommand;
// Stackを初期化
undoCommands = new Stack<IUndoableCommand>();
}
public void DoUp()
{
// カウントアップ実行
upCommand.Do();
// 実行したCommandをStackにPush
undoCommands.Push(upCommand);
}
public void DoDown()
{
// カウントダウン実行
downCommand.Do();
// 実行したCommandをStackにPush
undoCommands.Push(downCommand);
}
public void Undo()
{
if (undoCommands.Count != 0)
{
// Stackが空でなければ、PopしてUndo実行
var undoCommand = undoCommands.Pop();
undoCommand.Undo();
}
}
}
では、実行してみましょう。先ほどと違って、Undoを3回行うようにしてみます。
class Program
{
static void Main(string[] args)
{
// 新規カウンター(Receiver)作成
Counter counter = new Counter(10);
Console.WriteLine(counter.Count);
// カウントアップ、ダウン用Commandクラスのインスタンス作成
CountUpCommand cuc = new CountUpCommand(counter);
CountDownCommand cdc = new CountDownCommand(counter);
// CountManager(Invoker)を作成
CountManagerMultiUndoable cm = new CountManagerMultiUndoable(cuc, cdc);
// カウントアップ
cm.DoUp();
Console.WriteLine(counter.Count);
// カウントダウン
cm.DoDown();
Console.WriteLine(counter.Count);
// Undo
cm.Undo();
Console.WriteLine(counter.Count);
// Undo
cm.Undo();
Console.WriteLine(counter.Count);
// Undo
cm.Undo();
Console.WriteLine(counter.Count);
}
}
実行結果は以下の通りです。
2度Undoが行われ、3度目は何もカウンターに変化がないことが確認できます。
ここまで3回にわたりCommandパターンを紹介しましたが、いかがでしたでしょうか?
Commandパターンは他にも、
- QueueにCommandオブジェクトを登録して順次呼び出す
- Commandオブジェクトを永続化して、必要な際に取り出して呼び出す
- Commandオブジェクトを直列化(シリアライズ)して、ネットワークなどの境界を越えて実行
など、色々と活用する場面があるようです。
私も今後もどのように有効に活用できるか考えていきたいと思います。
昨年はまったくと言っていいほど進まなかった、このデザインパターンシリーズ。今年はがんばってやっていこうと思います。次回は「Adapterパターン」に入っていくつもりですので、今後ともお付き合いをお願いします。
なお、今後コードはC#のみとさせてもらいます。案外VBにportするのが大変なものでf(^^;