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} €");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:
- De creación: cómo y dónde se hacen los objetos. Factory Method y Simple Factory, Builder, Singleton y Dependency Injection.
- Estructurales: cómo se juntan los objetos en algo más grande. Adapter y Facade, Decorator, Composite.
- De comportamiento: quién llama a quién y quién decide qué. Strategy, Observer, Command, State, Template Method e Iterator.
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} €");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} €");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
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.
AbstractDiscountStrategyFactorydice cómo está hecho,Discounteddice 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
switchaparece 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");Correo: el pedido 1042 fue enviadoAlmacén: quita 1042 de la colaB.
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));Paquete para Sevilla, 1250 gC.
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));30.00 € con IVA25.00 € sin IVALas respuestas
- A es Observer. El pedido no sabe quién lo escucha. El correo y el almacén se suscriben, y
Shiplos llama en el orden en que se suscribieron. Un oyente nuevo no tocaOrder. - B es Adapter.
CourierShippingtraduce tuShip(kilogramos) alSendParcelajeno (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
switcho una cadena deifque se repita en dos lugares. ¿Con qué palabra lo nombrarías en una revisión?
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Listas · Dictionary y HashSet · Lambdas y delegados · Patrones de diseño
- Ejercicios sobre el tema: Patrones de diseño