Gabriel Jiménez | Hace más de 1 año
En nuestros inicios como programadores comenzamos escribiendo código conforme se nos ocurra y esto es totalmente valido, estamos aprendiendo. Sin embargo, a la hora de ir adquiriendo más experiencia, nos empezamos a cuestionar si el código que estoy escribiendo es entendible para otro dev y fácil de testear.
Los patrones de diseño son soluciones a problemas recurrentes en el día a día de los devs. Por lo que son un buen instrumento para que nuestro código sea entendible por otros.
Para que un patrón sea entendible fácilmente por otros, generalmente tiene un nombre, descripción del problema, solución, estructura y los participantes. Estos puntos hacen que cualquier patrón de diseño sea fácil de entender por los devs.
El patrón de diseño Composición permite que los objetos se compongan en estructuras de árbol para representar jerarquías de parte-todo. Este patrón trata de individuos y sus composiciones de forma uniforme.
Design Patterns: Elements of Reusable Object-Oriented Software" por Erich Gamma, Richard Helm, Ralph Johnson, y John Vlissides (1994)
Para entender una estructura jerárquica podemos pensar en un organigrama organizacional donde existen áreas y roles dentro de ellas.
Director General
Gerente de Ventas
Gerente de Marketing
Ahora bien, supongamos que necesitamos una aplicación donde nos permita pagar un servicio. El servicio necesita un tipo, un monto, una cuenta de cliente y el tipo de moneda.
Los tipos que existen son Recargas y Paquetes Telcel.
Los tipos de moneda que se aceptan son MXN y USD.
1: public Payment paidService(Payment payment) {
2: validarTipoServicio(payment);
3: validarTipoMoneda(payment);
4:
5: var saldo = validarSaldoCliente(payment);
6: if(payment.getAmount() > saldo) {
7: throw new UnprocessableEntityException("No tiene el saldo suficiente para realizar un pago de servicio");
8: }
9:
10: var pagoServicioTerceroRes = realizarPagoServicioTercero(payment);
11:
12: sincronizarPagoConPagoServicioTerceroRes(pagoServicioTerceroRes);
13:
14: return payment;
15: }
Esta solución parece bastante fácil de entender, sin embargo, conforme vaya creciendo iremos perdiendo el foco. Supongamos que el día de mañana se nos pide insertar logs, ya que al ser un proceso que maneja dinero es super importante trazar el camino por donde pasa el pago.
1: public Payment paidService(Payment payment) {
log.info("inicio del pago “ + payment);
log.info("inicio de validarTipoServicio “ + payment);
2: validarTipoServicio(payment);
log.info("fin de validarTipoServicio “ + payment);
log.info("inicio de validarTipoMoneda “ + payment);
3: validarTipoMoneda(payment);
log.info("fin de validarTipoMoneda “ + payment);
4:
log.info("inicio de validarSaldoCliente “ + payment);
5: var saldo = validarSaldoCliente(payment);
log.info("fin de validarSaldoCliente “ + payment);
6: if(payment.getAmount() > saldo) {
7: throw new UnprocessableEntityException("No tiene el saldo suficiente para realizar un pago de servicio");
8: }
9:
log.info("inicio de realizarPagoServicioTercero “ + payment);
10: var pagoServicioTerceroRes = realizarPagoServicioTercero(payment);
log.info("fin de realizarPagoServicioTercero “ + payment);
11:
log.info("fin de sincronizarPagoConPagoServicioTerceroRes “ + payment);
12: sincronizarPagoConPagoServicioTerceroRes(pagoServicioTerceroRes);
log.info("inicio de sincronizarPagoConPagoServicioTerceroRes “ + payment);
13:
log.info("fin del pago “ + payment);
14: return payment;
15: }
El proceso de paidServer se empieza a ensuciar poco a poco conforme nuevos requerimientos. La siguiente semana se nos pide agregar un requerimiento. Una vez que se realiza el pago del servicio usando un servicio de terceros se nos solicita notificar al servicio de saldos el estatus del pago.
1: public Payment paidService(Payment payment) {
log.info("inicio del pago “ + payment);
log.info("inicio de validarTipoServicio “ + payment);
2: validarTipoServicio(payment);
log.info("fin de validarTipoServicio “ + payment);
log.info("inicio de validarTipoMoneda “ + payment);
3: validarTipoMoneda(payment);
log.info("fin de validarTipoMoneda “ + payment);
4:
log.info("inicio de validarSaldoCliente “ + payment);
5: var saldo = validarSaldoCliente(payment);
log.info("fin de validarSaldoCliente “ + payment);
6: if(payment.getAmount() > saldo) {
7: throw new UnprocessableEntityException("No tiene el saldo suficiente para realizar un pago de servicio");
8: }
9:
log.info("inicio de realizarPagoServicioTercero “ + payment);
10: var pagoServicioTerceroRes = realizarPagoServicioTercero(payment);
log.info("fin de realizarPagoServicioTercero “ + payment);
log.info("inicio de notificaciónPagoAlServicioSaldos “ + pagoServicioTerceroRes);
var pagoServicioTerceroRes = notificaciónPagoAlServicioSaldos(pagoServicioTerceroRes);
log.info("fin de notificaciónPagoAlServicioSaldos “ + pagoServicioTerceroRes);
11:
log.info("fin de sincronizarPagoConPagoServicioTerceroRes “ + payment);
12: sincronizarPagoConPagoServicioTerceroRes(pagoServicioTerceroRes);
log.info("inicio de sincronizarPagoConPagoServicioTerceroRes “ + payment);
13:
log.info("fin del pago “ + payment);
14: return payment;
15: }
Conforme el código vaya creciendo será más difícil de interpretarlo. Por lo que surge la necesidad de pensar en un nuevo enfoque para estructurar el código. ¿El patrón composición se adaptaría a nuestro problema?
paidService
stopService
Sumado a esto, tenemos que varios subprocesos comparten la data, ya sea del pago o de la respuesta propia del subproceso.
Autor: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (1994).
Siguiendo la definición, tendríamos.
// Component
interface PaymentServiceOperation {
Object execute();
}
// Composite
class PaidProcess {
private static final Logger logger = LoggerFactory.getLogger(PaidServiceProcess.class);
private final List<AbstractPaymentOperation> operations = new ArrayList<>();
public void addOperation(AbstractPaymentOperation operation) {
operations.add(operation);
}
public void execute(Payment payment, Object data) {
logger.info("Starting paid service process for payment ID: {}", payment.getId());
try {
for (AbstractPaymentOperation operation : operations) {
logger.info("Executing operation: {}", operation.getClass().getName());
operation.setPayment(payment);
data = operation.execute(data);
}
} finally {
paymentRepository.save(payment);
logger.info("Api apiRequest saved: {}", payment.getId());
}
}
}
// Leaf
class ValidarTipoServicio extends AbstractPaymentOperation {
@Override
public Object execute(Object data) {
return new Object();
}
}
class ValidarTipoMoneda extends AbstractPaymentOperation {
@Override
public Object execute(Object data) {
return new Object();
}
}
class ValidarSaldoCliente extends AbstractPaymentOperation {
@Override
public Object execute(Object data) {
return new Object();
}
}
class RealizarPagoServicioTercero extends AbstractPaymentOperation {
@Override
public Object execute(Object data) {
return new Object();
}
}
class NotificacionPagoAlServicioSaldos extends AbstractPaymentOperation {
@Override
public Object execute(Object data) {
return new Object();
}
}
class SincronizarPagoConPagoServicioTerceroRes extends AbstractPaymentOperation {
@Override
public Object execute(Object data) {
if (!(data instanceof PagoServicioTerceroDto)) {
throw new IllegalArgumentException("Invalid data type for SincronizarPagoConPagoServicioTerceroRes");
}
return new Object();
}
}
// Esta clase es extra, la ocupamos para mantener accesible el Payment a través de todo el proceso
abstract class AbstractPaymentOperation implements PaymentServiceOperation {
protected Payment payment;
}
public Payment paidService(Payment payment) {
var paidProcess = new PaidProcess();
paidProcess.addOperation(new ValidarTipoServicio());
paidProcess.addOperation(new ValidarTipoMoneda());
paidProcess.addOperation(new ValidarSaldoCliente());
paidProcess.addOperation(new RealizarPagoServicioTercero());
paidProcess.addOperation(new NotificacionPagoAlServicioSaldos());
paidProcess.addOperation(new SincronizarPagoConPagoServicioTerceroRes());
// Si es necesario inicializar el proceso con algún valor.
paidProcess.execute(payment, null);
return payment;
}
Al implementar este patrón, el proceso luce más limpio y fácil de entender por cualquier dev que no tenga contexto sobre el proceso. Sin embargo, hay que ser cautelosos al estar agregando capas de abstracción a nuestras aplicaciones, a futuro pueden ser difíciles de mantener.
Aquí es donde el patrón se conecta directamente con las pruebas.
Cada Leaf:
El Composite:
El testing deja de ser una carga y se vuelve natural.
Aunque el ejemplo es backend, el concepto es exactamente el mismo en frontend:
Cuando un flujo de UI se diseña como composición:
El patrón no es backend ni frontend. Es diseño orientado a testing.
El mayor beneficio del patrón de composición no es solo código más limpio, sino código diseñado para ser probado.
Cuando los flujos se modelan como operaciones pequeñas y claras:
Si te interesa profundizar en cómo este tipo de diseño simplifica las pruebas en interfaces y formularios, puedes continuar en la sección de Testing en React, donde llevamos estas ideas al frontend paso a paso.
Hub de Testing