Hacker News new | ask | show | jobs
by munificent 1471 days ago
I've implemented undo/redo a number of times. I agree whole-heartedly that the Command pattern is the way to do it. Undo has a reputation for being difficult, but my experience is that it's smooth sailing as long as you build it into the tool on day 1. If the app is architected around undo, it's easy, but trying to retrofit it onto an application later is always a nightmare.

This is very similar to the experience of writing a networked multiplayer game. If you build a single-player game and try to bolt multiplayer on later, you're gonna have a bad time. But if you design it for multiplayer initially and treat single-player mode as essentially just a multiplayer game with only one player, it's relatively easy.

I think both of these come down to the same core issue: mutating state.

When playing a game, or editing a document, you are mutating some state. To support undo, you need to capture all of those mutations so that you can reverse them. To support multiplayer, you need to capture them so that they can be synchronized with the other players.

It's trivially easy in most programs to just directly mutate some state by setting fields or by calling methods that do that under the hood. So, if you just start coding, you will end up with mutation happening everywhere. At that point, you have already lost.

But if you design your application for undo, you isolate the document state from the rest of the application so that the only way to modify it is by going through the undo/redo mechanism. (In other words, the only way to apply a change is to create a Command object which does it on your behalf.) Likewise, if you design for multiplayer, you'll build a separation between game state and the rest of the application. Then the program has a well-defined interface that can modify the state.

Once all mutation goes through a narrow well-defined interface, it's relatively easy to grow the application over time without compromising undo or multiplayer.

But if you're adding that afterwards, you have to dig through the program to find every single piece of code that changes some state. It's hell.

3 comments

For https://curvefever.pro we actually decided to store the state of the world for undo purposes, this way you can go anywhere back in time in constant time, and to redo you execute the commands again. The big advantage is that you don't have to create an undo function for each command (which might be tricky in some cases, as many times it still involves storing state) and you don't have to iterate through/apply all the commands to go back at a specific time. To save memory, we actually only store the state every 5 ticks (so if you want go to back to tick 23, you go back to tick 20 and run the simulation forward for 3 ticks).
To be more clear, in case an "undo" of a player action has to happen, the state before that action is loaded, the action is removed and the world ticked forward again with all the commands (excepting the removed one).
This is some great nugget of knowledge, thanks for sharing. it's too bad all my programming enthusiasm is going into a 9-5
If you want more nuggets from this poster, I suggest reading his games programming book. It’s light reading (but full of great concepts and explanations) that I personally read outside my 9-5 for pleasure.

https://gameprogrammingpatterns.com/

Command-pattern is the secret sauce for the ultimate and optimal Undo functionality. Glad you're spreading the good word!