Lazy Loading e Contextos de Domínio

Quando optamos por desenvolver uma aplicação orientada ao domínio (DDD), uma série de termos e conceitos devem ser entendidos para que sejam bem aproveitados e consigamos assim expressar em nosso código o que for necessário para atender a demanda daquela aplicação, serviço ou funcionalidade. O principal guia que agrupa todos esses termos é o livro do Eric Evans, chamado de Domain-Driven Design: Tackling Complexity in the Heart of Software.

Para persistência geralmente utilizamos repositórios (também descrito neste livro), que recorremos para inserir novos dados ou até mesmo para extrair os registros existentes, e mais tarde, persistir as eventuais alterações realizadas durante o processamento de alguma tarefa. E na maioria das implementações de repositórios, por trás (infraestrutura), sempre há um ORM que faz toda a mágica.

Na medida em que vamos desenvolvendo nosso domínio, vamos agregando às entidades diversas características e funcionalidades, tornando a classe cada vez mais próxima ao mundo real. E, como sabemos, os ORMs fornecem a possibilidade de habilitarmos um recurso chamado de lazy loading. Apenas para recordar, ele posterga a extração dos dados até que a mesma seja demandada, o que em outras palavras significa que a consulta será encaminhada à base de dados somente quando acessarmos a propriedade onde estão o(s) dado(s) custoso(s). É comumente relacionado à coleções, mas há situações onde se refere à outras classes ou até mesmo propriedades (como um array de bytes). Abaixo alguns exemplos (em negrito) do que poderia ser considerado como lazy loading:

public class Duplicata
{
    public AnaliseConfirmativa Confirmacao { get; private set; }

    public string Numero { get; private set; }

    public Sacado Sacado { get; }

    public IEnumerable<AcaoDeCobranca> AcoesDeCobranca
    {
        get
        {
            return acoesDeCobranca;
        }
    }

}

Com pouca configuração, os ORMs permitem postergar a carga das informações somente quando elas são solicitadas. Abaixo um pseudo-código que ilustra os vários momentos que vamos recorrer ao banco de dados; repare que somente quando acessamos as propriedades negritadas é que elas são extraídas, tornando um processo transparente para quem escreve o código.

var duplicata = repositorioDeTitulos.BuscarPorId(1);
//SELECT Numero, Sacado FROM Duplicata WHERE TituloId = 1

Console.WriteLine(duplicata.Confirmacao.Data);
//SELECT Data, Status, Confirmador FROM AnaliseConfirmativa WHERE Id = 3

foreach (var acao in duplicata.AcoesDeCobranca)
    //SELECT * FROM AcoesDeCobranca WHERE TituloId = 1
{
    //…
}

Como já era de se esperar, o ORM honra a configuração de lazy loading, e recorre ao banco somente quando de fato precisamos dos dados. Mas vamos detalhar melhor o que acontece no código acima: primeiramente recorremos ao repositório para extrair a duplicata de identificador 1. A consulta que foi feita devolve apenas os dados básicos da duplicata (número e sacado), e é tudo o que queremos até então. Logo que precisamos da parte da confirmação, uma nova consulta é feita. Por fim, quando queremos iterar pelas ações de cobrança, uma terceira consulta é feita para extrair os registros filhos.

Podemos perceber que as “partes” da duplicata são carregadas sob demanda, mas como disse anteriormente, em certos contextos, poderíamos poupar trabalho e já carregar juntamente com os dados básicos, os dados complementares para executar uma determinada ação. Vamos supor que tivéssemos dois ambientes: confirmação e cobrança. No primeiro ambiente, gostaria de que em uma única consulta me retornasse os dados inerentes ao processo de confirmação da duplicata; já no segundo ambiente, gostaria que as ações de cobrança também fossem extraídas tão logo quando a classe Duplicata fosse materializada.

Mas a configuração do ORM é única, não me permitindo customizar isso caso a caso, ambiente por ambiente. Se a performance é um ponto crucial da aplicação, é capaz de termos que começar a poluir a interface do repositório com métodos que retorne – ainda – a duplicata, mas que contextualizem para qual ambiente queremos:

var duplicata = repositorioDeTitulos.BuscarDuplicataParaConfirmacao(1);
var duplicata = repositorioDeTitulos.BuscarDuplicataParaCobranca(1);

E no interior de cada um dos métodos, recorreríamos a API do ORM para fazer a carga antecipada das informações relativas aquele contexto. Apesar de funcionar, em pouco tempo, é provável que o repositório comece a ter diversos métodos que estão ali mais para tentar “burlar” o ORM/sistema, e induzi-los a extrair os dados necessários para executar a operação desejada pelo usuário dentro daquele contexto.

Note que com um pequeno exemplo é possível ver o tamanho do problema que podemos ter ao criar um grande domínio e sem nos preocuparmos com a separação em contextos. Eles precisam ser bem pensados para tentar mantermos as entidades com a estrutura necessária para atender aquele contexto específico, caso contrário, podemos degradar a performance e termos dificuldades na manutenção e evolução da aplicação.

Publicidade