Pull to refresh

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

Perfect code *
Почитать описание других паттернов.
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.
Tags:
Hubs:
Total votes 50: ↑42 and ↓8 +34
Views 60K
Comments Comments 18