What is the Command Design Pattern in Go?

Command

Let’s begin with a simple bank account structure:

var overdraftLimit = -500

type BankAccount struct {
    balance int
}

func (b *BankAccount) Deposit(amount int) {
    b.balance += amount
    fmt.Println("Deposited:", amount, "\b, balance is now", b.balance)
}

func (b *BankAccount) Withdraw(amount int) {
    if b.balance-amount >= overdraftLimit {
        b.balance -= amount

    }
    fmt.Println("Withdrew:", amount, "\b, balance is now", b.balance)
}

However, the user shouldn’t be the one who takes directives over the bank account objects. So, let’s use the Command Design Pattern to establish an interface for the user to interact with the account:

type Command interface {
    Call()
}

type Action int

const (
    Deposit Action = iota
    Withdraw
)

type BankAccountCommand struct {
    account  *BankAccount
    action   Action
    amount   int
}

And, of course, we are going to need a Constructor in order to instantiate each command:

func NewBankAccountCommand(account *BankAccount, action Action, amount int) *BankAccountCommand {
    return &BankAccountCommand{account: account, action: action, amount: amount}
}

Lastly, we need our implementation of the Command interface:

func (b *BankAccountCommand) Call() {
    switch b.action {
    case Deposit:
        b.account.Deposit(b.amount)
    case Withdraw:
        b.account.Withdraw(b.amount)
    }
}

As easy as it looks, the Call() method will consist of a switch given the BankAccountCommand action as a sort of execution method.

To test this, let’s create a bank account and two commands, one to Deposit some money and one to withdraw:

func main() {
    ba := BankAccount{}
    cmd := NewBankAccountCommand(&ba, Deposit, 100)
    cmd.Call()
    fmt.Println(ba)
    cmd2 := NewBankAccountCommand(&ba, Withdraw, 25)
    cmd2.Call()
    fmt.Println(ba)
package main
import "fmt"
var overdraftLimit = -500
type BankAccount struct {
balance int
}
func (b *BankAccount) Deposit(amount int) {
b.balance += amount
fmt.Println("Deposited:", amount, "\b, balance is now", b.balance)
}
func (b *BankAccount) Withdraw(amount int) {
if b.balance-amount >= overdraftLimit {
b.balance -= amount
}
fmt.Println("Withdrew:", amount, "\b, balance is now", b.balance)
}
type Command interface {
Call()
}
type Action int
const (
Deposit Action = iota
Withdraw
)
type BankAccountCommand struct {
account *BankAccount
action Action
amount int
}
func NewBankAccountCommand(account *BankAccount, action Action, amount int) *BankAccountCommand {
return &BankAccountCommand{account: account, action: action, amount: amount}
}
func (b *BankAccountCommand) Call() {
switch b.action {
case Deposit:
b.account.Deposit(b.amount)
case Withdraw:
b.account.Withdraw(b.amount)
}
}
func main() {
ba := BankAccount{}
cmd := NewBankAccountCommand(&ba, Deposit, 100)
cmd.Call()
fmt.Println(ba)
cmd2 := NewBankAccountCommand(&ba, Withdraw, 25)
cmd2.Call()
fmt.Println(ba)
}

Undo commands

Since we have a Command interface, let’s imagine the possibility of having an Undo command that we are going to use take our changes back. So, let’s add an Undo() signature to our interface:

type Command interface {
    Call()
    Undo()
}

What we should take into account is that the Undo method will only be able to be called if the previous command was successful; so, let’s add a boolean attribute to our bank account command and then modify the methods that might fail:

type BankAccountCommand struct {
    account  *BankAccount
    action   Action
    amount   int
    succeded bool
}
func (b *BankAccountCommand) Call() {
    switch b.action {
    case Deposit:
        b.account.Deposit(b.amount)
        b.succeded = true
    case Withdraw:
        b.succeded = b.account.Withdraw(b.amount)
    }
}
func (b *BankAccount) Withdraw(amount int) bool {
    if b.balance-amount >= overdraftLimit {
        b.balance -= amount
        fmt.Println("Withdrew:", amount, "\b, balance is now", b.balance)
        return true
    }
    return false
}

To show a simple implementation of the Undo command, let’s assume that a withdrawal is the reverse operation of the deposit command and vice-versa:

func (b *BankAccountCommand) Undo() {
    if !b.succeded {
        return
    }
    switch b.action {
    case Deposit:
        b.account.Withdraw(b.amount)
    case Withdraw:
        b.account.Deposit(b.amount)
    }
}

The brief example would look like this:

func main() {
    ba := BankAccount{}
    cmd := NewBankAccountCommand(&ba, Deposit, 100)
    cmd.Call()
    fmt.Println(ba)
    cmd2 := NewBankAccountCommand(&ba, Withdraw, 25)
    cmd2.Call()
    fmt.Println(ba)
    cmd2.Undo()
    fmt.Println(ba)
}
package main
import "fmt"
var overdraftLimit = -500
type BankAccount struct {
balance int
}
type Command interface {
Call()
Undo()
}
type BankAccountCommand struct {
account *BankAccount
action Action
amount int
succeded bool
}
func (b *BankAccount) Deposit(amount int) {
b.balance += amount
fmt.Println("Deposited:", amount, "\b, balance is now", b.balance)
}
func (b *BankAccount) Withdraw(amount int) bool {
if b.balance-amount >= overdraftLimit {
b.balance -= amount
fmt.Println("Withdrew:", amount, "\b, balance is now", b.balance)
return true
}
return false
}
type Action int
const (
Deposit Action = iota
Withdraw
)
func NewBankAccountCommand(account *BankAccount, action Action, amount int) *BankAccountCommand {
return &BankAccountCommand{account: account, action: action, amount: amount}
}
func (b *BankAccountCommand) Call() {
switch b.action {
case Deposit:
b.account.Deposit(b.amount)
b.succeded = true
case Withdraw:
b.succeded = b.account.Withdraw(b.amount)
}
}
func (b *BankAccountCommand) Undo() {
if !b.succeded {
return
}
switch b.action {
case Deposit:
b.account.Withdraw(b.amount)
case Withdraw:
b.account.Deposit(b.amount)
}
}
func main() {
ba := BankAccount{}
cmd := NewBankAccountCommand(&ba, Deposit, 100)
cmd.Call()
fmt.Println(ba)
cmd2 := NewBankAccountCommand(&ba, Withdraw, 25)
cmd2.Call()
fmt.Println(ba)
cmd2.Undo()
fmt.Println(ba)
}

Composite command

The thing is that, without transfers between accounts, this interface is barely useful. So, let’s spice things up a little bit in order to be able to transfer money between accounts.

We are going to add two new methods to the command interface (I’m well aware that this interface is saturated by now and thus not a good interface, but stay with me on this one):

type Command interface {
    Call()
    Undo()
    Succeded() bool
    SetSucceded(value bool)
}
func (b *BankAccountCommand) Succeded() bool {
    return b.succeded
}

func (b *BankAccountCommand) SetSucceded(value bool) {
    b.succeded = value
}

These methods are really simple – they are mostly a getter and a setter for the succeed attribute on the BankAccountCommand.

What we will add in order to transfer money is a composite command struct that will store commands to be executed:

type CompositeBankAccountCommand struct {
    commands []Command
}

Now, we have to implement all of the interface methods so that our struct belongs to the Command interface:

// The call method will cycle through all the commands and execute their Call method
func (c *CompositeBankAccountCommand) Call() {
    for _, cmd := range c.commands {
        cmd.Call()
    }
}
// The Undo method will cycle backwards through all the commands and Undo them
func (c *CompositeBankAccountCommand) Undo() {
    for idx := range c.commands {
        c.commands[len(c.commands)-idx-1].Undo()
    }
}
// The Succeded Getter will ask if there's at least one failed command and return false, otherwise everything is Ok
func (c *CompositeBankAccountCommand) Succeded() bool {
    for _, cmd := range c.commands {
        if !cmd.Succeded() {
            return false
        }
    }
    return true
}
// The Succeded Setter will set succeded value with the operations status
func (c *CompositeBankAccountCommand) SetSucceded(value bool) {
    for _, cmd := range c.commands {
        cmd.SetSucceded(value)
    }
}

Ok, now that everything is mostly settled, let’s create a MoneyTransfer struct with its constructor to let users transfer money among themselves:

type MoneyTransferCommand struct {
    CompositeBankAccountCommand
    from, to *BankAccount
    amount   int
}

func NewMoneyTransferCommand(from *BankAccount, to *BankAccount, amount int) *MoneyTransferCommand {
    c := &MoneyTransferCommand{from: from, to: to, amount: amount}
    c.commands = append(c.commands,
        NewBankAccountCommand(from, Withdraw, amount))
    c.commands = append(c.commands,
        NewBankAccountCommand(to, Deposit, amount))
    return c
}

Unfortunately, we need another change. Imagine that one of the commands fails while doing a Money Transfer. In this case, we would need to undo the operations. So, let’s implement a new Method for the MoneyTransfer struct:

func (m *MoneyTransferCommand) Call() {
    ok := true
    for _, cmd := range m.commands {
        if ok {
            cmd.Call()
            ok = cmd.Succeded()
        } else {
            cmd.SetSucceded(false)
        }
    }
}

Ok, everything is settled! Now, let’s run this program:

from := BankAccount{100}
    to := BankAccount{0}
    mtc := NewMoneyTransferCommand(&from, &to, 25)

    mtc.Call()
    fmt.Println(from, to)

We define two bank accounts and a money transfer from A to B:

package main
import "fmt"
var overdraftLimit = -500
type BankAccount struct {
balance int
}
type Command interface {
Call()
Undo()
Succeded() bool
SetSucceded(value bool)
}
func (b *BankAccountCommand) Succeded() bool {
return b.succeded
}
func (b *BankAccountCommand) SetSucceded(value bool) {
b.succeded = value
}
type BankAccountCommand struct {
account *BankAccount
action Action
amount int
succeded bool
}
func (b *BankAccount) Deposit(amount int) {
b.balance += amount
fmt.Println("Deposited:", amount, "\b, balance is now", b.balance)
}
func (b *BankAccount) Withdraw(amount int) bool {
if b.balance-amount >= overdraftLimit {
b.balance -= amount
fmt.Println("Withdrew:", amount, "\b, balance is now", b.balance)
return true
}
return false
}
type Action int
const (
Deposit Action = iota
Withdraw
)
func NewBankAccountCommand(account *BankAccount, action Action, amount int) *BankAccountCommand {
return &BankAccountCommand{account: account, action: action, amount: amount}
}
func (b *BankAccountCommand) Call() {
switch b.action {
case Deposit:
b.account.Deposit(b.amount)
b.succeded = true
case Withdraw:
b.succeded = b.account.Withdraw(b.amount)
}
}
// The call method will cycle through all the commands and execute their Call method
func (c *CompositeBankAccountCommand) Call() {
for _, cmd := range c.commands {
cmd.Call()
}
}
// The Undo method will cycle backwards through all the commands and Undo them
func (c *CompositeBankAccountCommand) Undo() {
for idx := range c.commands {
c.commands[len(c.commands)-idx-1].Undo()
}
}
// The Succeded Getter will ask if there's at least one failed command and return false, otherwise everything is Ok
func (c *CompositeBankAccountCommand) Succeded() bool {
for _, cmd := range c.commands {
if !cmd.Succeded() {
return false
}
}
return true
}
// The Succeded Setter will set succeded value with the operations status
func (c *CompositeBankAccountCommand) SetSucceded(value bool) {
for _, cmd := range c.commands {
cmd.SetSucceded(value)
}
}
type MoneyTransferCommand struct {
CompositeBankAccountCommand
from, to *BankAccount
amount int
}
func NewMoneyTransferCommand(from *BankAccount, to *BankAccount, amount int) *MoneyTransferCommand {
c := &MoneyTransferCommand{from: from, to: to, amount: amount}
c.commands = append(c.commands,
NewBankAccountCommand(from, Withdraw, amount))
c.commands = append(c.commands,
NewBankAccountCommand(to, Deposit, amount))
return c
}
func (m *MoneyTransferCommand) Call() {
ok := true
for _, cmd := range m.commands {
if ok {
cmd.Call()
ok = cmd.Succeded()
} else {
cmd.SetSucceded(false)
}
}
}
func (b *BankAccountCommand) Undo() {
if !b.succeded {
return
}
switch b.action {
case Deposit:
b.account.Withdraw(b.amount)
case Withdraw:
b.account.Deposit(b.amount)
}
}
type CompositeBankAccountCommand struct {
commands []Command
}
func main() {
from := BankAccount{100}
to := BankAccount{0}
mtc := NewMoneyTransferCommand(&from, &to, 25)
mtc.Call()
fmt.Println(from, to)
}

And if we wanted to undo that money transfer, we would only have to add an Undo command at the end:

from := BankAccount{100}
    to := BankAccount{0}
    mtc := NewMoneyTransferCommand(&from, &to, 25)

    mtc.Call()
    fmt.Println(from, to)

    mtc.Undo()
    fmt.Println(from, to)
package main
import "fmt"
var overdraftLimit = -500
type BankAccount struct {
balance int
}
type Command interface {
Call()
Undo()
Succeded() bool
SetSucceded(value bool)
}
func (b *BankAccountCommand) Succeded() bool {
return b.succeded
}
func (b *BankAccountCommand) SetSucceded(value bool) {
b.succeded = value
}
type BankAccountCommand struct {
account *BankAccount
action Action
amount int
succeded bool
}
func (b *BankAccount) Deposit(amount int) {
b.balance += amount
fmt.Println("Deposited:", amount, "\b, balance is now", b.balance)
}
func (b *BankAccount) Withdraw(amount int) bool {
if b.balance-amount >= overdraftLimit {
b.balance -= amount
fmt.Println("Withdrew:", amount, "\b, balance is now", b.balance)
return true
}
return false
}
type Action int
const (
Deposit Action = iota
Withdraw
)
func NewBankAccountCommand(account *BankAccount, action Action, amount int) *BankAccountCommand {
return &BankAccountCommand{account: account, action: action, amount: amount}
}
func (b *BankAccountCommand) Call() {
switch b.action {
case Deposit:
b.account.Deposit(b.amount)
b.succeded = true
case Withdraw:
b.succeded = b.account.Withdraw(b.amount)
}
}
// The call method will cycle through all the commands and execute their Call method
func (c *CompositeBankAccountCommand) Call() {
for _, cmd := range c.commands {
cmd.Call()
}
}
// The Undo method will cycle backwards through all the commands and Undo them
func (c *CompositeBankAccountCommand) Undo() {
for idx := range c.commands {
c.commands[len(c.commands)-idx-1].Undo()
}
}
// The Succeded Getter will ask if there's at least one failed command and return false, otherwise everything is Ok
func (c *CompositeBankAccountCommand) Succeded() bool {
for _, cmd := range c.commands {
if !cmd.Succeded() {
return false
}
}
return true
}
// The Succeded Setter will set succeded value with the operations status
func (c *CompositeBankAccountCommand) SetSucceded(value bool) {
for _, cmd := range c.commands {
cmd.SetSucceded(value)
}
}
type MoneyTransferCommand struct {
CompositeBankAccountCommand
from, to *BankAccount
amount int
}
func NewMoneyTransferCommand(from *BankAccount, to *BankAccount, amount int) *MoneyTransferCommand {
c := &MoneyTransferCommand{from: from, to: to, amount: amount}
c.commands = append(c.commands,
NewBankAccountCommand(from, Withdraw, amount))
c.commands = append(c.commands,
NewBankAccountCommand(to, Deposit, amount))
return c
}
func (m *MoneyTransferCommand) Call() {
ok := true
for _, cmd := range m.commands {
if ok {
cmd.Call()
ok = cmd.Succeded()
} else {
cmd.SetSucceded(false)
}
}
}
func (b *BankAccountCommand) Undo() {
if !b.succeded {
return
}
switch b.action {
case Deposit:
b.account.Withdraw(b.amount)
case Withdraw:
b.account.Deposit(b.amount)
}
}
type CompositeBankAccountCommand struct {
commands []Command
}
func main() {
from := BankAccount{100}
to := BankAccount{0}
mtc := NewMoneyTransferCommand(&from, &to, 25)
mtc.Call()
fmt.Println(from, to)
mtc.Undo()
fmt.Println(from, to)
}

And that’s it! Notice how little our main code is right now and how easy our interface looks for the user.

Free Resources

Attributions:
  1. undefined by undefined
Copyright ©2024 Educative, Inc. All rights reserved