Patrón de Diseño Command en Python: Cómo Implementarlo

Patrón de Diseño Command en Python

Si estás realizando un programa teniendo en cuenta principios y patrones de diseño software, es posible que te encuentres con alguna situación en la que el patrón a aplicar es el patrón comando y, claro, necesitas conocer alguna manera de implementarlo en Python.

Una manera sencilla de implementar el patrón command en Python es, aprovechando que las funciones son objetos de primer orden, proporcionar al invocador funciones que representen comandos. Si necesitamos un control más complejo lo mejor es optar por una implementación tradicional orientada a objetos.

Vamos a ver a continuación estas dos alternativas de implementación y cuándo es más adecuada utilizar una y otra. Pero para comprender perfectamente estas dos implementaciones haremos antes un breve repaso de los elementos que intervienen en el patrón.

Receptor, comandos e invocador

En el patrón comando intervienen tres elementos principales, que tienes que tener claramente definidos en tu problema:

  • El receptor o receiver es el elemento u objeto sobre el cual se aplicarán una serie de funciones para producir algún tipo de cambio.
  • Los comandos o commands son las funciones que se van a aplicar directamente sobre el receptor.
  • El invocador o invoker es el elemento que dispara la ejecución de un comando concreto, por tanto, es el encargo de ejecutar el comando sobre el receptor.

Ejemplo sencillo de aplicación del patrón comando

Te voy a pedir que te imagines un pequeño ejemplo para ilustrar la implementación del patrón. Se trata básicamente de un tablero de juego rectangular de f filas y c columnas. Además, tendremos un personaje ubicado en una de esas celdas que puede adoptar dos apariencias: G o P (grande o pequeño). Podremos realizar varias acciones en el juego que serán mover al personaje hacia arriba, hacia abajo, hacia la derecha o hacia la izquierda y cambiar su apariencia. También podremos guardar en disco el estado del juego o recuperarlo en cualquier momento.

De esta manera, es posible que el juego se encuentre en el siguiente estado en un momento dado, suponiendo un tablero de juego de 9×9 (9 filas y 9 columnas, donde cada celda está representada por un punto), donde el personaje se encuentra en la posición (2, 3) y su aspecto es G. Ten en cuenta que la posición superior izquierda es la (0, 0) y la inferior derecha la (8, 8):

 · · · · · · · · ·
 · · · · · · · · ·
 · · · G · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·

Vamos a implementar este pequeño juego, para el que hemos determinado que es aplicable el patrón comando. Pero antes vamos a identificar los diferentes elementos:

  • El receptor será una lista con 5 valores que representará el estado del juego. Esos valores serán:
    1. Número de filas del tablero
    2. Número de columnas del tablero
    3. Fila actual del personaje
    4. Columna actual del personaje
    5. Aspecto del personaje
  • Los comandos serán las diferentes acciones que podremos realizar en el juego (subir, bajar, izquierda, derecha, cambiar de aspecto, guardar tablero o cargar tablero).
  • El invocador será un elemento que nos sirva de control (un controlador) que se encargará de ejecutar el comando sobre el juego.

Necesitaremos además una serie de funciones auxiliares, por ejemplo, una para mostrar el tablero por la pantalla y otra para solicitar una acción al usuario por teclado. También guardaremos los comandos en un diccionario para acceder a ellos más fácilmente.

A continuación te presento las dos alternativas de implementación para este pequeño problema.

Patrón comando implementado con funciones

Lo primero será definir el tablero y el estado de juego, que como ya he dicho, será una lista con 5 valores y que, además, será el receptor en nuestro problema. Vamos a definir un tablero de 9×9 y a ubicar a nuestro personaje en la posición (4, 4) con aspecto G:

#RECEPTOR
#es nuestro tablero de juego con 5 valores: dimensiones, posición y aspecto (G o P)
#tamaño 9x9, posición (4, 4) y aspecto G
juego = [9, 9, 4, 4, 'G']

Una vez definido esto vamos con los comandos, una serie de funciones que reciben el estado del juego y hacen las operaciones oportunas. Finalmente ubicamos estos comandos en un diccionario llamado comandos para poder acceder a ellos de manera sencilla. Esto podemos hacerlo porque en Python las funciones son objetos de primer orden, como ya te he dicho:

#COMANDOS
#para subir disminuimos la fila en una unidad (posición mínima 0)
def subir(juego):
	juego[2] = juego[2]-1 if juego[2] > 0 else 0

#para bajar aumentamos la fila en una unidad (posición máxima es dimensión máxima)
def bajar(juego):
	juego[2] = juego[2]+1 if juego[2] < juego[0] - 1 else juego[0] - 1

#para ir a la izquierda disminuimos la columna en una unidad (posición mínima 0)
def izquierda(juego):
	juego[3] = juego[3]-1 if juego[3] > 0 else 0

#para ir a la derecha aumentamos la columna en una unidad (posición máxima es dimensión máxima)
def derecha(juego):
	juego[3] = juego[3]+1 if juego[3] < juego[1] - 1  else juego[1] - 1

def cambiar_aspecto(juego):
	juego[4] = 'P' if juego[4] == 'G' else 'G'

#guardamos el estado del juego en un fichero para recuperarlo cuando queramos
def guardar(juego):
	with open('juego.txt','w') as fichero:
		for elemento in juego:
			fichero.write(str(elemento))
			fichero.write('\n')

#cargamos el juego guardado en fichero
def cargar(juego):
	with open('juego.txt', 'r') as fichero:
		leido = fichero.readlines()
	for i in range(0,4):
		juego[i] = int(leido[i][:-1])
	juego[4] = leido[4][:-1]

#ubicamos los comandos en un diccionario para acceder a ellos cómodamente
comandos = {'S':subir,
				'B':bajar,
				'I':izquierda,
				'D':derecha,
				'A':cambiar_aspecto,
				'G':guardar,
				'C':cargar}

A continuación creamos el invocador, que será una función controlador que en este caso es muy sencilla y lo único que hace es aplicar un comando concreto a un estado de juego concreto, ambos recibidos por parámetro:

#INVOCADOR
#en este caso es una función muy sencilla que recibe el comando a ejecutar y lo ejecuta
def controlador(comando, juego):
	comando(juego)

Haremos ahora las funciones auxiliares para mostrar el estado del juego por pantalla y para solicitar al usuario por teclado un comando a ejecutar:

#añadimos una funcione que dibuje el estado de juego por pantalla
def dibujar_juego(juego):
	print(' * Estado del juego:', juego)
	print()
	for fila in range(0, juego[0]):
		for columna in range(0, juego[1]):
			if fila == juego[2] and columna == juego[3]:
				print(' '+juego[4], end='')
			else:
				print(' ·', end='')
		print()
	print()


#función para la selección del comando
def solicitar_comando():
	valor = input('Introduzca un comando '+str(list(comandos.keys()))+' (otro para terminar): ')
	return comandos[valor] if valor in comandos.keys() else None

Finalmente solo nos queda crear el bucle principal del juego que se encarga de dibujar el estado del juego, solicitar una acción al usuario y proporcionar esa acción y el estado del juego al controlador para ejecutar el comando:

while True:
	dibujar_juego(juego)
	comando = solicitar_comando()
	if not comando:
		break
	controlador(comando, juego)

Y listo, ya lo tenemos, si lo ejecuto, muevo un poco al personaje, guardo el estado del juego, vuelvo a moverlo y a cambiar su estado y cargo el estado del juego para recuperar lo que había guardado, obtengo la siguiente ejecución:

 * Estado del juego: [9, 9, 4, 4, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · G · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): D
 * Estado del juego: [9, 9, 4, 5, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · G · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): D
 * Estado del juego: [9, 9, 4, 6, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · G · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): D
 * Estado del juego: [9, 9, 4, 7, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · G ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): B
 * Estado del juego: [9, 9, 5, 7, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · G ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): B
 * Estado del juego: [9, 9, 6, 7, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · G ·
 · · · · · · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): B
 * Estado del juego: [9, 9, 7, 7, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · G ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): G
 * Estado del juego: [9, 9, 7, 7, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · G ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): I
 * Estado del juego: [9, 9, 7, 6, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · G · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): I
 * Estado del juego: [9, 9, 7, 5, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · G · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): I
 * Estado del juego: [9, 9, 7, 4, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · G · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): I
 * Estado del juego: [9, 9, 7, 3, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · G · · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): I
 * Estado del juego: [9, 9, 7, 2, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · G · · · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): S
 * Estado del juego: [9, 9, 6, 2, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · G · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): A
 * Estado del juego: [9, 9, 6, 2, 'P']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · P · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): C
 * Estado del juego: [9, 9, 7, 7, 'G']

 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · · ·
 · · · · · · · G ·
 · · · · · · · · ·

Introduzca un comando ['S', 'B', 'I', 'D', 'A', 'G', 'C'] (otro para terminar): X

Patrón comando implementado con orientación a objetos

Si lo que necesitas es una aproximación más tradicional y utilizar la orientación a objetos, te propongo esta solución más formal:

from abc import ABC, abstractmethod

#definimos las clases abstractas que forman el esqueleto del patrón
#la clase receptor está vacía, pero en el futuro podría tener más funcionalidad
class Receptor(ABC):
	pass

class Comando(ABC):

	@abstractmethod
	def ejecutar(self, receptor):
		pass

class Invocador(object):

	receptor = None

	def __init__(self, receptor):
		self.receptor = receptor

	def aplicar_comando(self, comando):
		comando.ejecutar(self.receptor)


#creamos el la clase para el juego con todas las funcionalidades necesarias
class Juego(object):

	filas_tablero = None
	columnas_tablero = None
	fila_personaje = None
	columna_personaje = None
	aspecto_personaje = None

	def __init__(self, filas, columnas, posicion_f, posicion_c, aspecto):
		self.filas_tablero = filas
		self.columnas_tablero = columnas
		self.fila_personaje = posicion_f
		self.columna_personaje = posicion_c
		self.aspecto_personaje = aspecto

	def mover_fila(self, pasos):
		self.fila_personaje = min(self.filas_tablero-1, max(0, self.fila_personaje + pasos))

	def mover_columna(self, pasos):
		self.columna_personaje = min(self.columnas_tablero-1, max(0, self.columna_personaje + pasos))

	def cambiar_aspecto(self, aspecto):
		self.aspecto_personaje = aspecto

	def dibujar(self):
		print(' * Estado del juego:')
		print()
		for fila in range(0, self.filas_tablero):
			for columna in range(0, self.columnas_tablero):
				if fila == self.fila_personaje and columna == self.columna_personaje:
					print(' ' + self.aspecto_personaje, end='')
				else:
					print(' ·', end='')
			print()
		print()

	def guardar(self):
		with open('juego.txt', 'w') as fichero:
			fichero.write(str(self.filas_tablero)+'\n')
			fichero.write(str(self.columnas_tablero) + '\n')
			fichero.write(str(self.fila_personaje) + '\n')
			fichero.write(str(self.columna_personaje) + '\n')
			fichero.write(self.aspecto_personaje + '\n')

	def cargar(self):
		with open('juego.txt', 'r') as fichero:
			leido = fichero.readlines()
		self.filas_tablero = int(leido[0][:-1])
		self.columnas_tablero = int(leido[1][:-1])
		self.fila_personaje = int(leido[2][:-1])
		self.columna_personaje = int(leido[3][:-1])
		self.aspecto_personaje = leido[4][:-1]



#creamos los comandos
class Subir(Comando):
	def ejecutar(self, juego):
		juego.mover_fila(-1)

class Bajar(Comando):
	def ejecutar(self, juego):
		juego.mover_fila(1)

class Izquierda(Comando):
	def ejecutar(self, juego):
		juego.mover_columna(-1)

class Derecha(Comando):
	def ejecutar(self, juego):
		juego.mover_columna(1)

class CambiarAspecto(Comando):
	def ejecutar(self, juego):
		juego.cambiar_aspecto('P' if juego.aspecto_personaje == 'G' else 'G')

class Guardar(Comando):
	def ejecutar(self, juego):
		juego.guardar()

class Cargar(Comando):
	def ejecutar(self, juego):
		juego.cargar()


#creamos una función para la selección del comando
def solicitar_comando():
	valor = input('Introduzca un comando '+str(list(comandos.keys()))+' (otro para terminar): ')
	return comandos[valor] if valor in comandos.keys() else None

#creamos el juego
juego = Juego(9,9,4,4,'P')

#creamos los comandos y los añadimos al diccionario
comandos = {'S':Subir(),
				'B':Bajar(),
				'I':Izquierda(),
				'D':Derecha(),
				'A':CambiarAspecto(),
				'G':Guardar(),
				'C':Cargar()}

#creamos el controlador proporcionándole el juego con el que va a trabajar
controlador = Invocador(juego)

#bucle principal del juego
while True:
	juego.dibujar()
	comando = solicitar_comando()
	if not comando:
		break
	controlador.aplicar_comando(comando)

Este código funciona como el anterior pero, es evidente, tiene algunas diferencias. El hecho de que utilicemos orientación a objetos hace que la organización sea un poco mejor y las responsabilidades estén mejor repartidas.

Por ejemplo, ahora es la clase Juego la que contiene la lógica de movimiento del personaje, así como el cambio de aspecto y las operaciones de guardar y cargar en disco. Esto permite, además, implementar operaciones un poco más generales. Así, en esta versión permitimos que el personaje pueda dar pasos de cualquier tamaño o adquirir cualquier aspecto. Es en los comandos donde restringimos que los pasos sean de longitud 1 y el aspecto solo se pueda cambiar entre P y G. Fíjate en que, además, el código de los comandos es más sencillo porque la lógica de las operaciones se encuentra en la clase Juego.

Además, el invocador (clase Controlador) mantiene una referencia al receptor concreto, el juego, para poder aplicar los comandos sobre él. En otras aproximaciones del patrón comando, quienes mantienen la referencia al receptor son los comandos. Se pueda optar por una forma u otra según el caso, que habría que estudiar.

¿Qué tipo de implementación elegir?

¿Que opción debes elegir? Pues depende de tu problema en concreto. Si el problema es sencillo y solo necesitas aplicar comandos sobre un receptor que son muy sencillos y nada más, tal vez puedas decantarte por la opción con funciones.

Si en todo tu programa estás utilizando orientación a objetos, yo me inclinaría por la segunda versión. Además, si necesitas mayor control sobre las operaciones o los comandos son más complejos, igualmente optaría por la versión con orientación a objetos. Por ejemplo, si necesitas implementar una operación de deshacer (undo o el típico Ctrl + Z) la orientación a objetos te dará más facilidades.

Una manera sencilla (que no la única) de poder implementar la operación de deshacer sería permitir que el controlador guardase la última operación aplicada, proporcionar un método de deshacer en el propio controlador y, finalmente, implementar la operación de deshacer en cada uno de los comandos. Veamos los cambios solo para el comando de subir (sería similar para los otros comandos):

#controlador que guarda el último comando aplicado y permite deshacerlo
class Invocador(object):

	receptor = None
	ultimo_comando = None

	def __init__(self, receptor):
		self.receptor = receptor

	def aplicar_comando(self, comando):
		comando.ejecutar(self.receptor)
		self.ultimo_comando = comando

	def deshacer_comando(self):
		if self.ultimo_comando:
			self.ultimo_comando.deshacer(self.receptor)
			self.ultimo_comando = None

#añadimos el método de deshacer a la clase Comando
class Comando(ABC):

	@abstractmethod
	def ejecutar(self, receptor):
		pass

	def deshacer(self, receptor):
		pass

#comando de subir con operación de deshacer implementada (dar un paso en sentido contrario)
class Subir(Comando):
	
	def ejecutar(self, juego):
		juego.mover_fila(-1)
		
	def deshacer(self, juego):
		juego.mover_fila(1)

Simplemente con eso ya tendríamos la posibilidad de deshacer la última operación. Bastaría con llamar a la función deshacer_comando del controlador en cuanto quisiéramos deshacer una operación.

Por cierto, existe un pequeño problema en este último código, ¿eres capaz de encontrarlo?

Otras utilidades del patrón command

Además de la ventaja propia del patrón comando, que es encapsular una petición como un objeto y desacoplar el invocador de la petición, este patrón permite, o al menos facilita, la implementación de una serie de funcionalidades muy interesantes.

Una ya te la he comentado, que es la operación de deshacer. Pero también podemos realizar un registro o log de todas las operaciones realizadas, hacer colas de peticiones para su ejecución en otro momento, cambiar peticiones en tiempo de ejecución, etc.

¿Útil? Yo creo que sí.

La Hoja de Referencia de Python – ¡Gratis!

La Hoja de Referencia de Python - Código Pitón
Consigue trucos, consejos y actualizaciones y, por supuesto, la Hoja de Referencia de Python gratis.



Antes de suscribirte consulta aquí la Información Básica sobre Protección de Datos. Responsable de los datos: Laura Otero Moreira. Finalidad de la recogida y tratamiento de los datos personales: enviarte boletín informativo de Python y comunicaciones comerciales. Legitimación: tu consentimiento. Destinatarios: no se ceden a terceros. Los datos se almacenan en los servidores de marketing (MailRelay). Derechos: podrás ejercer tus derechos de acceso, rectificación, limitación y supresión de datos en info @ codigopiton.com así como presentar una reclamación ante una autoridad de control. Más información en nuestra política de privacidad, encontrarás información adicional sobre la recopilación y el uso de tu información personal, incluida información sobre acceso, conservación, rectificación, eliminación, seguridad y otros temas.