Command pattern

Introduction

If you look at the Diffgram source code, you will find two different implementations of the Command pattern (one here and another one here).

This is related to the fact that we constantly improving our product and now migrating to the new implementation of the pattern. The main differences in the implementation are:

  • The new command pattern is implemented 100% using TypeScript, while the old implementation uses JS and TS alongside
  • The old command pattern is tightly coupled to the implementation of the component itself (for example, you had to pass a reference to the component in contractor and it had to store instance list in instance_list property if the name didn't match - that would crash your app). With the new pattern implementation, our classes are completely decoupled from the component implementation, and it requires to pass a History class that implements HistoryInterface
  • Also, we change our approach to the creation of the command. In the old implementation we had only 2 commands: create and update, and the update command was handling all the actions (delete, update label, etc.). In the new implementation, we still keep those two commands, however now we changed the philosophy of command implementation and now you can find commands that are handling only one action: create, delete and update_label
  • Possibility to undo/redo for multiple instances. If you go to check our text interface you will see that some operations require an update of a few instances at the time (for example, when you delete text_token instance and it has linked relations - you need to delete them at the same time, and for undo - restore at the same time, another example is bulk label functionality)

As an addition to all the listed above, we significantly improved the testability of all the classes and got the possibility to test them win isolation, without knowing anything about the implementation of the component.

Using a new implementation in new annotation interfaces

First of all, on the mount you need to initialize two classes History and CommandManager:

// Should be set during mounting
this.history = new History()
this.command_manager = new CommandManager(this.new_history)

After this you need to initialize you list of instances, by using InstanceList class:

this.instance_list = new InstanceList(instance_list)

Note that InstanceList class can be initialized without initial instances or with an empty array

After you perform some actions, create a command and pass it to the command manager(note that here we are using create command, but others act similarly):

const command = new CreateInstanceCommand(newly_created_instances, this.new_instance_list)
this.new_command_manager.executeCommand(command)

The History class mentioned above has two boolean properties that show if undo/redo operations are possible. if you want to trigger undo or redo, it's also super easy to do through the instance of History class:

//Check if undo/redo possible 
this.history.undo_posible
this.history.redo_posible

// Trigger undo/redo
this.command_manager.undo()
this.command_manager.redo()

Creating a new command

To create a new command, open frontend/src/helpers/command/available_commands and create a your_command_name.ts file. Inside the new file, import the abstract Command class and implement two required methods:

import { Command } from "../command"

export default class YourNameCommand extends Command {
    execute(){
        // Your implementation of execute
    };

    undo() {
        // Your implementation of undo
    };
}

After the command is implemented, import it to index.ts file and add it to the exports:

import YourNameCommand from "./your_command_name"

export {
    ...,
    YourNameCommand
};