CHAPTER 11 Command Pattern
I ll be back.
Terminator
The robots are coming.
In this chapter, we are going to look at how we can control a robot using Python code. For our purposes, we will be using the turtle module included in the Python standard library. This will allow us to handle commands like move forward, turn left, and turn right without the need to build an actual robot, although you should be able to use the code developed in this chapter, combined with a board like the Raspberry Pi and some actuators, to actually build a robot you could control with code.
Let s begin by doing something simple, like drawing a square on the screen using the turtle module.
turtle_basic.py import turtle turtle.color(’blue’, ’red’) turtle.begin_fill()
for _ in range(4):
turtle.forward(100) turtle.left(90)
turtle.end_fill() turtle.done()
167
' Wessel Badenhorst 2017
W. Badenhorst, Practical Python Design Patterns, https://doi.org/10.1007/978-1-4842-2680-3_11
CHAPTER 11 COMMAND PATTERN
The turtle is a simple line-and-fill tool that helps to visualize what the robot is doing. In the preceding code, we import the turtle library. Next, we set the line color to blue and the background color to red. Then, we tell the turtle to record the current point as the start of a polygon that will be filled later. Then, the loop draws the four sides of the square. Finally, the end_fill() method fills the polygon with red. The done() method keeps the program from terminating and closing the window once the process is completed.
If you are using an Ubuntu system and get an error message regarding python3-tk, you need to install the package.
sudo apt-get install python3-tk
Run the code, and you should see a little triangle (the turtle) move on the screen.
This approach works while we are trying to draw shapes and lines on the screen, but since the turtle is just a metaphor for our robot, drawing pretty shapes does not have a lot of use. We are going to extend our robot control script to accept four commands (start, stop, turn left 10 degrees, and turn right 10 degrees), which we will map to keyboard keys.
turtle_control.py import turtle turtle.setup(400, 400)
screen = turtle.Screen() screen.title("Keyboard drawing!")
t = turtle.Turtle() distance = 10
def advance():
t.forward(distance)
def turn_left():
t.left(10)
def turn_right():
t.right(10)
168
CHAPTER 11 COMMAND PATTERN
def retreat():
t.backward(10)
def quit():
screen.bye()
screen.onkey(advance, "w") screen.onkey(turn_left, "a") screen.onkey(turn_right, "d") screen.onkey(retreat, "s") screen.onkey(quit, "Escape")
screen.listen() screen.mainloop()
What you are essentially doing is creating an interface with which to control the turtle. In the real world, this would be akin to a remote control car that takes one
signal to mean turn left, another for turn right, a third for accelerate, and a fourth for decelerate. If you were to build an RC sender that translated the signals that usually come from the remote to now take input from a keyboard using some electronics and your USB port, you would be able to use code similar to the code we have here to control the remote control car. In the same way, you could control a robot via remote control.
In essence, we are sending commands from one part of our system to another.
Whenever you want to send an instruction or set of instructions from one object to another, while keeping these objects loosely coupled, it is wise to encapsulate everything needed to execute the instructions in some kind of data structure.
The client that initiates the execution does not have to know anything about the
way in which the instruction will be executed; all it is required to do is to set up all the required information and hand off whatever needs to happen next to the system.
Since we are dealing with an object-oriented paradigm, the data structure used to encapsulate the instruction will be an object. The class of objects used to encapsulate information needed by some other method in order to execute is called a command. The client object instantiates a command containing the method it wishes to execute, along
with all the parameters needed to execute the method, as well as some target object that has the method.
This target object is called the receiver. The receiver is an instance of a class that can execute the method given the encapsulated information.
All of this relies on an object called an invoker that decides when the method on the receiver will execute. It might help to realize that invocations of methods are similar to instances of classes.
Without any actual functionality, the command pattern can be implemented like
this:
class Command:
def __init__(self, receiver, text):
self.receiver = receiver self.text = text
def execute(self):
self.receiver.print_message(self.text)
class Receiver(object):
def print_message(self, text):
print("Message received: {}".format(text))
class Invoker(object):
def __init__(self):
self.commands = []
def add_command(self, command):
self.commands.append(command)
def run(self):
for command in self.commands:
command.execute()
if __name__ == "__main__":
receiver = Receiver()
command1 = Command(receiver, "Execute Command 1") command2 = Command(receiver, "Execute Command 2")
170
CHAPTER 11 COMMAND PATTERN
invoker = Invoker() invoker.add_command(command1) invoker.add_command(command2) invoker.run()
Now you would be able to queue up commands to be executed even if the receiver were busy executing another command. This is a very useful concept in distributed systems where incoming commands need to be captured and handled, but the system may be busy with another action. Having all the information in an object in the queue allows the system to deal with all incoming commands without losing important commands while executing some other command. You essentially get to dynamically create new behavior at runtime. This process is especially useful when you want to set up the execution of a method at one time and then sometime later execute it.
Think about the implication for a robot like the Mars Rover if operatives were only able to send a single command to be executed and then had to wait for the command
to finish execution before they could transmit the next command. It would make doing anything on Mars extremely time-consuming to the point of being impossible. What
can be done instead is to send a sequence of actions to the rover. The rover can then invoke each command in turn, allowing it to get more work done per unit of time, and potentially negating much of the effect of the time lag between transmission and receipt of commands between the rover and Earth. This string of commands is called a macro.
Macros are also useful when running automated tests where you want to mimic
user interaction with the system, or when you want to automate certain tasks that you
do repeatedly. You could create a command for each step in the process and then string them up before letting some invoker handle the execution. The decoupling allows you to swap out direct human intervention via the keyboard for generated computer input.
Once we begin stringing commands together, we can also look at undoing the commands that were executed. This is another area where the command pattern is particularly useful. By pushing an undo command onto a stack for every command that gets invoked, we get multi-level undo for free, much like you see in everything from modern word processors to video-editing software.
To illustrate how you could go about using the command pattern to build a multi- level undo stack, we are going to build a very simple calculator that can do addition, subtraction, multiplication, and division. For the sake of keeping the example simple, we will not concern ourselves with the case where we have multiplication by zero.
171
CHAPTER 11 COMMAND PATTERN
class AddCommand(object):
def __init__(self, receiver, value):
self.receiver = receiver self.value = value
def execute(self):
self.receiver.add(self.value)
def undo(self):
self.receiver.subtract(self.value)
class SubtractCommand(object):
def __init__(self, receiver, value):
self.receiver = receiver self.value = value
def execute(self):
self.receiver.subtract(self.value)
def undo(self):
self.receiver.add(self.value)
class MultiplyCommand(object):
def __init__(self, receiver, value):
self.receiver = receiver self.value = value
def execute(self):
self.receiver.multiply(self.value)
def undo(self):
self.receiver.divide(self.value)
class DivideCommand(object):
def __init__(self, receiver, value):
self.receiver = receiver self.value = value
def execute(self):
self.receiver.divide(self.value)
172
CHAPTER 11 COMMAND PATTERN
def undo(self):
self.receiver.multiply(self.value)
class CalculationInvoker(object): def __init__(self): self.commands = [] self.undo_stack = []
def add_new_command(self, command):
self.commands.append(command)
def run(self):
for command in self.commands:
command.execute() self.undo_stack.append(command)
def undo(self):
undo_command = self.undo_stack.pop() undo_command.undo()
class Accumulator(object): def __init__(self, value):
self._value = value
def add(self, value):
self._value += value
def subtract(self, value): self._value -= value
def multiply(self, value): self._value *= value
def divide(self, value):
self._value /= value
def __str__(self):
return "Current Value: {}".format(self._value)
if __name__ == "__main__":
acc = Accumulator(10.0)
invoker = CalculationInvoker() invoker.add_new_command(AddCommand(acc, 11)) invoker.add_new_command(SubtractCommand(acc, 12)) invoker.add_new_command(MultiplyCommand(acc, 13)) invoker.add_new_command(DivideCommand(acc, 14))
invoker.run() print(acc)
invoker.undo() invoker.undo() invoker.undo() invoker.undo()
print(acc)
Just note that the __str()__ method is used when Python casts the accumulator object to a string when printing it.
As we have seen a couple of times now, the duck typing in Python allows us to skip defining an overarching base class to inherit from. As long as our commands
have execute() and undo() methods and take a receiver and value as parameters on construction, we can use any command we feel like.
In the preceding code, we push every command that gets executed by the invoker onto a stack. Then, when we want to undo a command, the last command is popped from the stack and the undo() method of that command is executed. In the case of the calculator, the inverse calculation is executed to return the value of the accumulator back to what it would have been before the command was executed.
All implementations of the Command class in the command pattern will have a method such as execute() that is called by the invoker to execute the command. The instance of the Command class keeps a reference to the receiver, the name of the method to be executed, and the values to be used as parameters in the execution. The receiver is the only object that knows how to execute the method kept in the command, and it manages its own internal state independent of the other objects in the system. The client has no knowledge of the implementation details of the command, and neither do the
174
CHAPTER 11 COMMAND PATTERN
command or the invoker. To be clear, the command object does not execute anything; it
is just a container. By implementing this pattern, we have added a layer of abstraction between the action to be executed and the object that invokes that action. The added abstraction leads to better interaction between different objects in the system, and looser coupling between them, making the system easier to maintain and update.
The heart of this design pattern is the translation of method calls into data that can be saved in a variable, passed to a method or function as a parameter, and returned from a function as a result. The result of applying this pattern is that functions or methods become first-class citizens. When functions are first class citizens variables can point to functions, functions can be passed as a parameter to other functions, and they can be returned as the result from executing a function.
What is interesting about the command pattern, specifically this ability to turn behavior into a first-class citizen, is that we could use this pattern to implement a functional programming style with lazy evaluation, meaning that functions could be passed to other functions and could be returned by functions. All functions get passed all that is needed for execution, with no global state, and functions are only executed when an actual result needs to be returned. This is a fairly shallow description of functional programming, but the implications are deep.
The interesting twist in our tale is that in Python everything is a function, and as such we could also do away with the class that encapsulates the function call and just pass the function with the relevant parameters to the invoker.
def text_writer(string1, string2):
print("Writing {} - {}".format(string1, string2))
class Invoker(object):
def __init__(self):
self.commands = []
def add_command(self, command):
self.commands.append(command)
def run(self):
for command in self.commands:
command"function"
if __name__ == "__main__":
invoker = Invoker() invoker.add_command({
"function": text_writer,
"params": ("Command 1", "String 1") })
invoker.add_command({
"function": text_writer,
"params": ("Command 2", "String 2") })
invoker.run()
For a simple case like this, you could just define a lambda function, and you would not even have to write the function definition.
class Invoker(object):
def __init__(self):
self.commands = []
def add_command(self, command):
self.commands.append(command)
def run(self):
for command in self.commands:
command"function"
if __name__ == "__main__":
f = lambda string1, string2: print("Writing {} - {}".format
(string1, string2))
invoker = Invoker() invoker.add_command({
"function": f,
"params": ("Command 1", "String 1") })
invoker.add_command({
"function": f,
176
CHAPTER 11 COMMAND PATTERN
"params": ("Command 2", "String 2") })
invoker.run()
Once the function you want to execute gets more complicated, you have two options other than the method we used in the previous program. You could just wrap the command s execute() function in a lambda, letting you avoid creating a class. Even though it is a nifty trick, your code will probably be more clear if you just create a class for the complex case, which is the second option. So, a good rule of thumb is that when you need to create a command for anything more complex than a piece of code you can wrap in a single-line lambda function, create a class for it.
Whether you are building a self-driving car or sending messages to outer space, decoupling the request for execution from the actual execution allows you to not only buffer actions that need to be executed, but also exchange the mode of input.
It is important that the command pattern isolates the invoker from the receiver. It also separates the time the execution is set up from the time it is processed.
Use the command pattern to create a command-line word-
replacement script that can replace every instance of a given word in a given text file with some other word. en, add a command to undo the last replace action.
'Practical Python Design Patterns' 카테고리의 다른 글
Iterator Pattern (0) | 2023.03.29 |
---|---|
Interpreter Pattern (0) | 2023.03.28 |
Chain of Responsibility Pattern (0) | 2023.03.28 |
Proxy Pattern (0) | 2023.03.28 |
Facade Pattern (0) | 2023.03.28 |
댓글