segunda-feira, 4 de maio de 2009

VO (Value Object) vs DTO (Data Transfer Object)

Eu percebo que várias pessoas têm confundido o Value Objects com Data Transfer Objects, pra muita gente tudo isso quer dizer uma coisa, mas na prática não são, e isso é independente se você esta utilizando DDD.


Eu não quero ousar ficar dando definições para esses caras, por tanto, vou basear as minhas explicações nas definições do Martin Fowler:


Value Object
“A small simple object, like money or a date range, whose equality isn't based on identity.”


Um exemplo muito comum de Value Object que utilizamos no nosso dia-a-dia é a classe DateTime, perceba que a classe por si só não nos diz nada, na verdade as propriedades que a constituem é que são realmente importantes (dia, mês, ano, etc...).


Um exemplo muito comum em que se utiliza o Value Object é no caso de endereço, imagine aplicar as informações de endereço para Cliente e Fornecedor:


public class Cliente
{
        private string logradouro;
        private string bairro;
        private string cidade;
        private string estado;


        public string Logradouro
        {
                get{ return logradouro;}
                set{ logradouro = value;}
        }


        public string Bairro
        {
                get{ return bairro;}
                set{ bairro = value;}
        }


        public string Cidade
        {
                get{ return cidade;}
                set{ cidade = value;}
        }


        public string Estado
        {
                get{ return estado;}
                set{ estado = value;}
        }
}


public class Fornecedor
{
        private string logradouro;
        private string bairro;
        private string cidade;
        private string estado;

        public string Logradouro
        {
                get{ return logradouro;}
                set{ logradouro = value;}
        }

        public string Bairro
        {
                get{ return bairro;}
                set{ bairro = value;}
        }

        public string Cidade
        {
                get{ return cidade;}
                set{ cidade = value;}
        }

        public string Estado
        {
                get{ return estado;}
                set{ estado = value;}
        }
}


Agora veja uma outra forme de disponibilizar as informações de endereço para as classes:

public class Cliente
{
        private Endereco endereco;
        public Endereco Endereco
        {
                get{ return endereco;}
                set{ endereco = value;}
        }
}


public class Fornecedor
{
        private Endereco endereco;


        public Endereco Endereco
        {
                get{ return endereco;}
                set{ endereco = value;}
        }
}


public struct Endereco
{
        private string logradouro;
        private string bairro;
        private string cidade;
        private string estado;


        public string Logradouro
        {
                get{ return logradouro;}
                set{ logradouro = value;}
        }


        public string Bairro
        {
                get{ return bairro;}
                set{ bairro = value;}
        }


        public string Cidade
        {
                get{ return cidade;}
                set{ cidade = value;}
        }


        public string Estado
        {
                get{ return estado;}
                set{ estado = value;}
        }
}


Pronto, agora temos um Value Object de negócio aplicado em nosso sistema, mas existem mais detalhes que precisamos nos preocupar quando utilizar um Value Object.
Um Value Object tem características importantes que o diferenciam:


1) Construção:
No .Net geralmente o Value Object é criado como um struct, mas tome cuidado quando você estiver pensando em DDD, nem toda classe struct é um Value Object.

2) Determinar igualdade:
No mundo real quando lidamos com uma classe cliente para saber se um cliente é igual a outro podemos simplesmente comparar a o campo referente a chave primário do nosso banco de dados (ex.: cliente1.Id == cliente2.Id), isso já seria o suficiente para determinar se uma entidade é igual, mas na comparação entre value object é necessário comparar todas as propriedades que o compõe, no caso do DateTime, por exemplo, para saber se uma pessoa nasceu no mesmo dia de outra é necessário comparar o Dia, mês e ano dos objetos.

3) Contexto:
Apesar de ter usado a classe Endereço como um exemplo de Value Object de negócio, o contexto é um fator diferencial para determinar se um object deve ser tratado como um Value Object ou não, no caso dos Correios, por exemplo, cada endereço é tratado de forma única existe um identificador único para ele, pois é necessário calcular taxas como o sedex, nesse cenário o Endereço não será um Value Object e sim Entity.


Data Transfer Object
“An object that carries data between processes in order to reduce the number of method calls.”


Redução de chamadas é palavra chave na definição do DTO.
Na empresa em que trabalho nós utilizamos .Net com Adobe Flex, imagine um exemplo de uma classe simples de pedido:


namespace Domain.Entities
{
    public enum TipoEntrega
    {
        CorreioSedex,
        CorreioNormal,
        Transportadora
    }


    public class Pedido
    {
        private List<IItemPedido> items;
        private Cliente cliente;
        private Nullable<TipoEntrega> tipoEntrega;
        private Endereco destinoEntrega;
        private bool freteCalculado;
        private decimal frete;
        private decimal desconto;
        private decimal total;


        public Pedido()
            : this(null)
        {
        }


        public Pedido(Nullable<decimal> frete)
        {
            items = new List<IItemPedido>();
            if (frete != null)
            {
                this.frete = (decimal)frete;
                this.freteCalculado = true;
            }
        }


        public int AdicionarProduto(Produto produto)
        {
            return this.AdicionarProduto(produto, 1);
        }


        public int AdicionarProduto(Produto produto, int quantidade)
        {
            if (produto == null)
                throw new ArgumentNullException("produto");
            if (quantidade < 1)
                throw new ArgumentException("Não pode ser adicionar um produto cuja quantidade seja inferior a 1");
            if (items.Exists(delegate(IItemPedido item) { return item.Nome.Equals(produto.Nome); }))
                throw new InvalidOperationException("O produto já se encontra no pedido");
            IItemPedido novoItem = new ItemPedido(produto, quantidade);
            items.Add(novoItem);
            //retorna o indice do produto adicionado
            return items.Count - 1;
        }


        public ReadOnlyCollection<IItemPedido> Items
        {
            get { return items.AsReadOnly(); }
        }


        public Cliente Cliente
        {
            get { return cliente; }
            set { cliente = value; }
        }


        public Nullable<TipoEntrega> TipoEntrega
        {
            get { return tipoEntrega; }
            set { tipoEntrega = value; }
        }


        public decimal Frete
        {
            get
            {
                if (!freteCalculado)
                {
                    CalculadorFreteFactory factory = CalculadorFreteFactory.GetInstance();
                    CalculadorFrete calculador = factory.ObterCalculador(this.tipoEntrega);
                    frete = calculador.CacularFrete(destinoEntrega.Cidade);
                    freteCalculado = true;
                }
                return frete;
            }
        }


        public Endereco DestinoEntrega
        {
            get { return destinoEntrega; }
            set { destinoEntrega = value; }
        }


        public decimal Desconto
        {
            get { return desconto; }
            set { desconto = value; }
        }


        public decimal Total
        {
            get
            {
                if (total == 0)
                {
                    foreach (IItemPedido item in items)
                    {
                        total += item.ValorParcial;
                    }
                    total += Frete - desconto;
                }
                return total;
            }
        }


        private class ItemPedido : IItemPedido
        {
            private Produto produto;
            private int quantidade;
            public ItemPedido(Produto produto)
                : this(produto, default(int))
            { }


            public ItemPedido(Produto produto, int quantidade)
            {
                this.produto = produto;
                this.quantidade = quantidade;
            }


            public int Quantidade
            {
                get { return quantidade; }
                set { quantidade = value; }
            }


            public string Nome
            {
                get { return produto.Nome; }
            }


            public decimal ValorUnitario
            {
                get { return produto.ValorUnitario; }
            }


            public decimal ValorParcial
            {
                get { return produto.ValorUnitario * quantidade; }
            }
        }
    }


    public class CalculadorFreteFactory
    {
        private static CalculadorFreteFactory uniqueInstance = new CalculadorFreteFactory();


        private CalculadorFreteFactory()
        { }


        public static CalculadorFreteFactory GetInstance()
        {
            return uniqueInstance;
        }


        public CalculadorFrete ObterCalculador(Nullable<TipoEntrega> tipoEntrega)
        {
            if (tipoEntrega == null)
                throw new ArgumentNullException("tipoEntrega");
            CalculadorFrete retorno;
            if (tipoEntrega == TipoEntrega.CorreioNormal)
                retorno = new CalculadorCorreioNormal();
            else if (tipoEntrega == TipoEntrega.CorreioSedex)
                retorno = new CalculadorFreteSedex();
            else
                retorno = new CalculadorFreteTransportadora();
            return retorno;
        }
    }


    public interface IItemPedido
    {
        string Nome { get;}
        decimal ValorUnitario { get; }
        decimal ValorParcial { get; }
        int Quantidade { get; set; }
    }


    public class Produto
    {
        private string nome;
        private decimal valorUnitario;


        public string Nome
        {
            get { return nome; }
            set { nome = value; }
        }


        public decimal ValorUnitario
        {
            get { return valorUnitario; }
            set { valorUnitario = value; }
        }
    }


    public class Cliente
    {
        private int id;
        private InformacoesPessoais informacoesPessoais;
        private Endereco endereco;


        public InformacoesPessoais InformacoesPessoais
        {
            get { return informacoesPessoais; }
            set { informacoesPessoais = value; }
        }


        public int Id
        {
            get { return id; }
            set { id = value; }
        }


        public Endereco Endereco
        {
            get { return endereco; }
            set { endereco = value; }
        }
    }


    public struct InformacoesPessoais
    {
        private string nome;
        private string sobreNome;
        private DateTime dataNascimento;


        public InformacoesPessoais(string nome)
            : this(nome, String.Empty, default(DateTime))
        { }


        public InformacoesPessoais(string nome, string sobreNome)
            : this(nome, sobreNome, default(DateTime))
        { }


        public InformacoesPessoais(string nome, string sobreNome, DateTime dataNascimento)
        {
            this.nome = nome;
            this.sobreNome = sobreNome;
            this.dataNascimento = dataNascimento;
        }


        public string Nome
        {
            get { return nome; }
            set { nome = value; }
        }


        public string SobreNome
        {
            get { return sobreNome; }
            set { sobreNome = value; }
        }


        public string NomeCompleto
        {
            get { return nome + " " + sobreNome; }
        }


        public DateTime DataNascimento
        {
            get { return dataNascimento; }
            set { dataNascimento = value; }
        }


        public int Idade
        {
            get
            {
                TimeSpan diff = DateTime.Now - dataNascimento;
                return diff.Days / 364;
            }
        }
    }


    public struct Endereco
    {
        private string logradouro;
        private string numero;
        private string bairro;
        private string cidade;
        private string estado;
        private string cep;


        public string Logradouro
        {
            get { return logradouro; }
            set { logradouro = value; }
        }


        public string Numero
        {
            get { return numero; }
            set { numero = value; }
        }


        public string Bairro
        {
            get { return bairro; }
            set { bairro = value; }
        }


        public string Cidade
        {
            get { return cidade; }
            set { cidade = value; }
        }


        public string Estado
        {
            get { return estado; }
            set { estado = value; }
        }


        public string Cep
        {
            get { return cep; }
            set { cep = value; }
        }
    }
}


namespace Domain.Services
{
    public abstract class CalculadorFrete
    {
        public abstract decimal CacularFrete(string cidadeDestino);
    }


    public class CalculadorFreteSedex : CalculadorFrete
    {
        public override decimal CacularFrete(string cidadeDestino)
        {
            if (cidadeDestino.Equals("Sorocaba"))
                return 15;
            else
                return 25;
        }
    }


    public class CalculadorCorreioNormal : CalculadorFrete
    {
        public override decimal CacularFrete(string cidadeDestino)
        {
            if (cidadeDestino.Equals("Sorocaba"))
                return 10;
            else
                return 25;
        }
    }


    public class CalculadorFreteTransportadora : CalculadorFrete
    {
        public override decimal CacularFrete(string cidadeDestino)
        {
            if (cidadeDestino.Equals("Sorocaba"))
                return 30;
            else
                return 35;
        }
    }
}


No momento de realizar a comunicação com o Adobe Flex teremos vários problemas:

  • Não podemos replicar as regras de negócio que estão contidas nelas para o Adobe Flex;
  • Existem algumas características nas classes que são complicadas de serem implementadas no Flex (Ex.: classes tipos enum e aplicação de Lazy Loading);

Nesse caso uma solução simples para realizar a comunicação seria criar uma classe DTO somente para realizar a comunicação entre o .Net e Adobe Flex:


namespace ComunicacaoAdobeFlex.ValueObjects
{
public class Pedido
{
private Cliente cliente;
private decimal frete;
private decimal desconto;
private decimal total;


public Cliente Cliente
{
get { return cliente; }
set { cliente = value; }
}


public decimal Frete
{
get { return frete; }
set { frete = value; }
}


public decimal Desconto
{
get { return desconto; }
set { desconto = value; }
}


public decimal Total
{
get { return total; }
set { total = value; }
}
}


public class Cliente
{
private string nome;
private string sobreNome;
private DateTime dataNascimento;
private int idade;


public string Nome
{
get { return nome; }
set { nome = value; }
}


public string SobreNome
{
get { return sobreNome; }
set { sobreNome = value; }
}


public DateTime DataNascimento
{
get { return dataNascimento; }
set { dataNascimento = value; }
}


public int Idade
{
get { return idade; }
set { idade = value; }
}
}
}


Perceba que no exemplo acima, além de criar uma classe mais simples, ela contém somente as informações que o Adobe Flex irá utilizar (isso foi de propósito).


Com certeza existem outras características que podem ser elencadas para diferenciar Value Object e Data Transfer Object, mas espero que as características levantadas possam iluminar o seu caminho.

Nenhum comentário: