Card 07/ 09
Undo That Cannot Restore the Previous State
Delete removed the text but did not keep it, so undo has nothing to put back.
A command can undo itself only if it kept what its execute destroyed. A delete that removes text without saving it, or a setter that overwrites a value without recording the old one, has an undo method that cannot work, and usually fails silently rather than raising. This card shows that bug, why it is easy to write, and the rule that prevents it.
This Delete command stores only the positions it was given. Its undo tries to put back the text between them.
def undo(self) -> None:
t = self.doc.text
self.doc.text = t[: self.start] + t[self.start : self.end] + t[self.start :]The document holds 'Hello, world', and the command deletes positions 5 to 12.
What does the document hold after the undo? Decide, then look.
class Document:
def __init__(self, text: str) -> None:
self.text = text
class Delete:
def __init__(self, doc: Document, start: int, end: int) -> None:
self.doc, self.start, self.end = doc, start, end
def execute(self) -> None:
t = self.doc.text
self.doc.text = t[: self.start] + t[self.end :]
def undo(self) -> None:
t = self.doc.text
self.doc.text = t[: self.start] + t[self.start : self.end] + t[self.start :]
doc = Document("Hello, world")
command = Delete(doc, 5, 12)
command.execute()
print("after delete:", repr(doc.text))
command.undo()
print("after undo: ", repr(doc.text))after delete: 'Hello'
after undo: 'Hello'The document stays 'Hello'. After the delete, positions 5 to 12 are past the end of the text, so the slice t[5:12] is empty, and undo inserts nothing. The text it needed is gone from the document, and the command never kept a copy.
The mistake is to write undo as though it could read the old state from the receiver. After execute, the receiver holds only the new state; anything overwritten or removed exists nowhere unless the command saved it. The cost is an undo button that appears to work in a quick test of inserts and loses text without any warning on the first delete.
The fix is to save, inside execute, exactly what the change will destroy, and restore it in undo.
def execute(self) -> None:
t = self.doc.text
self.removed = t[self.start : self.end]
self.doc.text = t[: self.start] + t[self.end :]A useful rule when writing any command: for each field of the receiver that execute changes, ask whether undo can compute the old value from the command's own fields. Where it cannot, the command stores the old value before changing it.
← → move through sections and cards