1

I have read that using command pattern is one of the most popular ways to accomplish do/undo functionality. In fact, I have seen that it's possible to stack a bunch of actions and reverse them in order to reach a given state. However, I'm not quite sure how that can be done in Python and most of the tutorials I have read, dabble into concepts but don't show an actual implementation in Python.

Does anyone know how do/undo functionality work in Python?

For reference, this is my (naive and probably ridden with errors) code:

# command
class DrawCommand:
    def __init__(self, draw, point1, point2):
        self.draw = draw
        self.point1 = point1
        self.point2 = point2
    def execute_drawing(self):
        self.draw.execute(self.point1, self.point2)
    def execute_undrawing(self):
        self.draw.unexecute(self.point1, self.point2)
# invoker
class InvokeDrawALine:
    def command(self, command):
        self.command = command
    def click_to_draw(self):
        self.command.execute_drawing()
    def undo(self):
        self.command.execute_undrawing()
# receiver
class DrawALine:
    def execute(self, point1, point2):
        print("Draw a line from {} to {}".format(point1, point2))
    def unexecute(self, point1, point2):
        print("Erase a line from {} to {}".format(point1, point2))

instantiating as follows:

invoke_draw = InvokeDrawALine()
draw_a_line = DrawALine()
draw_command = DrawCommand(draw_a_line, 1, 2)
invoke_draw.command(draw_command)
invoke_draw.click_to_draw()
invoke_draw.undo()

output:

Draw a line from 1 to 2
Erase a line from 1 to 2

Obviously, this test doesn't allow stack several actions to undo. Maybe I'm completely mistaken so I would appreciate some help.

r_31415
  • 8,752
  • 17
  • 74
  • 121

2 Answers2

2

How I'd go about this

class Command(object):
    def execute(self, canvas):
         raise NotImplementedError

class DrawLineCommand(Command):
    def __init__(self, point1, point2):
        self._point1 = point1
        self._point2 = point2

    def execute(self, canvas):
        canvas.draw_line(self._point1, self._point2)

 class DrawCircleCommand(Command):
     def __init__(self, point, radius):
        self._point = point
        self._radius = radius

     def execute(self, canvas):
        canvas.draw_circle(self._point, self._radius)

class UndoHistory(object):
    def __init__(self, canvas):
        self._commands = []
        self.canvas = canvas

    def command(self, command):
        self._commands.append(command)
        command.execute(self.canvas)

    def undo(self):
        self._commands.pop() # throw away last command
        self.canvas.clear()
        for command self._commands:
            command.execute(self.canvas)

Some thoughts:

  1. Trying to undo an action can be hard. For example, how would you undraw a line? You'd need to recover what used to be under that line. A simpler approach is often to revert to a clean slate and then reapply all the commands.
  2. Each command should be contained in a single object. It should store all of the data neccesary for the command.
  3. In python you don't need to define the Command class. I do it to provide documentation for what methods I expect Command objects to implement.
  4. You may eventually get speed issues reapplying all the command for an undo. Optimization is left as an excersize for the reader.
Winston Ewert
  • 44,070
  • 10
  • 68
  • 83
  • Very clean and nice. Thanks for your answer. I have a question, though. It seems that this is not a textbook implementation of the command pattern. Is it right?. You have the job of the receiver inside the command subclasses. Nonetheless, looks like I need to keep instances of the command classes in a list. – r_31415 Sep 22 '11 at 04:02
  • @RobertSmith, the equivalent to the receiver class is the canvas object that I'm passing around. It is the target object that the actions act on. So no, I'm not combing the jobs of the receiver and command class. To my mind your implementation of the receiver has pieces of the command class in it. The reciever should know nothing about the command class/undo structure. It just knows how to perform the methods like draw_line, draw_circle, etc. – Winston Ewert Sep 22 '11 at 12:18
  • I understand. Well, this is a version of the command pattern. As for the receiver inside the command class, I didn't mean you're combining them but, given your clarification, you're passing the equivalent of the receiver into the command class (which is exactly the same it's done in my implementation). – r_31415 Sep 22 '11 at 17:09
  • @RobertSmith, not exactly the same... I pass it to execute() and you pass it to the constructor. It still seems to me that your a little confused on the details to this pattern. But that's no skin off my nose. You may also be interested to see this example of a command pattern in python: http://en.wikipedia.org/wiki/Command_pattern – Winston Ewert Sep 22 '11 at 18:03
  • Sure, you passed it to execute() but that's not exactly the command pattern as described in http://www.amazon.com/Python-3-Object-Oriented-Programming/dp/1849511268, page 269 (sorry, it's not available in Google Books). Remember I only want to learn the basics for now, so this is not for a real implementation. Therefore, I find easier to understand it when there is a clear distinction between receiver, command and invoker. – r_31415 Sep 22 '11 at 19:04
  • @RobertSmith, you were the one who said it was exactly the same not me. But that is just a minor variation. Don't let it distract you. My concern is that you have a DrawLine receiver class. If you look at your book, you don't have a SaveDocument receiver of CloseWindow reciever. You have a Document and Window receiver. The fact that you did in your original post and accepted the answer that is still doing makes me wonder if you've confusing the roles of Command and Reciever. – Winston Ewert Sep 22 '11 at 19:27
  • Uhm, I didn't understand what you mean. I looked at the book, and yes, there isn't a SaveDocument receiver or a CloseWindow receiver. To clarify, I have a Window and Document receiver (which are in charge of making an actual operation). These receivers are passed to SaveCommand and an ExitCommand while they're instantiated. Finally, I have invoker (which are triggered by the client, for example) with a property which contains a command (SaveCommand or ExitCommand) (pag. 269). Your point is that the receiver shouldn't be passed to the command? – r_31415 Sep 23 '11 at 01:23
  • @RobertSmith, it sounds like I was mistaken. I was concerned based on your code that were having classes like SaveDocumentInvoker, SaveDocumentCommand, SaveDocumentReciever when doing so really defeated the purpose of the command pattern. But it sounds like you aren't doing that. So that's all fine. – Winston Ewert Sep 23 '11 at 01:41
  • Whether or not you want to pass the receiver as a constructor parameter or as a parameter to execute function depends on the situation. The advantage is that you change the reciever, (say you want to replay the commands that create one image onto other image or something.) – Winston Ewert Sep 23 '11 at 01:44
  • Oh, great. Thank you very much for your interest. You were very helpful :-) – r_31415 Sep 23 '11 at 03:00
  • @RobertSmith, glad to be helpful. It'd be a waste not to understand after asking the question. – Winston Ewert Sep 23 '11 at 03:42
2

Here is an implementation keeping the commands in a list.

# command
class DrawCommand:
    def __init__(self, draw, point1, point2):
        self.draw = draw
        self.point1 = point1
        self.point2 = point2
    def execute_drawing(self):
        self.draw.execute(self.point1, self.point2)
# invoker
class InvokeDrawLines:
    def __init__(self, data):
        self.commandlist = data
    def addcommand(self, command):
        self.commandlist.append(command)
    def draw(self):
        for cmd in self.commandlist:
            cmd.execute_drawing()
    def undocommand(self, command):
        self.commandlist.remove(command)

# receiver
class DrawALine:
    def execute(self, point1, point2):
        print("Draw a line from" , point1, point2)
rtalbot
  • 1,615
  • 10
  • 13
  • Thanks for your answer. That looks great but shouldn't self.commandlist be assigned to a list instead of 'data' in the class InvokeDrawLines? – r_31415 Sep 22 '11 at 04:26
  • Sure but this way you can use it like this:draw1 = DrawCommand(draw_a_line, 5, 10) draw2 = DrawCommand(draw_a_line, 29, 55) draw3 = DrawCommand(draw_a_line, 99, 0) invoker = InvokeDrawLines([draw1,draw2,draw3]) – rtalbot Sep 22 '11 at 04:29
  • Oh, fair enough. Then after removing an instance of DrawCommand, I just re-run the draw method of InvokeDrawLines. Is that it? – r_31415 Sep 22 '11 at 05:51