Ir al contenido
wannacode

Por qué patrones

En resumen: Un patrón de diseño es una solución con nombre y probada para un problema que se repite en el código; los 23 patrones clásicos forman tres familias: de creación, estructurales y de comportamiento.

Problema

Dos párrafos para una sola palabra

Una revisión de código en una tienda en línea. Pablo lee la caja y deja un comentario en la línea marcada:

class Checkout
{
private readonly Dictionary<string, Func<decimal, decimal>> rules = new()
{
["tarjeta"] = amount => amount,
["contra reembolso"] = amount => amount + 2.50m,
["cupón"] = amount => Math.Max(amount - 10, 0),
};
public decimal Pay(string method, decimal amount)
{
return rules[method](amount);
}
}
var checkout = new Checkout();
Console.WriteLine($"{checkout.Pay("contra reembolso", 80):F2} €");
Console.WriteLine($"{checkout.Pay("cupón", 80):F2} €");
Salida
82.50 €
70.00 €

Pablo: No entiendo qué es esto. La caja guarda una tabla en la que cada método de pago es una regla aparte. Todas las reglas tienen la misma forma: toman una suma y devuelven una suma. Mientras el programa corre, la caja toma la regla por el nombre que eligió el cliente y la llama, sin saber cuál es. ¿Es a propósito? Si mañana llega el pago a plazos, ¿tengo que tocar Pay?

María: Es a propósito. No tocas Pay, añades una fila a la tabla. La regla solo calcula y no recuerda nada, por eso la caja puede preguntarle varias veces: para la vista previa, para la factura, para el pago. Si no, cada pregunta daría otra suma…

Dos personas, dos párrafos, y lo más importante sigue sin decirse: ¿qué pasa si el nombre no está en la tabla?

María podría haber escrito una palabra: Strategy. Pablo habría sabido qué estaba viendo y qué revisar. La regla solo calcula. Un método nuevo es una fila nueva, y la caja no se toca. Un nombre desconocido se atrapa antes de llegar a la tabla. Mucha gente ya resolvió estas preguntas, y están escritas bajo ese nombre.

La idea

Un patrón es el nombre de una solución

Un patrón de diseño es una solución probada para un problema que aparece en muchos proyectos, con un nombre que todos conocen. No es una biblioteca ni código para copiar: cada proyecto lo escribe a su manera. Un patrón lleva cuatro cosas:

  • un nombre: una palabra en lugar de dos párrafos;
  • el problema que resuelve: por ejemplo «la regla cambia, y la caja no debe tocarse»;
  • los roles: quién guarda qué y quién llama a quién;
  • el costo: lo que pagas por él (más clases, un salto más al leer) y cuándo no vale la pena.

La mayoría de los nombres vienen del libro «Design Patterns» (1994), de cuatro autores a quienes los desarrolladores llaman la «Gang of Four». Describe 23 patrones en tres familias. El módulo recorre los más usados:

Repository y las capas no es del libro. Es un patrón de arquitectura: dónde vive la base de datos en el programa.

El módulo termina con un ejercicio, La tienda: qué patrón dónde: una tienda desordenada que ordenas tú.

Míralo

La trampa: un patrón por el patrón

La tarea es una sola: un 10 % de descuento en pedidos de más de 100 €. Alguien acaba de leer sobre patrones:

interface IDiscountStrategy
{
decimal Apply(decimal amount);
}
class PercentOverThresholdDiscountStrategy : IDiscountStrategy
{
private readonly decimal percent;
private readonly decimal threshold;
public PercentOverThresholdDiscountStrategy(decimal percent, decimal threshold)
{
this.percent = percent;
this.threshold = threshold;
}
public decimal Apply(decimal amount)
{
return amount > threshold ? amount * (1 - percent / 100) : amount;
}
}
abstract class AbstractDiscountStrategyFactory
{
public abstract IDiscountStrategy CreateStrategy();
}
class DefaultDiscountStrategyFactory : AbstractDiscountStrategyFactory
{
public override IDiscountStrategy CreateStrategy()
{
return new PercentOverThresholdDiscountStrategy(10, 100);
}
}
class DiscountService
{
private readonly AbstractDiscountStrategyFactory factory;
public DiscountService(AbstractDiscountStrategyFactory factory)
{
this.factory = factory;
}
public decimal Discounted(decimal amount)
{
return factory.CreateStrategy().Apply(amount);
}
}
var service = new DiscountService(new DefaultDiscountStrategyFactory());
Console.WriteLine($"Total: {service.Discounted(120):F2} €");
Console.WriteLine($"Total: {service.Discounted(80):F2} €");
Salida
Total: 108.00 €
Total: 80.00 €

Cinco clases, dos patrones (Strategy y una fábrica) y un nombre como AbstractDiscountStrategyFactory. La misma tarea, sin patrón:

static decimal Discounted(decimal amount)
{
return amount > 100 ? amount * 0.9m : amount;
}
Console.WriteLine($"Total: {Discounted(120):F2} €");
Console.WriteLine($"Total: {Discounted(80):F2} €");
Salida
Total: 108.00 €
Total: 80.00 €

La salida es la misma. La diferencia está en la lectura: para saber de cuánto es el descuento, en la primera versión pasas por cinco clases, y en la segunda lees una línea.

Los patrones de la primera versión no resuelven ningún problema. Hay un solo descuento, nadie lo cambia mientras el programa corre y nadie elige entre descuentos. Cuando llega el segundo descuento y el cliente tiene que elegir, ahí es cuando la función se convierte en una estrategia. No antes.

Pruébalo

Qué familia, y si hace falta un patrón

1. Builder, Singleton y Factory Method pertenecen a una misma familia. ¿A cuál?
2. La biblioteca del servicio de mensajería quiere sendParcel(city, grams), y tu código llama a ship(city, kilograms). ¿De qué familia es la solución?
3. La etiqueta muestra el precio «con IVA» o «sin IVA». Se decide en un solo lugar, y las dos variantes no han cambiado en años. ¿Necesitas un patrón?
4. El cliente elige uno de cinco métodos de pago, y cada mes llega uno nuevo. ¿Necesitas un patrón?

La idea

Cuándo un patrón estorba

Un patrón es la respuesta a un problema. Sin el problema, solo queda el costo:

  • «Quizá lo necesitemos». Una interfaz con una sola implementación, una fábrica para una sola clase, una estrategia con una sola variante. Todos los que leen el código pagan por ello, y nadie lo usa.
  • El nombre dice el patrón, no el trabajo. AbstractDiscountStrategyFactory dice cómo está hecho, Discounted dice qué hace. Si no puedes describir la clase sin las palabras del patrón, probablemente no la necesitas.
  • Un patrón de otro lenguaje. Donde bastaría un delegado (Func<decimal, decimal>), una interfaz y una clase por cada regla son demasiado.
  • El patrón antes del dolor. El buen orden es el contrario. Escribes lo simple. Cuando el mismo switch aparece en un tercer lugar o las reglas pasan a ser cinco, refactorizas hacia el patrón. Para entonces ya sabes exactamente qué cambia.

Los patrones se aprenden para reconocerlos y nombrarlos en el código ajeno y en las revisiones. No para ponerlos en todas partes.

Ejercicio

Reconoce el patrón

Resuélvelo en tu editor.

Tres fragmentos de una misma tienda. Ejecútalos y di de cada uno: ¿hay un patrón?, ¿cuál es (Strategy, Observer, Factory Method, Singleton o Adapter, de la lección «Patrones de diseño» del módulo C# moderno?) y, en una frase, qué problema resuelve. Uno de los tres no necesita ningún patrón.

A.

class Order
{
private readonly List<Action<string>> listeners = new();
public void OnShipped(Action<string> listener)
{
listeners.Add(listener);
}
public void Ship(string id)
{
foreach (var listener in listeners)
{
listener(id);
}
}
}
var order = new Order();
order.OnShipped(id => Console.WriteLine($"Correo: el pedido {id} fue enviado"));
order.OnShipped(id => Console.WriteLine($"Almacén: quita {id} de la cola"));
order.Ship("1042");
Salida
Correo: el pedido 1042 fue enviado
Almacén: quita 1042 de la cola

B.

class CourierApi
{
public string SendParcel(string city, int grams)
{
return $"Paquete para {city}, {grams} g";
}
}
interface IShipping
{
string Ship(string city, decimal kilograms);
}
class CourierShipping : IShipping
{
private readonly CourierApi api = new();
public string Ship(string city, decimal kilograms)
{
return api.SendParcel(city, (int)Math.Round(kilograms * 1000));
}
}
IShipping shipping = new CourierShipping();
Console.WriteLine(shipping.Ship("Sevilla", 1.25m));
Salida
Paquete para Sevilla, 1250 g

C.

static string PriceLabel(decimal price, bool withVat)
{
if (withVat)
{
return $"{price * 1.20m:F2} € con IVA";
}
return $"{price:F2} € sin IVA";
}
Console.WriteLine(PriceLabel(25, true));
Console.WriteLine(PriceLabel(25, false));
Salida
30.00 € con IVA
25.00 € sin IVA
Las respuestas
  • A es Observer. El pedido no sabe quién lo escucha. El correo y el almacén se suscriben, y Ship los llama en el orden en que se suscribieron. Un oyente nuevo no toca Order.
  • B es Adapter. CourierShipping traduce tu Ship (kilogramos) al SendParcel ajeno (gramos), sin tocar ninguno de los dos.
  • C no necesita patrón. Dos variantes, en un solo lugar, que nadie cambia. Strategy aquí sería una interfaz y dos clases en lugar de un solo if.

Más tareas

  • Cuándo C necesita un patrón. La tienda empieza a vender en cinco países con distinto IVA, y el cliente elige el país. ¿Qué cambia? Escribe qué patrón ayudaría y por qué, sin código.
  • El patrón que desaparece. Toma las cinco clases de la tarjeta «La trampa» y reemplázalas por una sola función. Cuenta las líneas antes y después. La salida debe seguir igual.
  • Tu código. Busca en uno de tus proyectos un switch o una cadena de if que se repita en dos lugares. ¿Con qué palabra lo nombrarías en una revisión?

Test

Ponte a prueba

1. ¿Qué es un patrón de diseño?
2. ¿Por qué ayuda el nombre de un patrón en una revisión de código?
3. ¿Qué señal indica que un patrón no hace falta?
4. ¿Cuál es el buen orden?
5. Adapter, Decorator y Composite son:

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·