¡Buenas, buenas! a vos que estás curioseando del otro lado. Bienvenido al segundo post de esta serie sobre SOLID, hoy analizaremos el siguiente principio: Principio de Abierto/Cerrado. ¡¡¡Empecemos!!!
Open/Closed Principle (OCP) - Principio de Abierto/Cerrado
La definición de diccionario de este principio dice que un módulo debe estar abierto para extensiones pero cerrada para modificaciones.
¿Qué quiere decir?
Que debería ser capaz de agregar nuevas funcionalidades al código sin necesidad de modificar las existentes. Ejemplo rápido: pensemos en una casa, tengo que poder agregarle nuevas habitaciones (abierto a extensiones) sin tener que demoler o reconstruir las ya existentes (cerrado a modificaciones).
La importancia de este principio radica en evitar efectos secundarios, ya que al no modificar el código existente se reduce la posibilidad de introducir errores en módulos funcionales, facilitando así el mantenimiento.
Analicemos un punto clave de cómo se utiliza este principio antes de pasar a un ejemplo.
Polimorfismo: ya lo vimos en el post sobre POO (que si no lo leíste aún, te recomiendo arrancar por aquí), pero hagamos un breve resumen. Se trata de hacer uso de clases padres (clases base, abstractas o interfaces), para tener un contrato esperado y que las clases hijas se encarguen de las implementaciones.
Ahora sí, un ejemplo donde podría aplicar este principio.
Veamos cómo quedó el código del módulo anterior.
Primero vamos a tener nuestra base de clases Invoice y nuestro FakeStorage
public class Invoice
{
public List<InvoiceItem> Items { get; set; } = new List<InvoiceItem>();
}
public class InvoiceItem
{
public string Name { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
public class FakeStorage<T>
{
private ObservableCollection<T> collection;
public FakeStorage()
{
collection = new ObservableCollection<T>();
}
public T Add(T item)
{
collection.Add(item);
return item;
}
public IEnumerable<T> GetAll()
{
return collection;
}
}
nuestro InvoiceRepository e InovicePrinter
public class InvoiceRepository
{
private static FakeStorage<Invoice> storage;
public InvoiceRepository()
{
storage = new();
InitData();
}
private void InitData()
{
storage.Add(new Invoice
{
Items = new List<InvoiceItem>
{
new InvoiceItem { Name = "vino", Price = 54, Quantity= 2}
}
});
storage.Add(new Invoice
{
Items = new List<InvoiceItem>
{
new InvoiceItem { Name = "atun", Price = 80, Quantity= 5}
}
});
}
public IEnumerable<Invoice> GetAll()
{
return storage.GetAll();
}
}
public class InvoicePrinter
{
public void Print(decimal total)
{
Console.WriteLine($"El monto total de sus facturas es de {total}");
}
}
Y aquí el cambio, ahora tenemos dos InvoiceCalculator, uno para impuestos de Argentina y otro para Impuestos de USA.
public class InvoiceCalculatorArgentina
{
private const decimal iva = 1.21M;
public decimal GetTotal(IEnumerable<Invoice> invoices)
{
decimal total = 0;
foreach (var invoice in invoices)
{
total += invoice.Items.Sum(item => item.Price * item.Quantity) * iva;
}
return total;
}
}
public class InvoiceCalculatorUSA
{
private const decimal iva = 1.07M;
public decimal GetTotal(IEnumerable<Invoice> invoices)
{
decimal total = 0;
foreach (var invoice in invoices)
{
total += invoice.Items.Sum(item => item.Price * item.Quantity) * iva;
}
return total;
}
}
Finalmente, el Program para ver cómo interactuar con nuestro programa. Supongamos que va a imprimir las mismas facturas con el total para USA y el total para Argentina.
class Program
{
static void Main(string[] args)
{
PrintInvoices(new List<object>(){
new InvoiceCalculatorUSA(),
new InvoiceCalculatorArgentina()
});
}
static void PrintInvoices(List<object> calculators)
{
foreach (var calculator in calculators)
{
var InvoiceRepository = new InvoiceRepository();
var InvoicePrinter = new InvoicePrinter();
IEnumerable<Invoice> invoices = InvoiceRepository.GetAll();
if (calculator is InvoiceCalculatorUSA)
{
var usaCalculator = (InvoiceCalculatorUSA)calculator;
decimal total = usaCalculator.GetTotal(invoices);
InvoicePrinter.Print(total);
}
else
{
var argCalculator = (InvoiceCalculatorArgentina)calculator;
decimal total = argCalculator.GetTotal(invoices);
InvoicePrinter.Print(total);
}
}
}
}
Hasta aquí todo bien, pero notemos el problema de no aplicar el principio de Open-Close. Si quisiéramos agregar un nuevo país para calcular los totales de esas facturas e imprimirlo, deberíamos agregar la nueva clase, definir su comportamiento, y también modificar el if del program que ya no nos serviría. Deberíamos reemplazarlo por otra estructura, como un switch.
Veamos cómo modificar estas clases para cumplir el principio de open-close.
Primero creemos una clase abstracta, InvoiceCalculator
public abstract class InvoiceCalculator
{
protected virtual decimal Iva => 1M;
public decimal GetTotal(IEnumerable<Invoice> invoices)
{
decimal total = 0;
foreach (var invoice in invoices)
{
total += invoice.Items.Sum(item => item.Price * item.Quantity) * Iva;
}
return total;
}
}
Aquí podemos observar lo siguiente: Iva ahora es una propiedad protected y virtual, o sea solo puede accederse a ella desde la clase que la define o desde quienes la hereden.
También colocamos aquí el comportamiento esperado para GetTotal. Como las clases hijas van a compartir el mismo comportamiento, solo se necesita modificar el impuesto y este método se puede mantener inmutable en las clases hijas.
Ahora veamos cómo implementar esta abstracción en nuestras calculadoras existentes y sumemos una más.
public class InvoiceCalculatorUSA : InvoiceCalculator
{
protected override decimal Iva => 1.07M;
}
public class InvoiceCalculatorArgentina : InvoiceCalculator
{
protected override decimal Iva => 1.21M;
}
public class InvoiceCalculatorCostaRica : InvoiceCalculator
{
protected override decimal Iva => 1.13M;
}
Es mucho más sencillo, solo sobrescribimos el Iva.
Finalmente, veamos cómo modificar el program para interactuar con nuestro código ya creado y aprovechar la herencia de la abstracción.
class Program
{
static void Main(string[] args)
{
PrintInvoices(new List<InvoiceCalculator>(){
new InvoiceCalculatorUSA(),
new InvoiceCalculatorArgentina(),
new InvoiceCalculatorCostaRica()
});
Console.ReadKey();
}
static void PrintInvoices(List<InvoiceCalculator> calculators)
{
foreach (var calculator in calculators)
{
var InvoiceRepository = new InvoiceRepository();
var InvoicePrinter = new InvoicePrinter();
IEnumerable<Invoice> invoices = InvoiceRepository.GetAll();
decimal total = calculator.GetTotal(invoices);
InvoicePrinter.Print(total);
}
}
}
¿Qué pasó aquí? Como ya no necesitamos saber qué tipo de InvoiceCalculator es, podemos deshacernos del if y solamente indicar que realice el método de GetTotal. Luego cuando llegue el turno de cada implementación de InvoiceCalculator, estas sabrán qué hacer, ya que heredan este comportamiento del padre. También, como ahora tienen la misma base, ya podemos especificar que no es una lista de objetos sino una lista de InvoiceCalculator.
Ahora podemos ver cómo nuestro código cumple el principio. Cada vez que necesitemos agregar un nuevo país al cálculo de impuestos en los totales, solo debemos extender el código, creando una nueva clase que herede de InvoiceCalculator, es decir que esta parte del código quedó cerrada a modificaciones pero abierta a extensiones.
Hemos llegado al final del segundo posteo de esta serie sobre los principios SOLID. En los siguientes artículos exploraremos otro de los principios de SOLID para crear sistemas sólidos, flexibles y robustos. Espero que las explicaciones hayan sido claras, igualmente te invito a dejar tus dudas, sugerencias o ejemplos en los comentarios. Te leo!
Top comments (0)