.NET - Princípio TAD : 'Tell don't ask'


 Neste artigo vou recordar o princípio 'Tell don´t ask'.

O princípio "Tell, Don't Ask" nos incentiva a estruturar nosso código de forma que os objetos recebam comandos em vez de expor seus dados para que outro código decida o que fazer. Isso reforça o encapsulamento e reduz o acoplamento, tornando o código mais orientado a objetos



Vejamos a seguir um código que viola esse princípio onde temos o serviço ContaBancariaService que pode informações de Saldo da conta representa pela classe ContaBancaria e depois toma a decisão sobre a operação.

1- ContaBancaria

public class ContaBancaria
{
    public decimal Saldo { get; private set; }
    public ContaBancaria(decimal saldoInicial)
    {
        Saldo = saldoInicial;
    }
    public void Depositar(decimal valor)
    {
        Saldo += valor;
    }
    public void Sacar(decimal valor)
    {
        if (Saldo >= valor)
            Saldo -= valor;
        else
            throw new InvalidOperationException("Saldo insuficiente!");
    }
}

2- ContaBancariaService

public class ContaBancariaService
{
    public void ProcessarSaque(ContaBancaria conta, decimal valor)
    {
        if (conta.Saldo >= valor)
        {
            conta.Sacar(valor);
        }
        else
        {
            Console.WriteLine("Saldo insuficiente!");
        }
    }
}

Neste cenário a classe ContaBancariaService está perguntando o saldo (conta.Saldo) e decidindo se pode fazer o saque. Isso quebra o princípio "Tell, Don’t Ask", pois o comportamento deveria estar dentro da própria ContaBancaria.

Vejamos como fica o código ajustado para que ContaBancaria seja responsável por suas próprias regras de negócio, sem expor o saldo diretamente

public class ContaBancaria
{
    private decimal _saldo;
    public ContaBancaria(decimal saldoInicial)
    {
        _saldo = saldoInicial;
    }
    public bool TentarSacar(decimal valor)
    {
        if (_saldo >= valor)
        {
            _saldo -= valor;
            return true;
        }
        return false;
    }
}

A seguir temos o código da classe ContaBancariaService :

public class ContaBancariaService
{
    public void ProcessarSaque(ContaBancaria conta, decimal valor)
    {
        if (conta.TentarSacar(valor))
        {
            Console.WriteLine("Saque realizado com sucesso!");
        }
        else
        {
            Console.WriteLine("Saldo insuficiente!");
        }
    }
} 

O que mudou ?

✔ Agora, ContaBancaria decide se o saque pode ser feito (TentarSacar) e realiza a operação.

✔ O serviço ContaBancariaService diz à conta o que fazer, em vez de pedir informações para tomar decisões.

✔ O encapsulamento foi preservado, evitando exposição indevida de dados.
Conclusão

O princípio "Tell, Don't Ask" nos ajuda a estruturar o código de forma que os próprios objetos sejam responsáveis por suas operações, em vez de expor dados para que outra classe tome decisões. Isso melhora a coesão, reduz o acoplamento e torna o código mais alinhado com os princípios da Programação Orientada a Objetos.

Naturalmente podemos melhorar o código incluindo o validações adicionais, logs ou notificações e o lançamento de exxceções ao invés de retornar um booleano.

Código ajustado:

public class ContaBancaria
{
    private decimal _saldo;
    private decimal _limiteDiario;
    private decimal _saqueHoje;
    public ContaBancaria(decimal saldoInicial, decimal limiteDiario)
    {
        _saldo = saldoInicial;
        _limiteDiario = limiteDiario;
        _saqueHoje = 0;
    }
    public void Sacar(decimal valor)
    {
        if (valor <= 0)
            throw new ArgumentException("O valor do saque deve ser maior que zero.");
        if (_saqueHoje + valor > _limiteDiario)
            throw new InvalidOperationException("Limite diário de saque excedido!");
        if (_saldo < valor)
            throw new InvalidOperationException("Saldo insuficiente!");
        _saldo -= valor;
        _saqueHoje += valor;
        Console.WriteLine($"Saque de R${valor:F2} realizado com sucesso.");
    }
    public decimal ObterSaldo() => _saldo;
} 

Código do serviço ajustado:

public class ContaBancariaService
{
    public void ProcessarSaque(ContaBancaria conta, decimal valor)
    {
        try
        {
            conta.Sacar(valor);
            RegistrarLog($"Saque de R${valor:F2} realizado.");
        }
        catch (Exception ex)
        {
            RegistrarLog($"Erro ao tentar sacar R${valor:F2}: {ex.Message}");
            Console.WriteLine($"Operação falhou: {ex.Message}");
        }
    }
    private void RegistrarLog(string mensagem)
    {
        // Simulando log (poderia ser para um arquivo, banco de dados, etc.)
        Console.WriteLine($"[LOG] {DateTime.Now}: {mensagem}");
    }
}

Para testar podermos usar o código a seguir na classe Program:

class Program
{
    static void Main()
    {
        var conta = new ContaBancaria(1000, 500); // Saldo inicial de R$1000, limite diário de R$500
        var service = new ContaBancariaService();
        service.ProcessarSaque(conta, 200);
        service.ProcessarSaque(conta, 400); // Deve falhar por exceder o limite diário
        service.ProcessarSaque(conta, -50); // Deve falhar por valor inválido
        service.ProcessarSaque(conta, 900); // Deve falhar por saldo insuficiente
    }
} 

 O que melhoramos ?

✔ Lançamento de exceções → Garante que erros sejam tratados corretamente.
✔ Limite diário de saque → Protege contra saques excessivos.
✔ Registro de logs → Ajuda a monitorar operações e identificar falhas.
✔ Melhoria na validação → Evita saques negativos e torna o código mais robusto.

Agora, ContaBancaria é totalmente responsável por suas regras, enquanto ContaBancariaService apenas coordena as operações e trata exceções.

Agora temos um design sólido, seguindo Tell, Don’t Ask, encapsulamento adequado, e boas práticas de logging e validação.

E estamos conversados...  

"Porquanto não há diferença entre judeu e grego; porque um mesmo é o Senhor de todos, rico para com todos os que o invocam."
Romanos 10:12

Referências:


José Carlos Macoratti