Caso práctico: patrón Visitor

En este post voy a comentar un caso de uso del mundo real donde utilicé el patrón Visitor.

La historia comienza cuando en mi código me encontré con un método que estaba creciendo de tal forma que en poco tiempo tendría el siguiente aspecto (pseudo código):

public override void Procesar(FacturaViewModel entidad)
{
    //1-Crear Factura a partir de FacturaViewModel

    //2-Si la Factura implica mover stock
    //    Realizar Movimiento stock.

    //3-Si la Factura es de contado
    //    Realizar Operación Contable de Contado

    //4-Si la Factura es en Cuenta Corriente
    //    Realizar Operación Contable de CtaCte
    //    y generar comprobante XYZ

    //5-Si el monto total de la factura excede los 1000
    //    Aplicar consolidación de cliente

    //6-Si la Factura incluye "envases retornables"
    //    Generar comprobante de conformidad y
    //    actualizar info de envases XYZ

    base.Guardar(entidad);
}

Para simplificar he mencionado solo algunos escenarios o situaciones que se llevan a cabo en este proceso, según determinadas condiciones.

Esto era el ABC de mis servicios hasta hace algún tiempo, incluso es la forma que he utilizado ampliamente en aplicaciones VB6 (no estoy diciendo que ya no lo uso más). Este patrón esta muy bien explicado al estilo de Martin Fowler como Transaction Script.

Uno de los principales motivos por los cuales decidí este caso hacerlo con visitors, es que desde mi punto de vista esta suerte de Transaction Script tiene los siguientes problemas:

  • A mi criterio en algunos casos viola “Single Responsability Principle”; el principio dice que solo debería existir una razón para que una clase cambie. Depende en cierta forma de la granularidad que uno lo mire, pero para este escenario, encuentro la respuesta “Cambió el Script de Transacción” como inaceptable. Por lo tanto esta clase escrita de esta forma esta sujeta a cambios por múltiples razones:
    • Cambio la forma de tratar las facturas de contado.
    • Cambio la forma de mover stock.
    • etc.
  • Difícil de diseñar con TDD: esta es la razón principal que me hizo cambiar de idea en este escenario, realizar pruebas unitarias sobre un transaction script es prácticamente imposible. Lo que se debería hacer en realidad son pruebas con diferentes “escenarios” pero a mi criterio es mas una prueba de integración que unitaria. También quería destacar que configurar el contexto o escenario para la prueba involucra hacer mock de varios servicios.
  • Difícil de mantener y cambiar. Esto es el producto de los dos puntos anteriores.

Utilizando una especie de patrón Visitor

El patrón Visitor esta explicado en estos dos links (gracias a Fabio Maulo por los links y aclaraciones). La forma en que he aplicado el patrón es la siguiente, imaginese una interface así:

public interface IVisitor<T>
{
    bool Aplica(T entity);
    void Aplicar(T entity);
}

Esta interface tiene dos métodos. Aplica devuelve verdadero o falso dependiendo si el Visitor debe “visitar” dicha entidad del tipo T y el método Aplicar, que es el que finalmente ejecuta la acción sobre la entidad.

Mi servicio ahora tendría la siguiente forma

public class ServicioFacturacion 
{
    private readonly IVisitor<Factura>[] _visitors;
   
    public ServicioFactura(
        IVisitor<Factura>[] visitors, 
        ..) : base(..)
    {
        _visitors = visitors;
    }

    public override void Guardar(FacturaPedido entidad)
    {
        foreach (var visitor in _visitors)
        {
            if(visitor.Aplica(entidad))
            {
                visitor.Aplicar(entidad);
            }
        }
        base.Guardar(entidad);
    }
}

Como se puede ver el servicio ahora tiene una dependencia con un array de IVistor<Factura>. Probar esto es trivial:

var factura = new Factura();
var daoFactura = new Mock<IDao<Factura>>();

var visitor1 = new Mock<IVisitor<Factura>>();
visitor1.Setup(a => a.Aplica(factura)).Returns(true);

var visitor2 = new Mock<IVisitor<Factura>>();
visitor2.Setup(a => a.Aplica(factura)).Returns(false);


var modelo = new ServicioFacturacion(
        new[] {visitor1.Object, visitor2.Object},  
        new DaoFactoryMock().PushDao(daoFacturaPedido.Object));

modelo.Guardar(factura);


visitor1.VerifyAll();
visitor2.VerifyAll();
visitor2.Verify(a => a.Aplicar(factura), Times.Never());
visitor1.Verify(a => a.Aplicar(factura));
daoFactura.Verify(d => d.Save(factura));

Este test verifica:

  • Un visitor que aplica, debe aplicarse.
  • Un visitor que no aplica, no debe aplicarse.
  • El método Guardar del dao es llamado.

La idea en general es que uno ya esta usando inyección de dependencia y esto es muy fácil de configurar con cualquier container, por lo tanto nunca se construirá la instancia manualmente (excepto en el test je!). Aquí les dejo la referencia de como hacerlo con Castle Arrays, List and Dics.

Otra ventaja es que al estar dentro del Container, cualquier servicio puede ser inyectado en un Visitor.

Esto también nos da la posibilidad en el futuro de añadir visitors sin necesidad  de cambiar el servicio. Inclusive si los Visitors se crean en base a una interfaz pueden ser reutilizados en otros servicios similares.

Un ejemplo de un visitor es el siguiente:

public class VisitorMovimientoStock : IVisitor<Factura>
{
    private readonly IDaoReadOnly<Deposito> _daoDeposito;

    public VisitorMovimientoStock(IDaoFactory daoFactory)
    {
        _daoDeposito = daoFactory.GetDaoReadOnlyOf<Deposito>();
    }

    public bool Aplica(Factura entity)
    {
        return entity.Tipo.MueveStock;
    }

    public void Aplicar(Factura entidad)
    {
        var destino = _daoDeposito
                .First(d => d.Nombre == DepositosConocidos.EGRESO_VENTAS);

        var movimientosDeStock = entidad.Lineas
            .Select(l => l.ToMovimientoStock(entidad.PuntoVenta.DepositoVentas, destino))
            .ToList();

        entidad.AgregarMovimientosStock(movimientosDeStock);
    }
}

Hacer un test solamente de este aspecto es bastante fácil y voy a omitirlo.

Quería también pedir disculpas, por los nombres, sé que no son muy agradables. No me agrada mezclar ingles y castellano en los nombres de las clases, pero salió así!

Y por último comentar que este ejemplo es también parte del MundoReal.


blog comments powered by Disqus
  • Categories

  • Archives