Command Interpreter

2023 12 03 head

Most digital products have a command interpreter.

External systems send requests to the system and often await an answer. A command interpreter can model this behavior.

The channels used to transmit requests and return answers are variable. Messages can be transmitted other various buses and protocols.

An operator can send a command over a CAN bus, Serial Peripherical Interface, Ethernet or Bluetooth connection.

A regular command line interpreter inputs commands as a string containing a request and associated parameters. The answer is displayed as a string.

The processed commands are the instructions executed on the machine. They are independent of the transmission layer and communication protocol.

How could you design such a versatile interface using modern software constructs from Java or C++?

Which design delegates most of the validation to the compiler [1]?

Command Descriptors and Commands

A description of a command could be encoded in a set of classes defining a bounded domain. This approach is often used in command line interpreters. It works well, but the compiler will not validate the descriptors. The solution does not well integrate with modern communication protocols such as Protocol Buffers.

A modern approach would be to encode the command universe as a sealed class hierarchy in Java.

A similar approach could be based on a variant template instance in C++.

The key concept is to encode the command structure into declarations instead of defining a domain model to describe them. The sole drawback of this approach is that you cannot dynamically add a command type. You have to declare a new class and compile the program [2].

commands

The sealed class hierarchy defines the command types and their parameters. Each command type is defined through a class definition.

Instances of a command type are commands. Each command is defined through an instance of a class.

The sealed structure guarantees that the compiler will mark all code segments needing an edition when a new command type is added. The sealed class structure and the pattern matching approach are a compiler-validated visitor pattern.

We decided to store the result of a query into an answer object. A computed answer can have a reference to the query initiating the processing.

You can pack the result into a future wrapper to support asynchronous command processing. The interpreter itself behaves as a synchronous command processor matching answers to the related query command.

Interpreters and Dispatcher

The interpreter is quite simple to realize.

It processes all commands it is in charge of. An interpreter is in charge of one or more command sets with a specific group identifier.

A second responsibility is to build a command object based on input data. It parses the input and generates the command instance representing the received data.

Object-oriented approach states that you should not mix the received data with the internal command classes. Do not inherit from any classes defined in the channel abstraction.

Domain-driven design approach clearly states the channels are interfaces to a bounded domain. No abstraction defined in the interface layer should pollute the domain model.

The dispatcher delegates the processing to multiple interpreters. Each subsystem can provide their interpreter to execute commands specific to the bounded domain. The dispatching criteria is either a group identifier or an interface marker.

command-interpreter

A channel receives and transmits command data. The channel should be able to extract the message group from a raw message.

Why should we support multiple channels to propagate commands?

An operator would input commands as text on a command line. An external software system would send commands through a bus such as Ethernet, CAN bus or Serial Peripherical Interface. Multiple channels are a requirement for a versatile command interpreter.

I recommend Protocol Buffers for binary encoding of commands, queries, and answers.

Channels

The channel decodes all commands sent through it into command instances. The channel has knowledge of the command structure. This is necessary to decode the command and dispatch it to the right interpreter.

This approach delegates the encoding and decoding of commands to messages to the channel. This approach is the standard one for bus communication.

You can implement a two-step decoding process for maximum flexibility.

The first step is to decode the message type and encoding format before dispatching the message to the right interpreter. The interpreter will decode the message payload and transform it into a command instance.

This approach shall only be used if you need to support multiple complex encoding formats due to its complexity. Most decoding libraries for JSON, proto buffers, or command line parsers are ill-suited for this approach.

The payload of the message does not need to be interpreted by the channel. This activity can be delegated to the interpreter.

Consider using a factory pattern for the decoding operations.

If you are using the Google Protocol Buffers library, you will have to describe the structure of all messages send over the channel. The library will generate the code to encode and decode the messages.

The same can be said if your transmission format is JSON and you want to validate the structure of the messages with a JSON Schema.

The above describes constraints are in line with how major encoding approaches are designed.

Thoughts

This design approach is almost codified as a design pattern. The solution is constrained by the decision to use the compiler toolchain to validate the command types and to program the processing with a pattern matching approach.

This approach melts object-orientation with functional aspects. It reflects the evolution of modern programming languages blending object-orientation and functional approaches.


1. A powerful software quality approach is to delegate validation activities to the compiler.
2. Agile and DevOps approaches mitigate the problem. You should be able to generate a new version of your application in at most a few minutes without manual activities.