Паттерн проектирования «Команда» / «Command»

    Почитать описание других паттернов.
    A

    Проблема


    Необходимо иметь эффективное представление запросов к некоторой системе, не обладая при этом знаниями ни об их природе ни о способах их обработки.

    Описание


    Существует по крайней мере три мотивации к использованию шаблона “Команда”:
    • инкапсулирование запроса в виде объекта для последующего протоколирования/логирования и т.п.
    • наделение сущности “вызов метода объекта” свойствами самостоятельного объекта;
    • объектно-ориентированный обратный вызов (callback);


      На самом деле все вышеперечисленные мотивации сводятся к одной — “инкапсулирование запроса в виде объекта”. Известно большое количество типов систем, где подобный подход эффективен и оправдан. Во первых — это любое более мене серьезное desktop-приложение c возможностями отмены и повторения действий пользователя (undo/redo). Во вторых, это сетевые распределенные системы использующие запросы в виде объектов в качестве основного примитива инициализации каких-либо операций. В третьих — системы с поддержкой асинхронных вызовов, инкапсулирующие обратный вызов в виде callback-объекта. Перечислять можно бесконечно, важно понять, что шаблон команда — один из самых распространенных шаблонов, который, можно сказать, появился буквально на этапе становления ООП и только потом был формализован и описан в книге GoF.

      Основополагающая идея шаблона заключается в использовании единого интерфейса для описания всех типов операций, которые можно производить с системой. Тогда для добавления в систему поддержки новой операции достаточно реализовать предлагаемый интерфейс. Таким образом, каждая операция представляется самостоятельным объектом инкапсулирующим некоторый набор дополнительных свойств. Систем, в свою очередь, приобретает возможно выполнять дополнительный набор действий над запросами (объектами). Это протоколирование, отмена предыдущего действия повторение последующего и т.д.

      Практический пример


      Рассмотрим самую очевидную мотивацию для использования шаблона — логирование/undo-redo. Для этого, представим чтобы было,
      <sarcasm>
      если бы Стивен Борн (автор bash) прочитал книгу GoF и знал что такое паттерн проектирования “Команда”
      </sarcasm>
      . Было бы примерно следующие:
      • каждая команда в оболочке инкаспулировалась бы в отдельный класс — потомок класса Command;
      • командная оболочка поддерживала бы механизмы протоколирования/логирования и что самое главное — отмены и повторения (undo/redo) действий пользователя, т.е. команд;


      Попытаемся изменить проблемы в дизайне существующих решений и напишем свой велосипед шел с возможностью отмены и повторения команд.

      Для достижения поставленной цели достаточно ввести понятие очереди выполненных команд, куда попадает всякая команда (объект, наследник Command) после выполнения и очереди отмененных команд, куда поступает любая отмененная команда. Помимо этого, необходимо иметь разделенные механизмы выполнения и отмены действий команды. Очевидно, что эти механизмы индивидуальны для каждой команды. Тогда для реализации целостной системы нам достаточно, оперируя двумя очередями применять к объектам находящимся в их верхушке соответствующие действия — выполнение или отмену.

      Диаграмма классов


      Каждая команда, поддерживаемая оболочкой должна расширять класс Command, реализуя при этом три метода — execute(), cancel() и name() — выполнение, отмена и имя команды соответственно.

      Класс командной оболочки изображен лишь для наглядности. В приведенной реализации его заменяет метод main().


      Реализация на Python


      # -*- coding: cp1251 -*-
      
      from sys import stdout as console
      
      # Обработка команды exit
      class SessionClosed(Exception):
      	def __init__(self, value):
      		self.value = value
      
      # Интерфейс команды
      class Command:
      	def execute(self):
      		raise NotImplementedError()
      
      	def cancel(self):
      		raise NotImplementedError()		
      
      	def name():
      		raise NotImplementedError()
      
      # Команда rm
      class RmCommand(Command):
      	def execute(self):
      		console.write("You are executed \"rm\" command\n")
      	
      	def cancel(self):
      		console.write("You are canceled \"rm\" command\n")
      
      	def name(self):
      		return "rm"
      
      # Команда uptime
      class UptimeCommand(Command):
      	def execute(self):
      		console.write("You are executed \"uptime\" command\n")
      
      	def cancel(self):
      		console.write("You are canceled \"uptime\" command\n")
      	
      	def name(self):
      		return "uptime"
      
      # Команда undo
      class UndoCommand(Command):
      	def execute(self):
      		try:
      			cmd = HISTORY.pop()
      			TRASH.append(cmd)
      			console.write("Undo command \"{0}\"\n".format(cmd.name()))
      			cmd.cancel()
      			
      		except IndexError:
      			console.write("ERROR: HISTORY is empty\n")
      	
      	def name(self):
      		return "undo"
      
      # Команда redo
      class RedoCommand(Command):
      	def execute(self):
      		try:
      			cmd = TRASH.pop()
      			HISTORY.append(cmd)
      			console.write("Redo command \"{0}\"\n".format(cmd.name()))
      			cmd.execute()
      
      		except IndexError:
      			console.write("ERROR: TRASH is empty\n")
      	def name(self):
      		return "redo"
      
      # Команда history
      class HistoryCommand(Command):
      	def execute(self):
      		i = 0
      		for cmd in HISTORY:
      			console.write("{0}: {1}\n".format(i, cmd.name()))
      			i = i + 1
      	def name(self):
      		print "history"
      
      # Команда exit
      class ExitCommand(Command):
      	def execute(self):
      		raise SessionClosed("Good bay!")
      
      	def name(self):
      		return "exit"
      
      # Словарь доступных команд
      COMMANDS = {'rm': RmCommand(), 'uptime': UptimeCommand(), 'undo': UndoCommand(), 'redo': RedoCommand(), 'history': HistoryCommand(), 'exit': ExitCommand()}   
      
      HISTORY = list()
      TRASH = list()
      
      # Шелл
      def main():
      
      	try:
      		while True:
      			console.flush()
      			console.write("pysh >> ")
      			
      			cmd = raw_input()
      			
      			try:
      
      				command = COMMANDS[cmd]
      				command.execute() 
      
      				if not isinstance(command, UndoCommand) and not isinstance(command, RedoCommand) and not isinstance(command, HistoryCommand):
      					TRASH = list()
      					HISTORY.append(command)
      				
      			except KeyError:
      				console.write("ERROR: Command \"%s\" not found\n" % cmd)
      
      	except SessionClosed as e:
      		console.write(e.value)		
      	
      if __name__ == "__main__": main()
      
      


      Использование


      $ python pysh.py
      pysh >> rm
      You are executed "rm" command
      pysh >> rm
      You are executed "rm" command
      pysh >> rm
      You are executed "rm" command
      pysh >> history
      0: rm
      1: rm
      2: rm
      pysh >> undo
      Undo command "rm"
      You are canceled "rm" command
      pysh >> exit
      Good bay!
      


      Примечание


      В некоторых источниках шаблон называется Действие/Action или Транзакция/Transaction.
    Поделиться публикацией

    Комментарии 18

    • НЛО прилетело и опубликовало эту надпись здесь
        +6
        Это смотря что именно автор имел в виду. Если «вас казнили», то все правильно :D
          0
          Это я имел ввиду :)
            +1
            Тогда должно быть что-то вроде You are executed by smth. Глаза действительно режет, эстеты плачут.
              +1
              You have been executed by smth.
        0
        Спасибо.

        Что вы думаете по поводу отмены/повторения в Adobe Photoshop последних версий? Там какая-то нечеловечески замутная система. Жмешь Ctrl+Z, а оно работает не так, как ожидаешь.
          +1
          Use Ctrl + Alt + Z!
          Ctrl + Z работает только с последним действием: отменяет его, а также «отменяет отмену».
            0
            Если не ошибаюсь, такое поведение (Ctrl+Alt+X и окно History) было во всех известных мне версиях (начиная с 4-й точно), а не только в последних.
              0
              Ctrl+Alt+Z, конечно же
            +1
            Хороший пример, а также хорошо реализован. Спасибо.
              +1
              Кораздо полезней было бы сделать подобную оболочку с SQL-ем. Инкапсулировать билдер запросов, батч, транзакции. Наподобии criteria в Hibernate, только более организованно и строго.
                +2
                Mr. T как иллюстрация — это ок! :)
                  0
                  Почти первая картинка по запросу «Команда» :)
                  0
                  All your base are belong to us!
                    +2
                    «Good bay!» пишется как «Good Bye». Мелочь, но все же.
                      0
                      Я бы даже сказал: «хорошая бухта».
                      0
                      «Good bay» — это когда хочешь сделать покупку на e-bay и тебе желают удачи:)
                        0
                        Пример реализации паттерна Command на php можно посмотреть в библиотеки Rediska (клиент для Redis)

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое