Eventos

[.NET] Principio de Inversión de Dependencias (D)

Ahora veremos el último de los Principios de Diseño Orientado a Objetos, para el cual les recomiendo antes, revisar y entender el Principio de Responsabilidad Única, el de Abierto/Cerrado y el de Segregación de Interfaces.

Dicho esto, ya podemos empezar con el Principio de Inversión de Dependencias, el cual sostiene dos puntos clave, el primero de ellos es:

"Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deben depender de abstracciones."

Entendamos los módulos de alto nivel como aquellos elementos de nuestro sistema que están más cerca de nuestro cliente, que bien podría ser una persona u otro sistema, y los de bajo nivel como aquellos que están más cerca al núcleo o centro de nuestro sistema.

El siguiente punto clave es:

"Las abstracciones no deben depender de detalles, son los detalles los que deben depender de abstracciones."

Por ejemplo, cuando vamos a comprar a un supermercado y debemos recibir algún comprobante, independientemente a que sea una boleta o una factura, existe la operación "Emitir Comprobante", y del mismo modo cuando falta poco tiempo para que acabe el servicio mensual de luz o agua, hay una suerte de "Enviar Notificación", que bien puede ser por correo electrónico, un documento físico o una llamada telefónica.

Las abstracciones deben capturar el flujo o comportamiento esencial de nuestro sistema sin caer en especificaciones del detalle de su implementación.

Dejando estos dos puntos claros pasemos al siguiente ejemplo:

using System.Net.Mail;
namespace DependencyInversionPrinciple
{
public class OrderApplication
{
public void Checkout(UserInfo userInfo, CardInfo cardInfo, Cart cart)
{
if (ChargeCard(cardInfo, cart))
{
ReserveInventory(cart);
NotifyUserByEmail(userInfo, cart);
}
}
public bool ChargeCard(CardInfo cardInfo, Cart cart)
{
using (var gateway = new PaymentGateway())
{
gateway.PaymentSettings = cardInfo;
gateway.AmountToCharge = cart.TotalAmount;
return gateway.Charge();
}
}
public void ReserveInventory(Cart cart)
{
var inventorySystem = new InventorySystem();
foreach (var item in cart.Items)
inventorySystem.Reserve(item.SKU, item.Quantity);
}
public void NotifyUserByEmail(UserInfo userInfo, Cart cart)
{
using (var message = new MailMessage("hello@epicalsoft.com", userInfo.CustomerEmail))
using (var smtpClient = new SmtpClient("localhost"))
{
message.Subject = "¡Tu orden esta lista!";
message.Body = "El detalle de tu orden es el siguiente: \n" + cart.Description;
smtpClient.Send(message);
}
}
}
}

Podemos apreciar que hay una serie de operaciones en relación al pago de un carro de compras y se podría decir que no hay problema, pero la verdad es que esta situación esta cometiendo algunas infracciones. Y esto es así porque estamos trabajando con detalles, con implementaciones concretas, además de tener una clase que no solo es responsable de hacer un Checkout, sino también de inicializar los servicios que usaremos en todo el proceso.

Antes de proseguir con esto, debo mencionar que así como existen los principios de diseño orientado a objetos también existen una variedad de patrones de diseño que nos pueden ayudar dependiendo a la situación y en esta ocasión, elegiremos la inyección de dependencias por constructor.

Entonces, para el anterior ejemplo, lo adecuado sería definir las siguientes interfaces según las operaciones que hemos identificado:

using System.Collections.Generic;
namespace DependencyInversionPrinciple
{
public interface IInventoryService
{
void Reserve(List<OrderItem> items);
}
public interface INotificationService
{
void NotifyCustomerOrder(UserInfo userInfo, Cart cart);
}
public interface IPaymentProcessor
{
bool ChargeCard(CardInfo cardInfo, Cart cart);
}
}
view raw Interfaces.cs hosted with ❤ by GitHub

Una vez definidas, ya podremos conseguir la siguiente actualización para la clase en cuestión y, como se puede apreciar, en el constructor de la clase se reciben las interfaces que previamente hemos definido.

namespace DependencyInversionPrinciple
{
public class OrderApplication
{
private readonly IInventoryService _inventoryService;
private readonly INotificationService _notificationService;
private readonly IPaymentProcessor _paymentProcessor;
private readonly UserInfo _userInfo;
private readonly CardInfo _cardInfo;
private readonly Cart _cart;
public OrderApplication(UserInfo userInfo,
CardInfo cardInfo,
Cart cart,
IInventoryService inventoryService,
INotificationService notificationService,
IPaymentProcessor paymentProcessor)
{
_userInfo = userInfo;
_cardInfo = cardInfo;
_cart = cart;
_inventoryService = inventoryService;
_notificationService = notificationService;
_paymentProcessor = paymentProcessor;
}
public void Checkout()
{
if (_paymentProcessor.ChargeCard(_cardInfo, _cart))
{
_inventoryService.Reserve(_cart.Items);
_notificationService.NotifyCustomerOrder(_userInfo, _cart);
}
}
}
}

La pregunta que viene ahora, ya que tenemos varias interfaces que eventualmente deben inicializarse, es: ¿Dónde inicializamos los objetos? Esto se puede realizar a través de un contenedor de inversión de control (IoC Container) o definiendo un constructor por defecto, pero para este ejemplo, las inicializaremos manualmente. Primero asegurémonos de implementar las interfaces:

public class LegacyInventoryService : IInventoryService { /*...*/ }
public class LegacyNotificationService : INotificationService { /*...*/ }
public class LegacyPaymentProcessor : IPaymentProcessor { /*...*/ }
view raw Impl.cs hosted with ❤ by GitHub

Y una vez hecho eso, ya podemos continuar de la siguiente forma, definiendo clases concretas según las interfaces y enviando como parámetros de constructor todo lo necesario para hacer nuestro Checkout.

using System;
using System.Collections.Generic;
namespace DependencyInversionPrinciple
{
internal class Program
{
private static void Main(string[] args)
{
IInventoryService inventoryService = new FakeInventoryService();
INotificationService notificationService = new FakeNotificationService();
IPaymentProcessor paymentProcessor = new FakePaymentProcessor();
var userInfo = new UserInfo { CustomerEmail = "hello@epicalsoft.com" };
var cardInfo = new CardInfo { CardNumber = "1111-1111-1111-1111" ,
ExpirationDate = new DateTime(2030, 06, 27) };
var cart = new Cart { Items = new List<OrderItem>(),
Description ="DIP Rules!",
TotalAmount = 42 };
var orderApplication = new OrderApplication(userInfo, cardInfo, cart,
inventoryService,
notificationService,
paymentProcessor);
orderApplication.Checkout();
}
}
}
view raw Program.cs hosted with ❤ by GitHub

Como pueden ver, resulto un poco más trabajoso entender este principio, pero la verdad es que trae interesantes oportunidades. Por ejemplo, podemos tener diferentes implementaciones de las interfaces y realizar pruebas con mucha facilidad.

public class LegacyInventoryService : IInventoryService { /*...*/ }
public class FakeInventoryService : IInventoryService { /*...*/ }
public class BetaInventoryService : IInventoryService { /*...*/ }
public class LegacyNotificationService : INotificationService { /*...*/ }
public class FakeNotificationService : INotificationService { /*...*/ }
public class BetaNotificationService : INotificationService { /*...*/ }
public class LegacyPaymentProcessor : IPaymentProcessor { /*...*/ }
public class FakePaymentProcessor : IPaymentProcessor { /*...*/ }
public class BetaPaymentProcessor : IPaymentProcessor { /*...*/ }
view raw Impl_v2.cs hosted with ❤ by GitHub

Para terminar, os quiero hacer acordar que estos principios de diseño son guías, si sientes que tu proyecto no amerita ser estricto con alguno de ellos, no te hagas problemas, lo importante es que sea eficiente.




No hay comentarios.:

Publicar un comentario

Epicalsoft — Superheroic Software Development Blog Designed by Templateism Copyright © 2014

Con tecnología de Blogger.