LoginSignup
4

More than 1 year has passed since last update.

TypeScriptでデザインパターンを理解する ⑥Commandパターン

Last updated at Posted at 2020-11-28

今回は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

になります。

クラス図は以下のようになります。

スクリーンショット 2020-11-28 17.53.30.png

UMLはなんとなくわかればいいやくらいの気持ちで書いてるので正しいかは知りませんがこんな感じです。シチュエーションによって多少変化すると思うのであえて雑に書いてます。

Commandパターンを使うと何が嬉しいのか

リクエストを送る側であるClient(コマンドを使う側)はConcreteCommand.execute()の詳細を知らずに使うことができます。とにかく、何をしてくれるかはしらなくても実行してくれるのです。
これによってClient側ではReceiverを直接使うよりもコードの見た目がすっきりしますし、Receiver(実行方法の詳細を知っている側)と分離することができます。
あとは、下で実装するようにUndoが簡単に実装できるのも良い点です。
他に嬉しいことがあれば教えてください。

もう少し具体的なシチュエーション

上の定義でも十分だとは思うのですが、便利さが伝わるようにもうちょっと具体的にどう使うのか紹介していこうと思います。
具体的には以下のことをしていきます。

  • Commandインターフェースにundo()メソッドを付け加えます。
  • たいていのReceiverにはつけたり消したりする操作があると思うのでInvokerのcommandsをonCommandsoffCommandsに分離します。

ただし、そんなに難しいことをするわけでもないですし、今までの話を理解していれば問題ないと思うので、めんどくさかったら読まなくても大丈夫です。

じゃあ、やっていきます。

ベンダー固有のクラスたち(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の実装

まずは照明です。以下のようにLightOnCommandLightOffCommandを実装します。

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()

感想

キーボードの入力待ちとかコマンドパターン使って実装するときれいにまとまるんだろうなと思いました。
割と色んなところで使えそうではありますが、面白味はなくなってきた気がします。最初のストラテジーパターンとかオブザーバブルパターンとかは面白く感じたんだけどもオブジェクト指向に慣れてきて感動が少なくなってきたんだろうか・・・

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4