The Command Pattern [BETA]

You would figure that the namesake of this website would have a post about it. Welcome to the Command Pattern. I first read about it in the famous Gang of Four book [Gamma et al]

Notes

This design pattern is useful to implement scripting and undo/redo behavior.

Updates:

Prerequisites

Explanations & Definitions

If I had to describe it to another programmer or technically adept person, I would say its just functions. Functions all the way down. But the special part is how you handle state and the ability to undo state changes. Or, script it!

Let us let the machine try and explain it:

Imagine you’re playing with a toy robot. Instead of controlling the robot directly, you have a special remote control. Each button on the remote is like a “command” that tells the robot what to do.

When you press a button, the remote doesn’t actually make the robot move. Instead, it sends a message to the robot saying “do this action”. The robot then follows that instruction.

This is kind of like how the Command pattern works in computer programs. Instead of one part of the program directly telling another part what to do, it creates a “command” that can be sent and followed later. This makes it easier to add new commands, undo actions, or even save a list of commands to do later.

– Claude3.5 Sonnet

Implementation Motivation

This is my implementation, there are many like this but this what I would consider an easy to teach and implement solution. You can always add features, ERRRR… complications, later 🙂

State or “World”

State is part of most developer’s lives, but really you can think about this as the data you care about in the program. It could be your air-line reservation, forum post, baby photos. You don’t want your data to be lost. State is your data or world, and often like the real-world the state is hierarchical.

Mutable World or Immutable World?

I’ll skip right past the mutable world. It is useful for large simulations, but that is another topic and I will leave this for another moment. Sometimes the memory overhead actually matters, because you cannot afford a machine with that much RAM. If you have limited hardware, consider a hybrid-mutable-immutable-world.

I consider Immutable World the cleanest approach and most ideal for scripting.

The Immutable World

new_world = get_next_state(world, command) 

So it is an agreement that any time the “World” will be mutated, we make a copy and return new state. We never mutate state.

We will also keep all of our state objects in a stack linked to their commands that have been performed.

all_states = [] # list of all worlds ever

Function Interfaces

Finite State Machines (FSM) are very important for system design. In this scenario we will ensure to call our functions on our state/world objects to maintain consistent design. From this point on we will refer to world as state as it is the preferred term of the author.

It helps to have an actual problem to solve to teach the command pattern. We will start with the humble calculator.

Command Processor

# CRL LAB - COMMAND PATTERN - A
# COPYRIGHT 2024 CRYPTIDE RESEARCH - ALL RIGHTS RESERVED
# LICENSED UNDER GPL3

# generic processor that is slightly tuned for us to teach
class CommandProcessor:
    def __init__(self, config):
        self.config = config
        self.history = [config.get_default_cmd()]
        self._value = config.get_default_value()

    def exec(self, op, a, b=None):
        self._value, cmd_link = self.config.exec(op, a, b)
        self.history.append((self._value, cmd_link))

        return self._value

    def clear(self):
        self.history.clear()
        self.config.clear()

    def undo(self):
        if len(self.history) == 0:
            raise ValueError("No operations to undo")

        undo_value, undo_cmd = self.history.pop()

        new_value, _ = self.history[-1] #look at last one and get value
        self._value = new_value

        #this following approach is half working for side-effect based systems example
        #new_value = self.config.undo(self.history[-1])
        #but what else do we need?

        return self._value


    def value(self):
        return self._value

Note that this processor implementation is just a starting point. The core ingredients. As we develop our application we may dream up new features of the interfaces.

You can note that there is no specific implementation baked into the design. It is meant to operate as a shim or a harness to run specific state/command patterns.

We have a configuration object passed in which contains the domain specific code we will be wrapping in the façade. A history list and helper value_history to aid in debugging.

This is the core function, this executes a command. a, b are arguments to the command. op is the operation to be performed.

History is preserved and we return the result of the execution is returned for easy of use of the API.

Calculator Commando

from enum import Enum

# simple calculator command processor
class Calculator:
    def __init__(self):
        self._value = 0

    # for referencing the payload
    class CALC_ROW(Enum):
        OP = 0
        A = 1
        B = 2
        RESULT = 3

    # public interface
    def exec(self, op, a, b=None):
      print(f"executing {op} {a} {b}")
      coms = self._get_command_map()
      if op not in coms:
        raise ValueError(f"Invalid operation: {op}")
      self._value = coms[op](a, b)

      return (self._value, (op, a, b, self._value))

    def value(self):
        return self._value

    def undo(self, tuple4):
        op, b, a, _ = tuple4 # pull arguments in reverse order!!!

        if a is None:
            a = self._value

        self._value = self._get_undo_map(op)(a, b)
        return (self._value, (op, a, b, self._value))

    def reset(self):
        self._value = self.get_default_value()

    def get_default_value(self):
        return 0
        
    def get_default_cmd(self):
        return (self.get_default_value(), ('+', 0, 0, 0))

    def clear(self):
        self._value = 0

    # private implementation

    def _add(self, a, b):
        if b is None:
            b = self._value
        return a + b

    def _multiply(self, a, b):
        if b is None:
            b = self._value
        return a * b

    def _subtract(self, a, b):
        if b is None:
            b = self._value
        return a - b

    def _divide(self, a, b):
        if b is None:
            b = self._value
        if b == 0:
            raise ValueError("Division by zero is not allowed")
        return a / b

    def _get_command_map(self):
      return {
          '+': self._add,
          '*': self._multiply,
          '-': self._subtract,
          '/': self._divide,
      }

    def _get_undo_map(self, op:str):

          switch = {
              '+': self._subtract,
              '*': self._divide,
              '-': self._add,
              '/': self._multiply
          }
          return switch[op]

This is the domain specific part of the code. It would change depending on the task that the application programmer might need. Example usage of the latest code:

base_calc = Calculator()
x0 = base_calc.exec('+', 1, 2)
print('using implementation itself')
print(x0) # (3, ('+', 1, 2, 3))
print(base_calc.value()) # 3
x1 = base_calc.exec('+', 1) 
print(x1) # (4, ('+', 1, None, 4))
print(base_calc.value()) # 4

At this point you might be wondering why we’ve done all this boiler plate. The final details:

# you can see the (value, (op, a, b, value))
# data structure here as an artifact to help with undo
# clear up and do the real Command Pattern

base_calc.clear()
print(base_calc.value())

print('wrap it in command processor')
calc = CommandProcessor(base_calc)
calc.exec('-', 10, 4)
print(calc.history[-1])
print(calc.value())

calc.exec('+', 1)
print(calc.history[-1])
print(calc.value())

print('undo last calc...')
calc.undo()
print(calc.value())
print('undo last calc...')

calc.undo()
print(calc.value())
0
wrap it in command processor
executing - 10 4
(6, ('-', 10, 4, 6))
6
executing + 1 None
(7, ('+', 1, None, 7))
7
undo last calc...
6
undo last calc...
0

Discussion

With the encoding of the inverse mapping of the operations between add/subtract and divide/multiply we can safely undo any operation but applying it in reverse. Not all commando’s will have this luxury. Think if there is a function that creates a file on disk. The undo of that command should do what?

DELETE THE FILE

Ok. we can end here for this section for the lab.

There will be an on-going discussion, but part 2 of this lab will transition to image manipulation and scripting support.

Thanks for reading!

The Tool Maker’s Dilemma

A humorous illustration entitled "The Tool Maker's Dilemma" with the main person not so freaked out, replace speech bubbles with nonsense slogans, make it higher resolution if possible, make it appear like an old style 70s poster, and make it more technology based

Programmers love their tools. It can be a fetish. How many tools is too many tools between your software and your customer?

Sometimes having more tools in your toolbox is a hinderance not an asset. Organizational breakdowns. Lost tools. Normally there are a few key tools you always have close by if needed.

As engineers or programmers it is often our bench software: Matlab, python, various editors and programming environments. Everyone loves good tools.

Editors and IDEs

Typing is so important. The speed of thought into your editor, the interface between man and machine. Keyboard shortcuts, specialized editors, beyond qwerty… they all tie you to a configuration. It is not a deal-breaker, but its important to reduce the friction between you and your data entry point. But the more you configure the harder it is to work on another’s system.

Do I have to mention emacs and vim? Yes. These ancient editors require months of training to use effectively and perhaps years to master.

VIM and EMACS fighting in a ring in a hellscape outside in a post-apocalyptic setting with a mortal kombat style

1970s computing was defined by big iron machines or mainframes. Big servers, dumb clients. PDP-10’s main storage was tape machines and TECO was the editor of choice. A single page editor. This was great for editing LISP programs and eventually an Editor of Macros (Emacs) was developed mid-70s. Somehow it is still the preferred editor of many programmers.

On the other end of the editor spectrum was “line editors” which were more attuned to working on teletypes. Teletypes are typewriters with serial links. Could it be over a phone line? Vi is a visual line editor created shortly after Emacs. Its modern version was rebooted in the 1980s as Vim. Again, the editor is still used at large (in 2024) and there is a large key-binding extension/plugin for most Integrated Development Environments (IDEs).

Is it worth it to learn emacs or vim? Yes, but that is my opinion and I wouldn’t suggest learning to ship a product and learn vim at same time. It is a lifestyle change that seems less and less important these days. It’s a fun distraction from real work.

Programming outside of a UNIX environment is largely done in a IDE. Visual Studio being the de-facto standard for C++ development. As opposed to makefiles and shell scripts, the idea of an IDE is everything is in one spot. Editor, Compiler, Debugger. Very convenient, you can setup IDE like features in emacs and vim, you could say emacs is a IDE for LISP. Borland IDEs have a special place in my heart as this is where it all started for me, outside of QBasic of course.

The point I’m trying to make is, you can approximate one with the other. Your customer doesn’t care which IDE or text editor you use. The language, framework, etc is a builder’s artifact. A tool mark.

Custom Keybindings & Hardware

If you listen intently you can hear the crackle of mechanical keyboards chording and creating arpeggiations of clicks echoing. Mechanical Keyboards are nice, but I think if you wanted to become inept on the other peoples machines. Just learn DVORAK or COLMAN instead of QWERTY. Still awaiting learning how to use my ErgoDox.

The command interface or how you move around the file are what I would say splits emacs and vim editors. Modal (vim) vs normal. Let’s leave that for another post. This is all to say the mouse is not the most important tool of a programmer when writing code. Your hands re on your keyboard.

Some people (ahem) have even added a foot pedal for shift or other keyboard strokes.

Vendor Software

On a unix you are almost guaranteed to have a C compiler. I think that is POSIX standard. Your distribution will provide it. Microsoft provides the Visual Studio. One you thing you can do is make these IDEs emulate emacs or vim. I do this for vim in both Visual Studio (Proper) and VS Code. Its pretty close and I get some breakpoints for free. On Linux I’ve heard good things about Qt Creator as an IDE.

You may have other vendors outside of your main development tool kit provided by vendor 1. Every piece of software you add beyond this point is normally business related. Libraries are the most important consideration when writing software in a reasonable amount of time. I would vet your libraries thoroughly and “vendor them in” to your main source control.

Then there is the subscription as a service (SaaS). This means you normally don’t host it yourself. Github is a perfect example of a legit reason to include a SaaS product into your development pipeline. But the real thing here is with a remote service, you run risk to disruptions you have no control over. Sometimes the website goes bankrupt. I would avoid these in your flows if possible.

Of course there is our own customization of your tools on top of this. Configurations, Plugins, Extensions, our own shell scripts. Often it’s the “dotfiles”. These build up over the years.

3 Levels

It is important for you to customize your development environment so that you can build and ship your product to customer. Often these setups can be cumbersome and fragile, but that is ok, the product doesn’t suffer from this directly. Now developer tool fatigue is a real thing. Developer ergonomics are important, but taken to the extreme, turns into a naval gazing exercise.

  • Rolling Stock – Minimum settings changed, As it was intended
  • Custom Setup – Tinkering deeper, voiding warranty
  • Fired or Getting Paid – How many apps/scripts are between you and your product or customer?

I like to roll stock when I can. Just because you CAN customize and extend, doesn’t mean you should. Is it worth it to add this new complication to my codebase?

Fired or Getting Paid? — I think this is an important question, will you create an unmaintainable mess while getting paid and then get fired because the business cannot support the liability you have created, or will you keep it running as best you can and improve the system reliability slowly over time?

endcap

I may or may have not written a kernel extension for windows that maps caps lock to escape. The autohotkey hack wasn’t fast enough. I love the idea of “jig’s” I love making tools, but sometimes it gets me into deep trouble…