O “Tell, don’t ask” (diga, não peça) é um dos princípios da orientação a objetos que considero mais importantes, e também um dos mais difíceis de ver aplicado. Ele define claramente a diferença entre um sistema procedural e um sistema orientado a objetos. Vou explicar porque.

Uma linguagem é orientada a objetos porque, além de suportar todos os requerimentos formais, possui objetos como conceitos de primeira importância. Programar com uma linguagem orientada a objetos nos permite usar estes conceitos. Mas isso não significa que faremos isso. É absolutamente possível programar uma linguagem orientada a objetos de forma procedural, basta que você ignore o fato de que um objeto é, além de dados, também comportamento. Tratando os objetos como meros pacotes de dados você está efetivamente programando orientado a procedimentos. É aí que entra o “tell, don’t ask”.

Quando você diz a um objeto que deve fazer algo, é o objeto, dentro de suas regras internas e do seu encapsulamento, que vai fazer este algo. A outra opção é não dizer, mas pedir uma informação, e então decidir o que deve ser feito com ela.

Por exemplo, suponha um procedimento de emissão de notas fiscais:

public class NF
{
    public IList<Item> Itens {get; set; }
}

Você está acrescentando itens à nota:

var nf = ObterUmaNF();
var item = ObterItem();
nf.Itens.Add(item);

Esse é um código muito comum, certo? Mas ele viola diretamente o encapsulamento da classe de nota fiscal. Em vez de dizer à nota fiscal que deve acrescentar um item, estamos criando o item diretamente, e acrescentando ele. Estamos pedindo sua lista de itens, e acrescentando nós mesmos o item à lista. Esse código é procedural, não OO.

Há algumas alternativas para resolver isso. Mas antes de mais nada primeiro vamos deixar a lista de itens oculta na classe de NF:

public class NF
{
    protected IList<Item> ItensInterno {get; set; }
}

Dessa forma ninguém vai poder acrescentar itens à nossa lista de itens. “Mas como vamos poder acessar os itens da NF?”, você deve estar se perguntando. Bom, se você realmente precisar acessar esses itens (e essa questão vai dar argumento para outro post), você pode expô-los via um enumerador. Um enumerador não permite inclusões:

public class NF
{
    protected IList<Item> ItensInterno {get; set; }
    public IEnumerable<Item> Itens {get { return this.ItensInterno; } }
}

Para incluir um item é fácil. Nós dizemos à classe de NF que queremos criar um novo item, ou passando ele diretamente (estou ocultando o resto):

public class NF
{
    //lista e enumerador ocultos
    public void AdicionarItem(Item item)
    {
        this.ItensInterno.Add(item);
    }
}

Ou passando os dados necessários à inclusão do item e deixando a criação do item para a classe de NF.

public class NF
{
    //lista e enumerador ocultos
    public Item AdicionarItem(Produto produto, int quantidade, decimal preco)
    {
        var item = Item.Criar(produto, quantidade, preco); //algum factory method
        this.ItensInterno.Add(item);
        return item;
    }
}

Há cenários onde a primeira abordagem é melhor (o item já vem criado), principalmente se você está trabalhando com uma ligação mais simples, e uma camada de façade não baseada em DTOs. No segundo caso você está indo ainda mais a fundo, ao dizer que à classe de NF que quer incluir um item, mas sequer sabe como criá-lo. Ela ainda é gentil e te devolve o item criado, caso você queira atuar sobre ele.

Nesse cenário temos claramento dados encapsulados, e comportamento (incluir itens de nota fiscal). Nesse método está toda a regra de negócio necessária à inclusão de um item. Regras como a verificação do preço, por exemplo, poderiam ser acionadas de imediato.

Pedir a um objeto que revele detalhes de seu estado interno acaba muitas vezes levando a uma abordagem procedural. Não estou dizendo que essa é uma regra, mas é algo para se atentar. Sempre que acessar uma propriedade, verifique se não está tomando uma responsabilidade que deveria estar em outro lugar. Veja se não está violando o encapsulamento de uma classe.

Para deixá-los um pouco mais com a pulga atrás da orelha, vou apresentar um caso onde praticamente todos os desenvolvedores ASP.Net já violaram o princípio “tell, don’t ask”: validadores do webforms. A não ser que você tenha começado a usar ASP.Net agora, e caiu diretamente no ASP.Net MVC, vou já usou um validador. E validadores são um exemplo clássico de violação de encapsulamento. Quem deve validar o estado interno de uma entidade é ela mesma, não um formulário na interface gráfica (?!). Um validador viola tão profundamente o conceito de OO, que ele sequer pede à entidade por seu estado, ele primeiro determina se o valor é valido ou não, e só depois envia o dado à entidade. Mais procedural impossível.

E por fim, gostaria de lembrar porque sou tão contra trabalhar banco de dados antes de trabalhar o domínio: como o foco é o dado, você acaba pedindo dados o tempo tudo, muito mais do que dizendo a um objeto o que fazer.