こんにちは、KOUKIです。
この記事では、デザインパターンの一つであるCommandパターンについて紹介します。
前回は、Commandパターンの概要に触れ、サンプルコードと共に実装方法をみていきました。
本記事では、複数のCommandを組み合わせる(Composite)方法を紹介したいと思います。
前回
デザインパターンまとめ
BitCoin入出金プログラム
Bit Coinの入出金を例に、サンプルコードを実装していきます。
前回紹介したCommandパターンを実装後に、Composite Commandを適用したいと思います。
BitCoin構造体の作成
復習になりますが、Commandパターンは、オブジェクト(Go言語ではインスタンス)へ送る要求(Command)をオブジェクト化し、それを複数組み合わせて使うことができるパターンです。
そこで、まずはCommandインスタンスから指令を受け取る構造体を作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
package main import "fmt" type Bitcoin struct { balance int } func NewBitcoin(balance int) *Bitcoin { return &Bitcoin{ balance: balance, } } func (b *Bitcoin) Deposit(amount int) { b.balance += amount fmt.Println("Deposited", amount, "\b, balance is now", b.balance) } func (b *Bitcoin) Withdraw(amount int) bool { if b.balance-amount >= 0 { b.balance -= amount fmt.Println("Withdrew", amount, "\b, balance is now", b.balance) return true } return false } func (b *Bitcoin) String() { fmt.Println("Bitcoin balance is", b.balance) } func main() { b := NewBitcoin(100) // 足す b.Deposit(100) b.String() // 引く b.Withdraw(200) b.String() // balanceがマイナスの場合はスキップ b.Withdraw(100) b.String() } |
1 2 3 4 5 6 |
$ go run main.go Deposited 100, balance is now 200 Bitcoin balance is 200 Withdrew 200, balance is now 0 Bitcoin balance is 0 Bitcoin balance is 0 |
Commandパターンの適用
先ほどは、Bitcoin構造体から直接メソッドを呼び出していますが、これをCommandパターンに書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
type Command interface { Call() Undo() } type Action int const ( Deposit Action = iota Withdraw String ) type BitcoinCommand struct { bitcoin *Bitcoin action Action amount int succeeded bool } func NewBitcoinCommand( account *Bitcoin, action Action, amount int) *BitcoinCommand { return &BitcoinCommand{ bitcoin: account, action: action, amount: amount} } func (b *BitcoinCommand) Call() { switch b.action { case Deposit: b.bitcoin.Deposit(b.amount) b.succeeded = true case Withdraw: b.succeeded = b.bitcoin.Withdraw(b.amount) case String: b.bitcoin.String() b.succeeded = true } } func (b *BitcoinCommand) Undo() { if !b.succeeded { return } switch b.action { case Deposit: b.bitcoin.Withdraw(b.amount) case Withdraw: b.bitcoin.Deposit(b.amount) case String: b.bitcoin.String() } } |
ここまでは前回実装したパターンと大体同じなので、サクッと実装しました。
succeededプロパティは、後続のコマンドを実行するかしないか判定する値で、このプロパティにtrueが入っている時だけ、後続のコマンドが実行される仕様です。これは、Composite Commandで使用します。
ここまで実装したプログラムを呼び出してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
func main() { b := NewBitcoin(100) // String cmd := NewBitcoinCommand(b, String, 0) cmd.Call() // Deposit cmd2 := NewBitcoinCommand(b, Deposit, 100) cmd2.Call() cmd.Call() // Withdraw cmd3 := NewBitcoinCommand(b, Withdraw, 10) cmd3.Call() cmd.Call() // リセット cmd3.Undo() cmd.Call() } |
Deposit/Withdraw/String Commandのインスタンスを作成し、Call/Undoで処理を呼び出します。NewBitcoinCommandの第二引数はコマンドのアクションなので、コマンドごとの処理が行えます。
Actionを定義して処理を振り分けるこのパターンは、前にも実装したことがあります。結構便利です。
プログラムを実行してみましょう。
1 2 3 4 5 6 7 8 |
$ go run main.go Bitcoin balance is 100 Deposited 100, balance is now 200 Bitcoin balance is 200 Withdrew 10, balance is now 190 Bitcoin balance is 190 Deposited 10, balance is now 200 Bitcoin balance is 20 |
Composite Commandの実装
長かったですが、ようやく事前準備が終わりました。ここから、Composite Commandで実装していきます。
まず、インターフェースに以下のメソッドを追加してください。
1 2 3 4 5 6 7 |
type Command interface { Call() Undo() // 追加 Succeeded() bool SetSucceeded(value bool) } |
続いて、BitcoinCommand構造体に以下のメソッドを追加します。
1 2 3 4 5 6 7 |
func (b *BitcoinCommand) Succeeded() bool { return b.succeeded } func (b *BitcoinCommand) SetSucceeded(value bool) { b.succeeded = value } |
次に、Commandを蓄積する構造体を作成します。
1 2 3 |
type CompositeBitcoinCommand struct { commands []Command } |
この構造体には、以下のメソッドを持たせます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
func (c *CompositeBitcoinCommand) Undo() { for idx := range c.commands { c.commands[len(c.commands)-idx-1].Undo() } } func (c *CompositeBitcoinCommand) Call() { for _, cmd := range c.commands { cmd.Call() } } func (c *CompositeBitcoinCommand) Succeeded() bool { for _, cmd := range c.commands { if !cmd.Succeeded() { return false } } return true } func (c *CompositeBitcoinCommand) SetSucceeded(value bool) { for _, cmd := range c.commands { cmd.SetSucceeded(value) } } |
更に、お金を送受信する構造体を作成します。
1 2 3 4 5 |
type MoneyTransferCommand struct { CompositeBitcoinCommand from, to *Bitcoin amount int } |
この構造体のコンストラクタを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func NewMoneyTransferCommand( from *Bitcoin, to *Bitcoin, amount int) *MoneyTransferCommand { c := &MoneyTransferCommand{from: from, to: to, amount: amount} // コマンドを蓄積 c.commands = append( c.commands, NewBitcoinCommand(from, Withdraw, amount)) c.commands = append( c.commands, NewBitcoinCommand(to, Deposit, amount)) c.commands = append( c.commands, NewBitcoinCommand(to, String, 0)) c.commands = append( c.commands, NewBitcoinCommand(from, String, 0)) return c } |
コンストラクタ内で複数のコマンドをインスタンス化し、commandsプロパティに格納しました。
最後に、蓄積したコマンドを実行するCallメソッドを実装します。
1 2 3 4 5 6 7 8 9 10 11 |
func (m *MoneyTransferCommand) Call() { ok := true for _, cmd := range m.commands { if ok { cmd.Call() ok = cmd.Succeeded() } else { cmd.SetSucceeded(false) } } } |
これは、BitcoinCommand構造体のsucceededプロパティを最終的に見に行っており、Call/Undoメソッドで設定されています。
※NewBitcoinCommandは、NewMoneyTransferCommandのcommandsに複数格納されるので、それぞれのプロパティを見に行きます(わかりにくいですが)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// BitcoinCommandのCall/UndoのSwitch文でsucceededをセット func (b *BitcoinCommand) Call() { switch b.action { case Deposit: b.bitcoin.Deposit(b.amount) b.succeeded = true case Withdraw: b.succeeded = b.bitcoin.Withdraw(b.amount) case String: b.bitcoin.String() b.succeeded = true } } // Undoにはsucceededプロパティをセットしない func (b *BitcoinCommand) Undo() { if !b.succeeded { return } switch b.action { case Deposit: b.bitcoin.Withdraw(b.amount) case Withdraw: b.bitcoin.Deposit(b.amount) case String: b.bitcoin.String() } } |
このコードは、以下のように使います。
1 2 3 4 5 6 7 8 9 10 |
func main() { from := NewBitcoin(100) to := NewBitcoin(0) mtc := NewMoneyTransferCommand(from, to, 25) mtc.Call() fmt.Println("Undo....") mtc.Undo() } |
1 2 3 4 5 6 7 8 9 10 |
$ go run main.go Withdrew 25, balance is now 75 Deposited 25, balance is now 25 Bitcoin balance is 25 Bitcoin balance is 75 Undo.... Bitcoin balance is 75 Bitcoin balance is 25 Withdrew 25, balance is now 0 Deposited 25, balance is now 100 |
複数の処理を一まとめにしたいときに便利ですね。例えば、トランザクション処理に良さそうです。
まとめ
Composite Commandは、一連の処理の流れを作れるので、用途がある気がしますね。
コマンドラインツールなどで、プロセス毎の処理を作成する機会があると思います。そのプロセスを最終的にどのように処理したいかまとめる時に活躍しそうです。
それでは、また!
Go言語まとめ
ソースコード
Composite Command
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
package main import "fmt" type Bitcoin struct { balance int } func NewBitcoin(balance int) *Bitcoin { return &Bitcoin{ balance: balance, } } func (b *Bitcoin) Deposit(amount int) { b.balance += amount fmt.Println("Deposited", amount, "\b, balance is now", b.balance) } func (b *Bitcoin) Withdraw(amount int) bool { if b.balance-amount >= 0 { b.balance -= amount fmt.Println("Withdrew", amount, "\b, balance is now", b.balance) return true } return false } func (b *Bitcoin) String() { fmt.Println("Bitcoin balance is", b.balance) } type Command interface { Call() Undo() Succeeded() bool SetSucceeded(value bool) } type Action int const ( Deposit Action = iota Withdraw String ) type BitcoinCommand struct { bitcoin *Bitcoin action Action amount int succeeded bool } func NewBitcoinCommand(account *Bitcoin, action Action, amount int) *BitcoinCommand { return &BitcoinCommand{ bitcoin: account, action: action, amount: amount} } func (b *BitcoinCommand) Call() { switch b.action { case Deposit: b.bitcoin.Deposit(b.amount) b.succeeded = true case Withdraw: b.succeeded = b.bitcoin.Withdraw(b.amount) case String: b.bitcoin.String() b.succeeded = true } } func (b *BitcoinCommand) Undo() { if !b.succeeded { return } switch b.action { case Deposit: b.bitcoin.Withdraw(b.amount) case Withdraw: b.bitcoin.Deposit(b.amount) case String: b.bitcoin.String() } } func (b *BitcoinCommand) Succeeded() bool { return b.succeeded } func (b *BitcoinCommand) SetSucceeded(value bool) { b.succeeded = value } type CompositeBitcoinCommand struct { commands []Command } func (c *CompositeBitcoinCommand) Undo() { for idx := range c.commands { c.commands[len(c.commands)-idx-1].Undo() } } func (c *CompositeBitcoinCommand) Call() { for _, cmd := range c.commands { cmd.Call() } } func (c *CompositeBitcoinCommand) Succeeded() bool { for _, cmd := range c.commands { if !cmd.Succeeded() { return false } } return true } func (c *CompositeBitcoinCommand) SetSucceeded(value bool) { for _, cmd := range c.commands { cmd.SetSucceeded(value) } } type MoneyTransferCommand struct { CompositeBitcoinCommand from, to *Bitcoin amount int } func NewMoneyTransferCommand( from *Bitcoin, to *Bitcoin, amount int) *MoneyTransferCommand { c := &MoneyTransferCommand{from: from, to: to, amount: amount} // コマンドを蓄積 c.commands = append( c.commands, NewBitcoinCommand(from, Withdraw, amount)) c.commands = append( c.commands, NewBitcoinCommand(to, Deposit, amount)) c.commands = append( c.commands, NewBitcoinCommand(to, String, 0)) c.commands = append( c.commands, NewBitcoinCommand(from, String, 0)) return c } func (m *MoneyTransferCommand) Call() { ok := true for _, cmd := range m.commands { if ok { cmd.Call() ok = cmd.Succeeded() } else { cmd.SetSucceeded(false) } } } func main() { from := NewBitcoin(100) to := NewBitcoin(0) mtc := NewMoneyTransferCommand(from, to, 25) mtc.Call() fmt.Println("Undo....") mtc.Undo() } |
コメントを残す
コメントを投稿するにはログインしてください。