Patrones de diseño con Flutter: 14 — Command

Paolo Pinto
6 min readApr 26, 2024

--

también llamado: Comando, Orden, Action

Este patron se por ser una solución versátil en el desarrollo de software, permitiendo la encapsulación de solicitudes como objetos. A medida que las aplicaciones se vuelven más complejas, gestionar el deshacer o rehacer de las aplicaciones se convierte un desafío considerable. ¿Cómo se puede simplificar estos procesos de forma escalable y mantenible?

Imagina! Trabajas en un restaurante que es bastante concurrente y no cuenta con un sistema de pedidos y las cosas se ponen caoticas rapidamente. Ahi es donde nace la necesidad de implementar un sistema que permita que los pedidos se gestionen de manera ordenada y eficiente.

¿Que podemos hacer con el patron Command?

Este patron convierte una solicitud en un objeto independiente que contiene toda la información sobre la solicitud. Esto te permite parametrizar los métodos con diferentes solicitudes, retrasar o poner en cola la ejecución de una solicitud y soportar operaciones que no se pueden realizar.

Command nos sugiere extraer todos los detalles de la solicitud. Esto como el objeto que está siendo invocado, el nombre del método y la lista de argumentos, y ponerlos dentro de una clase Command separada con un único método que activa esta solicitud.

fuente: refactoring.guru

Así entonces de ahora en adelante, el objeto GUI no tiene que conocer qué objeto de la lógica de negocio recibirá la solicitud y cómo la procesará. El objeto GUI activa el comando, que gestiona todos los detalles.

¿Como podremos implementarlo en Flutter?

Un objeto de la UI puede haber proporcionado al objeto de la capa de negocio algunos parámetros. Ya que el método de ejecución del comando no tiene parámetros, ¿cómo pasaremos los detalles de la solicitud al receptor? Resulta que el comando debe estar preconfigurado con esta información o ser capaz de conseguirla por su cuenta.

diagrama de clases patron command

Invoker:

Se encarga de inicializar las solicitudes. Este activa el comando en lugar de enviar la solicitud directamente al Receiver, esto a través de un comando precreado de parte del cliente a través del constructor.

Command<interface>:

La interfaz Comando normalmente declara un único método para ejecutar el comando.

ConcreteCommand:

Implementan varios tipos de solicitudes. Lo que hace es pasar la llamada a uno de los objetos de la lógica de negocio. Se pueden fusionar interface con esta.

Receiver:

Contiene cierta lógica de negocio. Casi cualquier objeto puede actuar como receptor. Hace el trabajo real.

Client:

Crea y configura los objetos de comando concretos. El cliente debe pasar todos los parámetros de la solicitud, incluyendo una instancia del receptor, dentro del constructor del comando. Asi luego asocia con uno o varios emisores(Invokers).

Para este ejemplo utilizaremos el package de faker.

Invoker: PlatformButton

Receiver: Shape

class Shape {
Shape.initial()
: color = Colors.black,
height = 150.0,
width = 150.0;
Color color;
double height;
double width;
}

Command<interface>: Command

abstract interface class Command {
void execute();
String getTitle();
void undo();
}

ConcreteCommand: ChangeColorCommand | ChangeHeightCommand | ChangeWidthCommand

// ChangeColorCommand
class ChangeColorCommand implements Command {
ChangeColorCommand(this.shape) : previousColor = shape.color;

final Color previousColor;
Shape shape;

@override
String getTitle() => 'Change color';

@override
void execute() => shape.color = Color.fromRGBO(
random.integer(255),
random.integer(255),
random.integer(255),
1.0,
);

@override
void undo() => shape.color = previousColor;
}

// ChangeHeightCommand
class ChangeHeightCommand implements Command {
ChangeHeightCommand(this.shape) : previousHeight = shape.height;

final double previousHeight;
Shape shape;

@override
String getTitle() => 'Change height';

@override
void execute() => shape.height = random.integer(150, min: 50).toDouble();

@override
void undo() => shape.height = previousHeight;
}

// ChangeWidthCommand
class ChangeWidthCommand implements Command {
ChangeWidthCommand(this.shape) : previousWidth = shape.width;

final double previousWidth;
Shape shape;

@override
String getTitle() => 'Change width';

@override
void execute() => shape.width = random.integer(150, min: 50).toDouble();

@override
void undo() => shape.width = previousWidth;
}

CommandHistory: Clase para guardar la lista de comandos ejecutados

class CommandHistory {
final _commandList = ListQueue<Command>();

bool get isEmpty => _commandList.isEmpty;
List<String> get commandHistoryList =>
_commandList.map((c) => c.getTitle()).toList();

void add(Command command) => _commandList.add(command);

void undo() {
if (_commandList.isEmpty) return;

_commandList.removeLast().undo();
}
}

Client:

Extra: PlatformButton

import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class PlatformButton extends StatelessWidget {
final String text;
final Color materialColor;
final Color materialTextColor;
final VoidCallback? onPressed;

const PlatformButton({
required this.text,
required this.materialColor,
required this.materialTextColor,
this.onPressed,
});

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: kIsWeb || Platform.isAndroid
? MaterialButton(
color: materialColor,
textColor: materialTextColor,
disabledColor: Colors.grey,
disabledTextColor: Colors.white,
onPressed: onPressed,
child: Text(text, textAlign: TextAlign.center),
)
: CupertinoButton(
color: Colors.black,
onPressed: onPressed,
child: Text(text, textAlign: TextAlign.center),
),
);
}
}

Extra: CommandHistoryColumn

import 'package:flutter/material.dart';

class CommandHistoryColumn extends StatelessWidget {
final List<String> commandList;

const CommandHistoryColumn({
required this.commandList,
});

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Command history:', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 24),
if (commandList.isEmpty) const Text('Command history is empty.'),
for (final (i, command) in commandList.indexed)
Text('${i + 1}. $command'),
],
);
}
}

Extra: ShapeContainer

class ShapeContainer extends StatelessWidget {
final Shape shape;

const ShapeContainer({
required this.shape,
});

@override
Widget build(BuildContext context) {
return SizedBox(
height: 160.0,
child: Center(
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
height: shape.height,
width: shape.width,
decoration: BoxDecoration(
color: shape.color,
borderRadius: BorderRadius.circular(10.0),
),
child: const Icon(
Icons.star,
color: Colors.white,
),
),
),
);
}
}

CommandExample

En este ejemplo, el código del cliente (elementos de la UI, historial de comandos, etc.) no está acoplado a clases de comandos concretas(ConcreteCommands) porque funciona con comandos a través de la interfaz de comandos. Este enfoque permite introducir nuevos comandos en la aplicación sin romper ningún código existente.

class CommandExample extends StatefulWidget {
const CommandExample();

@override
_CommandExampleState createState() => _CommandExampleState();
}

class _CommandExampleState extends State<CommandExample> {
final _commandHistory = CommandHistory();
final _shape = Shape.initial();

void _changeColor() {
final command = ChangeColorCommand(_shape);
_executeCommand(command);
}

void _changeHeight() {
final command = ChangeHeightCommand(_shape);
_executeCommand(command);
}

void _changeWidth() {
final command = ChangeWidthCommand(_shape);
_executeCommand(command);
}

void _executeCommand(Command command) => setState(() {
command.execute();
_commandHistory.add(command);
});

void _undo() => setState(() => _commandHistory.undo());

@override
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: const ScrollBehavior(),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: LayoutConstants.paddingL,
),
child: Column(
children: <Widget>[
ShapeContainer(
shape: _shape,
),
const SizedBox(height: LayoutConstants.spaceM),
PlatformButton(
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _changeColor,
text: 'Change color',
),
PlatformButton(
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _changeHeight,
text: 'Change height',
),
PlatformButton(
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _changeWidth,
text: 'Change width',
),
const Divider(),
PlatformButton(
materialColor: Colors.black,
materialTextColor: Colors.white,
onPressed: _commandHistory.isEmpty ? null : _undo,
text: 'Undo',
),
const SizedBox(height: LayoutConstants.spaceM),
Row(
children: <Widget>[
CommandHistoryColumn(
commandList: _commandHistory.commandHistoryList,
),
],
),
],
),
),
);
}
}

y YA!

Otros articulos para esta serie

Creacionales:

Estructurales:

De Comportamiento:

Tu contribución

👏 ¡Presiona el botón de aplaudir a continuación para mostrar tu apoyo y motivarme a escribir mejor!
💬 Deje una respuesta a este artículo brindando sus ideas, comentarios o deseos para la serie.
📢 Comparte este artículo con tus amigos y colegas en las redes sociales.
➕ Sígueme en Medium.
⭐ Ve los ejemplos prácticos desde mi canal en Youtube: Pacha Code

--

--