José F. Romaniello

Las aventuras y desventuras de un codificador.

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.

| More

9 comentarios:

La idea me gusta, es mas o menos lo que yo uso para logica de negocios que cambia...
Obviamente esta limitado porque para crear nuevas reglas de negocio o modificar, necesitas compilar y tiene que ser hecho por un programador.

He visto que se usan DSL's para esto, pero la verdad nunca logre ver una forma "sencilla" en un caso real de aplicarlos, trate de seguir el libro de Ayende para hacerlo en Boo, pero no encontre un camino que me interese por ahi...
Si alguna vez investigas un DSL util para sacar estas reglas fuera del codigo y ponerlas al alcance de los expertos de dominio...bueno, postea :) .

José F. Romaniello dijo...

Si, ese es el paso siguiente :D
Rodolfo Finochietti nos cuenta que implemento BOO para un sistema en una
empresa de Argentina, la cual genera informes que indican si un argentino es
candidato para prestarle plata. Determinar si una persona es candidata
requiere evaluar diversas reglas, y el logro armar un DSL con BOO e
inyectarlas.
Esto es lo que yo entend� en la VAN, puedo estar totalmente equivocado. El
video de la VAN esta aqu�:
http://altnet-hispano.pbworks.com/van-2009-10-24-lenguaje-boo-introduccion .

Esa parte convendría resolverla con DomainEvent.
Practicamente luego del punto 1 el servicio solo tendría que disparar un DomainEvent del tipo NewInvoiceGoingToBeSaved pasar la fatura y guardar luego que se hayan ejecutados los eventos.
Los eventHandler o Listeners de los eventos se registran por afuera (en el GuyWire para entendernos) así que la logica, en ese caso, puede funcionar tranquilamente a plug-in.
Para José:
No te entusiasme con DomainEvent porque si exageras va a ser dolores.

José F. Romaniello dijo...

<span style="color: #808080;">Perdón que te contradiga Fabio, pero me entusiasma Domaint events... Creo me simplificaría mucho en cuanto a la inyección de visitors. Me desacopla ambas partes y creo que la configuración final termina siendo mas fácil.</span>
Sinceramente nunca use Domain Events, pero hace poco estuve leyendo algo y creo que se mas o menos como armarlo. 
Gracias por comentar!

Muy buena introducción al visitor pattern.. por cierto, yo también me hago bolas con los nombres en español y en ingles.. al final he decidido dejar todo en ingles (nombre de clases, metodos, variables, etc) y comentarios en español...

Una duda.. ¿como implementarias el metodo procesar si este debiese comportarse como una transacción? es decir, que si alguno de los pasos falla los pasos previos deben revertirse...

Saludos

José F. Romaniello dijo...

En mi c�digo esto es una transacci�n. Yo estoy utilizando
conversation-per-business-transaction:
http://fabiomaulo.blogspot.com/2009/01/aspect-conversation-per.html
El m�todo guardar esta marcado como [PersistenceConversation] de ah� en mas
todo lo que se haga, por m�s que sea en los visitors, forma parte de una
transacci�n at�mica. Si algo falla no se guarda nada.

<span style="color: #808080;">Y si estoy usando session-per-request con nhibernate, funcionaría mi transaction?</span>

José F. Romaniello dijo...

<span>Funciona perfectamente. Cada visitor podría tener dependencias a daos o repositorios y ejecutar cualquier cosa. Todo sucedería dentro de la transacción, cuyo ámbito es el request.</span>

<span>¿En qué estás pensando...?</span>

Publicar un comentario