dorivenの日記

気がついたら社会人。気になる技術的なことについて少しずつ書いていけたらと思っております。

【デザインパターン】コマンドパターン

リファクタリングがまだ中途半端なのですが、5月中までに結果を出さないと行けないので頑張ってやってます。
リファクタリングとは名ばかりの完全なコードの書き直しなのですが、まだまだ自分としては納得行ってないでの時間があれば続けたいんです。
最近気づいたんですけど、小さなコードのリファクタリングはよくやってるのですが、大きなコードのリファクタリングって大変で、自分はかなり苦手、というより慣れていないな、と実感しています。
今まで散りばめられていた様々な機能を分離したりで、勢いだけで書けないんで気持ちよさも最高潮ではないです。
そういえば、さっき見たらコードがコメントとテストコード込みで3000行くらい行ってました。
いつの間にこんなに書いたんだって話しですね。まだまだ増える予定だし。
そして、現在恒例の計算部でバグが出て死にそうです。デスマ確定です。
早くバグ滅っしたい。

プロジェクトメンバーは足引っ張ってごめんなさい。

さて、それでは今週は前から引き続き、デザインパターンの【Command】についての記事を書いていきます。

参考書



さらっと読める「OREILLY Head First デザインパターン」を参考にしながら書いています。
しばらくはこいつを参考にデザインパターンについて書いていくよ!

一覧


  1. Commandパターンって何ですか?
  2. Commandパターンのメリット
  3. 身近で簡単な使用例
  4. Commandパターンを使う

Commandパターンって何ですか?



自分が説明するよりも、Wikipedia先生を見てみましょう。

以下はWikpediaのクラス図とその説明です。

Command パターン - Wikipedia

Command パターン(英: command pattern)はオブジェクト指向プログラミングにおけるデザインパターンの一つで、 動作を表現するオブジェクトを示す。command オブジェクトは、動作とそれに伴うパラメータをカプセル化したものである。

要は処理に必要なパラメータと、その動作の詳細を隠蔽するって話ですね。

Commandパターンのメリット



上記にも書いてある通り、処理の詳細を隠蔽することができます。
また他の機能として

  1. Undo, Redo機能
  2. 特定の一連の処理(Command単位での)のマクロの定義

身近で簡単な使用例



参考書では食堂を例にして説明していきます。
食堂では以下の順番で注文がされますね。

  • お客がウェイトレスに注文
  • ウェイトレスはお客から注文を取り注文票に記載します
  • そして、注文票コックに渡します。

お客(Client)は料理の作り方や、コックへのオーダーの仕方を知る必要はありません。
全てで行っているのは注文票の受け渡しのみです。

そして、この注文票こそがCommandパターンでいう「Command」オブジェクトです。
注文票を見て、その情報(処理に必要なパラーメータ)を受け取ることでコックは料理をすることが出来ます(処理の開始)

この例で言えば上記の登場人物はクラス図の名前で以下のように当てはまります。

登場人物クラス図
お客Client
ウェイトレスInvoker
コックReceiver
注文票Command (Concrete Command)

Commandパターンを使う



さて、Commandパターンの概要がわかった所でまた参考書の例を元に実装を行っていきましょう。

あなたの会社はとある会社から、画期的なリモコンの制作を依頼されました。
そのリモコンには7つのスロットがあり、それぞれのスロットにONボタン、OFFボタンが付いており、スロットには家庭用機器(証明、扇風機、浴槽、音響機器、etc)を登録することが出来ます。
またUNDOボタンも付いており、先ほどのリモコンの操作を無効にすることが出来ます。
家庭用機器のAPIが入ったCD-Rも同封されている。

ちなみにここの例ではクラス図に照らし合わせると以下のようになります。

簡単な例での登場人物クラス図登場人物
お客Client操作する人(未登場)
ウェイトレスInvokerリモコン
コックReceiver家庭用機器
注文票Command (Concrete Command)家庭用機器のAPI

Invoker

早速、リモコンから作っていきましょう。

public class RemoteControl {
	Command[] onCommands;
	Command[] offCommands;
 
	public RemoteControl() {
		onCommands = new Command[7];
		offCommands = new Command[7];
 
		for (int i = 0; i < 7; i++) {
			onCommands[i] = () -> { };
			offCommands[i] = () -> { };
		}
	}
  
	public void setCommand(int slot, Command onCommand, Command offCommand) {
		onCommands[slot] = onCommand;
		offCommands[slot] = offCommand;
	}
 
	public void onButtonWasPushed(int slot) {
		onCommands[slot].execute();
	}
 
	public void offButtonWasPushed(int slot) {
		offCommands[slot].execute();
	}
  
	// ・・・・

}
Receiver

では、今度はReceiverの定義を見て行きましょう。
下は電灯のReceiverクラスです。

public class Light {
	String location = "";

	public Light(String location) {
		this.location = location;
	}

	public void on() {
		System.out.println(location + " light is on");
	}

	public void off() {
		System.out.println(location + " light is off");
	}
}

以上のような感じで7つの電子機器を作ります。

Command

対応するボタンにCommandオブジェクトを割り当てていきましょう。
以下がそのCommandオブジェクトの基本クラスになります。

public interface Command {
	public void execute();
}

ちなみにCommandを設定していないものはオーバーライドして動作しないように定義しましょう。

public class NoCommand implements Command {
	public void execute(){};
}

そして、Receiverの機能を使用します。
以下にはLightのCommandを定義します。

public class LightOnCommand implements Command {
	Light light;

	public LightOnCommand(Light light) {
		this.light = light;
	}

	public void execute() {
		light.on();
	}
}

public class LightOffCommand implements Command {
	Light light;
 
	public LightOffCommand(Light light) {
		this.light = light;
	}
 
	public void execute() {
		light.off();
	}
}
Client

それでは、最後にClientを作ります。
それぞれのリモコンのボタンに処理を割り当てて実行していきましょう。
以下がそのコードになります。

public class RemoteLoader {
 
	public static void main(String[] args) {
		RemoteControl remoteControl = new RemoteControl();
 
		Light livingRoomLight = new Light("Living Room");
		Light kitchenLight = new Light("Kitchen");
		CeilingFan ceilingFan= new CeilingFan("Living Room");
		GarageDoor garageDoor = new GarageDoor("");
		Stereo stereo = new Stereo("Living Room");
  
		LightOnCommand livingRoomLightOn = 
				new LightOnCommand(livingRoomLight);
		LightOffCommand livingRoomLightOff = 
				new LightOffCommand(livingRoomLight);
		LightOnCommand kitchenLightOn = 
				new LightOnCommand(kitchenLight);
		LightOffCommand kitchenLightOff = 
				new LightOffCommand(kitchenLight);
  
		CeilingFanOnCommand ceilingFanOn = 
				new CeilingFanOnCommand(ceilingFan);
		CeilingFanOffCommand ceilingFanOff = 
				new CeilingFanOffCommand(ceilingFan);
 
		GarageDoorUpCommand garageDoorUp =
				new GarageDoorUpCommand(garageDoor);
		GarageDoorDownCommand garageDoorDown =
				new GarageDoorDownCommand(garageDoor);
 
		StereoOnWithCDCommand stereoOnWithCD =
				new StereoOnWithCDCommand(stereo);
		StereoOffCommand  stereoOff =
				new StereoOffCommand(stereo);
 
		remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
		remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);
		remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff);
		remoteControl.setCommand(3, stereoOnWithCD, stereoOff);
  
		System.out.println(remoteControl);
 
		remoteControl.onButtonWasPushed(0);
		remoteControl.offButtonWasPushed(0);
		remoteControl.onButtonWasPushed(1);
		remoteControl.offButtonWasPushed(1);
		remoteControl.onButtonWasPushed(2);
		remoteControl.offButtonWasPushed(2);
		remoteControl.onButtonWasPushed(3);
		remoteControl.offButtonWasPushed(3);
	}
}

さて、これで使用するボタンがどのように処理されているか、ということをClient側は一切知る必要がなく、ただどのボタンにどの機器が対応しているか、ということを知っているだけです。機能が上手く隠蔽されていると思いませんか?

Undoボタンを作成する

しかし、このままではまだUndoボタンの機能を満たしていません。
単純に実装すると面白く無い(ただ、全開の動作と反対のボタンを押したように見せるだけ)ので、ON, OFFだけでなく3つのボタン(つまり、3つの状態)があるようにします。

ここで、CeilingFanという扇風機のクラスにUndo処理を定義する例を示します。

まずはReciverです。

public class CeilingFan {
	public static final int HIGH = 3;
	public static final int MEDIUM = 2;
	public static final int LOW = 1;
	public static final int OFF = 0;
	String location;
	int speed;
 
	public CeilingFan(String location) {
		this.location = location;
		speed = OFF;
	}
  
	public void high() {
		speed = HIGH;
		System.out.println(location + " ceiling fan is on high");
	} 
 
	public void medium() {
		speed = MEDIUM;
		System.out.println(location + " ceiling fan is on medium");
	}
 
	public void low() {
		speed = LOW;
		System.out.println(location + " ceiling fan is on low");
	}
  
	public void off() {
		speed = OFF;
		System.out.println(location + " ceiling fan is off");
	}
  
	public int getSpeed() {
		return speed;
	}
}

そして、これに対応するCommandを用意するのですが、ここが重要です。
先程まではCommandにはexecute()しかありませんでしたが、今度はundo()という関数も用意します。{{電源OFFがないととかいうツッコミはなしだ}}

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

そして、以下のように扇風機の3つ状態である高速、中速、低速の処理を定義します。
prevSpeedという前の速度を保持する変数が実行前に、現在の速度を保存する点に注目!

public class CeilingFanHighCommand implements Command {
	CeilingFan ceilingFan;
	int prevSpeed;
  
	public CeilingFanHighCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
 
	public void execute() {
		prevSpeed = ceilingFan.getSpeed(); // ここで実行前のスピードを保持する!
		ceilingFan.high();
	}
 
	public void undo() {
		if (prevSpeed == CeilingFan.HIGH) {
			ceilingFan.high();
		} else if (prevSpeed == CeilingFan.MEDIUM) {
			ceilingFan.medium();
		} else if (prevSpeed == CeilingFan.LOW) {
			ceilingFan.low();
		} else if (prevSpeed == CeilingFan.OFF) {
			ceilingFan.off();
		}
	}
}

public class CeilingFanMediumCommand implements Command {
	CeilingFan ceilingFan;
	int prevSpeed;
  
	public CeilingFanMediumCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
 
	public void execute() {
		prevSpeed = ceilingFan.getSpeed();
		ceilingFan.medium();
	}
 
	public void undo() {
		if (prevSpeed == CeilingFan.HIGH) {
			ceilingFan.high();
		} else if (prevSpeed == CeilingFan.MEDIUM) {
			ceilingFan.medium();
		} else if (prevSpeed == CeilingFan.LOW) {
			ceilingFan.low();
		} else if (prevSpeed == CeilingFan.OFF) {
			ceilingFan.off();
		}
	}
}

public class CeilingFanLowCommand implements Command {
	CeilingFan ceilingFan;
	int prevSpeed;
  
	public CeilingFanLowCommand(CeilingFan ceilingFan) {
		this.ceilingFan = ceilingFan;
	}
 
	public void execute() {
		prevSpeed = ceilingFan.getSpeed();
		ceilingFan.low();
	}
 
	public void undo() {
		if (prevSpeed == CeilingFan.HIGH) {
			ceilingFan.high();
		} else if (prevSpeed == CeilingFan.MEDIUM) {
			ceilingFan.medium();
		} else if (prevSpeed == CeilingFan.LOW) {
			ceilingFan.low();
		} else if (prevSpeed == CeilingFan.OFF) {
			ceilingFan.off();
		}
	}
}

あとはリモコンのボタンにUndo処理を定義して同じようにボタンの設定をするだけです。
長くなるのでボタンの設定は省きます

public class RemoteControlWithUndo {
	Command[] onCommands;
	Command[] offCommands;
	Command undoCommand;
 
	public RemoteControlWithUndo() {
		onCommands = new Command[7];
		offCommands = new Command[7];
 
		Command noCommand = new NoCommand();
		for(int i=0;i<7;i++) {
			onCommands[i] = noCommand;
			offCommands[i] = noCommand;
		}
		undoCommand = noCommand;
	}
  
	public void setCommand(int slot, Command onCommand, Command offCommand) {
		onCommands[slot] = onCommand;
		offCommands[slot] = offCommand;
	}
 
	public void onButtonWasPushed(int slot) {
		onCommands[slot].execute();
		undoCommand = onCommands[slot];
	}
 
	public void offButtonWasPushed(int slot) {
		offCommands[slot].execute();
		undoCommand = offCommands[slot];
	}
 
	public void undoButtonWasPushed() {
		undoCommand.undo();
	}
  
        // …
}

もちろん、3状態でなく、状態が無限にあるならば処理のログを保持するListなどを用意する必要があるが、このような感じでUndo処理を実現出来る。

つまりは、Undoする前の処理を保持しておけば、Redoも出来るということだ!

最後に



以上でCommandパターンを終わります。
Commandパターン自体は使ったことはないが、似たような感じでexecute()とかいう関数をクラスに用意してあげて、コンストラクタで必要なパラメータを渡して、実行する側はパラメータを渡す以外は何も気にする必要がない、という感じでやってたが、こっちのほうが凝集度は高そうですね。
色んなパターンがあってなかなかおもしろいです。