DEV Community

Cover image for Introducing re-agent: Java undo/redo & command delegation
Jake Lewis
Jake Lewis

Posted on • Updated on

Introducing re-agent: Java undo/redo & command delegation

As a preface, this is my first library, and somewhat embarrassingly, my first blog post 😱. Any feedback would be greatly appreciated! All gotta start somewhere right?

While procrastinating over my degree, I realised that my final project contained some useful command delegation code that could make for a nice little library. With no further ado, let me introduce...

re-agent

For the readme (it's down below as well) and code, checkout:

GitHub logo logdyn / re-agent

A Java command delegation library, providing undo/redo functionality

Logdyn re-agent Build Status

A Java command delegation & undo-redo library.

Features

  • Undo & Redo
  • Loosely coupled Command Pattern
  • Listener support

Installation

The current release version can be found on the maven central repository. To use it add the following dependency to your pom.xml:

<dependency>
  <groupId>com.logdyn</groupId>
  <artifactId>re-agent</artifactId>
  <version>1.2</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

The release can also be found on the GitHub releases.

How to Use

Commands

A realization of the Command interface is used to trigger an event in the subscribed Executor. Command's do not specify execution behaviour, rather they are used to trigger an event and to capture any information that will be needed for that event.

class ExampleCommand implements UndoableCommand {

    @Override
    public String getName() {
        return "Example Undoable Command";
    }
}
Enter fullscreen mode Exit fullscreen mode

There are also UndoableCommand's, which can be…

Background

This library started out as the undo / redo controller for another project, and was adapted by myself and my coder-in-crime Matt as part of our collaborative work on GitHub.

The product had to do three things:

  • Make command delegation easy
  • Make undo/redo easy
  • Be as loosely coupled as possible

So what do I mean by command delegation?

Say you have a program that opens a file, and provides you with two different editor windows. Each editor window will obviously have it's own controller, but the file operations can likely be shared between them. In our case, the editors don't particularly care how the file is opened, just that they don't get forgotten... editor.populate(text) will do them nicely.

A FileMenuController will know when you want to open a file, but we want our editors to know too, but we don't necessarily want the controller to know about the editors. This is where our command delegator comes in.

The editors will subscribe to the command delegator, saying that they want to know about certain events, which in this case is a FileOpenCommand. When the file controller opens a file it will publish a FileOpenCommand to the delegator, with whatever information necessary to open the file. The command delegator will forward this command on to the editors, and they will worry about the execution of the command.

Ta-da! Successful delegation 😄 but we're not done yet...

Undo & Redo

As anyone who has implemented an undo/redo system before will know, possibly the trickiest aspect is reverting state. To be able to undo an action, you need methods that act in a RESTful manner (behave the same regardless of state) or you need to store the state you want to go back to.

Being the magnanimous developers we are, we decided we didn't care, and you should be able to do what you want. Yay, freedom! In this vein, the process of undo/redo uses four entities: delegator, publisher, command, and executor. Commands may or may not contain state, but they are always fairly tightly coupled to their executor

  • Delegator: handles executor subscription, and passes around calls for DO, UNDO, and REDO to an executor of a command
  • Publisher: anything that sends a command to the delegator
  • Command: basically a data packet, might have utility methods to be used by the executor
  • Executor: Controls how commands are 'done', with the option of implementing undoing and redoing

So how do I install it?

Just add it as a dependency on Maven! (The always brilliant Jenkov tutorials have you covered)

<dependency>
  <groupId>com.logdyn</groupId>
  <artifactId>re-agent</artifactId>
  <version>1.2</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

So how do I use it?

I'm lazy so this is coming straight out of the readme, but I'm eager for suggestions to improve it 👍

Commands

A realization of the Command interface is used to trigger an event in the subscribed Executor. Command's do not specify execution behaviour, rather they are used to trigger an event and to capture any information that will be needed for that event.

class ExampleCommand implements UndoableCommand {

    @Override
    public String getName() {
        return "Example Undoable Command";
    }
}
Enter fullscreen mode Exit fullscreen mode

There are also UndoableCommand's, which can be executed by any Executor, although if you want to make use of undo/redo an UndoableExecutor must be used. By default the reexecute() method calls execute(), although this can be overridden. An UndoableCommand must provide data necessary for undoing an action, as well as the initial execution.

Executors

A realization of the Executor interface is used to execute specific behaviour when a Command has been published.

class ExampleExecutor implements UndoableExecutor<ExampleCommand> {
    @Override
    public void execute(ExampleCommand command) {
        System.out.println("Hello, World!");
    }

    @Override
    public void unexecute(ExampleCommand command) {
        System.out.println("Goodbye, World!");
    }

    @Override
    public void reexecute(ExampleCommand command) {
        System.out.println("Hello again, World!");
    }
}
Enter fullscreen mode Exit fullscreen mode
Subscribing to a Command

Subscribing to a Command requires you to specify an Executor and the Command that it will execute. An Executor will execute the type of Command it is subscribed to, or any sub-class of that Command. As a result of this, only one Executor of this type or sub-type of Command.

CommandDelegator.getINSTANCE().subscribe(new ExampleExecutor(), ExampleCommand.class);
Enter fullscreen mode Exit fullscreen mode
Publishing (Doing) a Command

Publishing a Command is as simple as passing it into the publish() method. This will then call the execute() method of the relevant Executor class. The call to execute() will be on the same thread as the call to publish, this means that if you want to initiate a task to run in the background it must be published from the background.

If a published Command is not undoable, it will clear the current undo history. Likewise if you have undone a Command and a new one is published, the redo history will be cleared.

CommandDelegator.getINSTANCE().publish(new ExampleCommand());
Enter fullscreen mode Exit fullscreen mode
Undo & Redo

Undo & Redo are method calls on the CommandDelegator. This operates in the same manner as the publish() method, it will call the unexecute() or reexecute() method of the relevant Executor class.

CommandDelegator.getINSTANCE().undo();
CommandDelegator.getINSTANCE().redo();
Enter fullscreen mode Exit fullscreen mode

All done!

Any questions or suggestions would be great, either here 💬, my Twitter, or email me at jake@logdyn.com

Top comments (0)