今回はCommandパターンというやつをやっていきます。
今回は登場する用語が多いので、なんというか、具体例を使わないでやっていきます。
つまり、ハンバーガー屋さんとか寿司屋とかそういう比喩は使わず。InvokerだとかConcreateCommandだとかそういうのがそのまま出てきます。
こっちのほうがわかりやすいとの判断です。
ただし、そんなんじゃ使い方わからねえよという声が聞こえてきそうなので**Invoker(リモコン)**みたいな感じでしつこめにやっていきます。
リモコンの例えはこちらの書籍からのパクリです。まあ、コードはほぼ自分で書きます。public
とかprivate
とかは特に気にせず書いてるので必要に応じて選択してください。(基本的にpublic
で書くことにします。ゲッターとかセッターを実装すると長くなるので)
用語
登場する用語が多いのでここで一旦名前と役割を書いておきます。
Receiver
ベンダー固有のクラスです。ベンダーによって機器などを操作するメソッドはLight.on()
だったりStereo.volumeUp()
だったりで統一感がないと思いますがこれをConcreateCommandのコンストラクタに渡してexecute()
メソッドにラップしてやることでCommandインターフェースを満たすようにします。そうすることでInvoker(リモコン)で操作できるようになります。
Command
execute()
メソッドを実装しているインターフェース。必要に応じてundo()
も実装させたりする。
ConcreteCommand
Commandインターフェースを満たすクラス。Receiver(ベンダ固有のクラス)を受け取ってCommandインターフェースを満たすようにする。Command、Receiver、ConcreteCommandの関係は以下のコードみたいな感じになります。Receiverのaction()
メソッドをCommandインターフェースを満たすように強引にConcreteCommandに渡していることがわかります。
export interface Command{
execute():void
}
export class Receiver{
out:string
constructor(str:string){
this.out = str
}
action(){
console.log(this.out)
}
}
export class ConcreteCommand implements Command{
receiver:Receiver
constructor(receiver:Receiver) {
this.receiver = receiver
}
execute(){
this.receiver.action()
}
}
Invoker
リモコンみたいなものです。(今回の例ではまさしくリモコンですが)
SetCommand()
メソッドの引数にConcreteCommandを受け取ってCommands
プロパティにセットします。
まあ、わかりづらいと思うのでコードを書きます。
export class Invoker{
commands:Command[] = []
setCommand(command:Command){
this.commands.push(command)
}
executeCommand(i:number){
this.commands[i].execute()
}
}
こんな感じでcommands
(リモコンでいうボタンに対応)にセットしていきます。どのボタンが押されたらどのコマンドが実行されるか割り当てるメソッドがsetCommand()
だと思ってください。
そんでexecuteCommand
メソッドで指定されたコマンドを実行します。(executeCommandメソッドは本来のCommandパターンの定義にはないので別に他のメソッドでも問題ありません。たとえば、登録されたメソッドを全て実行するメソッドに置き換えてもCommandパターンです。)
Client
Receiver(実際の機器を操作するベンダー固有のクラス)オブジェクトを作成後、それをConcreteCommand(ベンダー固有のクラスをCommandインターフェースを満たすようにラップするクラス)オブジェクトのコンストラクターに渡す。
他の書籍とか記事ではあまり見かけないが、Invoker(リモコン)オブジェクトも作成してるじゃん?という気はしてます。Invoker(リモコン)のCommands(スロット)にConcreatCommandを割り当てたりしてるし、、、
まあ、今回の例では要するに実行ファイルです。
たとえば、以下のようになっています。(Receiverの設定を行うのはClientだというのをわかりやすくするためにReciever
クラスのコンストラクタに文字列をとるようにしています)
let invoker = new Invoker()
let receiver = new Receiver("hoge")
let concreteCommand = new ConcreteCommand(receiver)
invoker.setCommand(concreteCommand)
invoker.executeCommand(0)
出力は
hoge
になります。
クラス図は以下のようになります。
UMLはなんとなくわかればいいやくらいの気持ちで書いてるので正しいかは知りませんがこんな感じです。シチュエーションによって多少変化すると思うのであえて雑に書いてます。
Commandパターンを使うと何が嬉しいのか
リクエストを送る側であるClient(コマンドを使う側)はConcreteCommand.execute()
の詳細を知らずに使うことができます。とにかく、何をしてくれるかはしらなくても実行してくれるのです。
これによってClient側ではReceiverを直接使うよりもコードの見た目がすっきりしますし、Receiver(実行方法の詳細を知っている側)と分離することができます。
あとは、下で実装するようにUndoが簡単に実装できるのも良い点です。
他に嬉しいことがあれば教えてください。
もう少し具体的なシチュエーション
上の定義でも十分だとは思うのですが、便利さが伝わるようにもうちょっと具体的にどう使うのか紹介していこうと思います。
具体的には以下のことをしていきます。
-
Command
インターフェースにundo()
メソッドを付け加えます。 - たいていのReceiverにはつけたり消したりする操作があると思うのでInvokerのcommandsを
onCommands
とoffCommands
に分離します。
ただし、そんなに難しいことをするわけでもないですし、今までの話を理解していれば問題ないと思うので、めんどくさかったら読まなくても大丈夫です。
じゃあ、やっていきます。
ベンダー固有のクラスたち(Receiver)
今回はLight
(照明)、Fan
(扇風機)を操作するリモコンを作ることにします(気分が乗ったらStereo
も自分で考えてみてください)。
こいつらのクラスはそれぞれのベンダが思い思いに作っていて統一感はありませんが以下のように実装されています。
照明
export class Light{
public isOn:boolean = false
public location:string
constructor(location:string){
this.location = location
}
on(){
this.isOn = true
console.log(this.location + "の照明がつきました")
}
off(){
this.isOn = false
console.log(this.location + "の照明が消えました")
}
}
扇風機
export class Fan{
static readonly high:number = 2
static readonly low:number = 1
static readonly off:number = 0
public speed:number = Fan.off
high(){
this.speed = Fan.high
console.log("扇風機が強になりました")
}
low(){
this.speed = Fan.low
console.log("扇風機が弱になりました")
}
off(){
this.speed = Fan.off
console.log("扇風機が停止しました")
}
}
照明はon()
,off()
、扇風機はhigh()
,low()
,off()
メソッドを持っています。
じゃあ、次はこれがCommand
インターフェースを満たすようにラップしていきましょう
Commandインターフェース(undo()
メソッドを追加)
Commandインターフェースは以下のようになります。
export interface Command{
execute():void
undo():void
}
ConcreteCommandの実装
まずは照明です。以下のようにLightOnCommand
とLightOffCommand
を実装します。
export class LightOnCommand implements Command{
public light:Light
constructor(light:Light){
this.light = light
}
execute(){
this.light.on()
}
undo(){
this.light.off()
}
}
LightOffCommand
を作る代わりにCommandインターフェースを満たすクラスを引数に取りexecute()
メソッドとundo()
メソッドを入れ替えるOffCommand
クラスを実装することにします。
export class OffCommand implements Command{
public onCommand:Command
constructor(onCommand:Command){
this.onCommand = onCommand
}
public execute(){
this.onCommand.undo()
}
public undo(){
this.onCommand.execute()
}
}
次は扇風機のコマンドを作成していきます。
export class FanOnCommand implements Command{
public fan:Fan
constructor(fan:Fan) {
this.fan = fan
}
public execute(){
switch(this.fan.speed){
case Fan.high:
break
case Fan.low:
this.fan.high()
break
case Fan.off:
this.fan.low()
break
}
}
public undo(){
switch(this.fan.speed){
case Fan.high:
this.fan.low()
break
case Fan.low:
this.fan.off()
break
case Fan.off:
break
}
}
}
これでConcreteCommandの実装が完了したのでInvoker(リモコン)の実装をしていきましょう。(やっぱり、コードが長くなるのでOnとOffを実装したのは間違いだった気がしてきましたがここまで書いたので引き返しません。私自身の学習用に書いているからです。)
Invoker(リモコン)
プロパティとしてonCommands
,offCommands
,previousCommands
を持っています。
run()
メソッドを使えば、例えば、標準入力でlight on
と打ち込めば照明がついて、previousCommand
の末尾にLightOnCommand
が追加される。といった具合に働かせることができます。
export class RemoteController{
public onCommands = new Map<string,Command>()
public offCommands = new Map<string,Command>()
public previousCommands:Command[] = []
public setCommand(name:string, onCommand:Command, offCommand:Command){
this.onCommands.set(name, onCommand)
this.offCommands.set(name, offCommand)
}
public onButtonPushed(name:string){
let command = this.onCommands.get(name)
command?.execute()
if(command!==undefined){
this.previousCommands.push(command)
}
}
public offButtonPushed(name:string){
let command = this.offCommands.get(name)
command?.execute()
if(command!==undefined){
this.previousCommands.push(command)
}
}
public undo(){
this.previousCommands[this.previousCommands.length - 1].undo()
this.previousCommands = this.previousCommands.slice(0, this.previousCommands.length - 1)
}
public run(){
process.stdin.resume()
process.stdin.setEncoding('utf8')
process.stdin.on("data",(data)=>{
let input = data.toString().replace("\n","")
console.log("入力:",input)
let command = input.split(" ")
if(input === "undo"){
this.undo()
}else if(command[1] === "on"){
this.onButtonPushed(command[0])
}else if(command[1] === "off"){
this.offButtonPushed(command[0])
}
})
}
}
Client(実行ファイル)
以下のコードを実行して、light on
と入力すればlightOnCommand.execute()
が実行されます。undo
と入力すればundo()
が実行されて照明が消えます。
let light = new Light("天井")
let lightOnCommand = new LightOnCommand(light)
let lightOffCommand = new OffCommand(lightOnCommand)
let fan = new Fan()
let fanOnCommand = new FanOnCommand(fan)
let fanOffCommand = new OffCommand(fanOnCommand)
let remoteController = new RemoteController()
remoteController.setCommand("light",lightOnCommand,lightOffCommand)
remoteController.setCommand("fan",fanOnCommand,fanOffCommand)
remoteController.run()
感想
キーボードの入力待ちとかコマンドパターン使って実装するときれいにまとまるんだろうなと思いました。
割と色んなところで使えそうではありますが、面白味はなくなってきた気がします。最初のストラテジーパターンとかオブザーバブルパターンとかは面白く感じたんだけどもオブジェクト指向に慣れてきて感動が少なくなってきたんだろうか・・・