DEV Community

Cover image for Command Design Pattern in Go
Tomas Sirio
Tomas Sirio

Posted on

Command Design Pattern in Go

Hey there!

This week we are focusing on the Command Design Pattern. I find this pattern extremely useful for CLI applications. In order for the user to have to interact as little as possible with the raw objects, we are going to provide them an interface that will receive and execute the wanted command on the objects.

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) bool {
    if b.balance-amount >= overdraftLimit {
        b.balance -= amount

    }
    fmt.Println("Withdrew:", amount, "\b, balance is now", b.balance)
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

But the user shouldn't be the one who takes the 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
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

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}
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

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)
    }
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

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

In order to test this, let's create a bank account and two commands. One to Deposit some money and one to withdrew as well:

func main() {
    ba := BankAccount{}
    cmd := NewBankAccountCommand(&ba, Deposit, 100)
    cmd.Call()
    fmt.Println(ba)
    cmd2 := NewBankAccountCommand(&ba, Withdraw, 25)
    cmd2.Call()
    fmt.Println(ba)
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini
Deposited: 100, balance is now 100
{100}
Withdrew: 25, balance is now 75
{75}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

Undo commands

Since we have a Command interface. Let's imagine the possibility of having an Undo command with which we are going to take our changes back. So let's add an Undo() signature to our interface:

type Command interface {
    Call()
    Undo()
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

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 let's modify the methods which might fail:

type BankAccountCommand struct {
    account  *BankAccount
    action   Action
    amount   int
    succeded bool
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini
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)
    }
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini
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
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

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)
    }
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

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)
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini
Deposited: 100, balance is now 100
{100}
Withdrew: 25, balance is now 75
{75}
Deposited: 25, balance is now 100
{100}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

Composite Command

Thing is, without transfers between accounts, this interface is barely useful, so let's spice things 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 by now is saturated, thus not a good interface, but cope with me on this one):

type Command interface {
    Call()
    Undo()
    Succeded() bool
    SetSucceded(value bool)
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini
func (b *BankAccountCommand) Succeded() bool {
    return b.succeded
}

func (b *BankAccountCommand) SetSucceded(value bool) {
    b.succeded = value
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

These methods are really simple, mostly a getter and a setter for the succeded 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
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

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()
    }
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini
// 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()
    }
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini
// 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
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini
// 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)
    }
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

Ok, so everything is mostly settled. So 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
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

Unfortunately, we need another change. Imagine that one of the commands fails while doing a Money Transfer. 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)
        }
    }
}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

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)
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

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

Withdrew: 25, balance is now 75
Deposited: 25, balance is now 25
{75} {25}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

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)
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini
Withdrew: 25, balance is now 75
Deposited: 25, balance is now 25
{75} {25}
Withdrew: 25, balance is now 0
Deposited: 25, balance is now 100
{100} {0}
πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini πŸ† Live Soccer TV: tomassirio/command design pattern in go lpl - Pertandingan Hari Ini

And that's it for this week. Look how little our main code is right now and how easy our interface looks for the user.

I hope that you all have some Happy Holidays and a Happy Coding too

Top comments (0)