Encapsulamento protege invariantes

Uma invariante é uma condição que deve permanecer verdadeira para um objeto válido. Se um pedido não pode ser enviado sem itens, deixar sua lista pública e mutável permite que qualquer chamador viole essa regra. Em vez disso, o tipo deve oferecer operações que validam as mudanças permitidas.

public sealed class Pedido
{
    private readonly List _itens = [];

    public IReadOnlyCollection<string> Itens => _itens.AsReadOnly();
    public bool Enviado { get; private set; }

    public void AdicionarItem(string item)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(item);
        if (Enviado)
            throw new InvalidOperationException("Pedido já enviado.");

        _itens.Add(item);
    }

    public void Enviar()
    {
        if (_itens.Count == 0)
            throw new InvalidOperationException("Pedido sem itens.");

        Enviado = true;
    }
}

O estado interno e os pontos de alteração ficam sob controle do tipo. Expor IReadOnlyCollection evita oferecer diretamente uma API de alteração, mas wrappers de leitura não garantem snapshot imutável se a coleção por trás continuar mudando; escolha o contrato de acordo com o uso pretendido.

Propriedades não são sempre campos públicos disfarçados

Uma propriedade com get e set automáticos pode ser apropriada para dados simples. Porém, um setter irrestrito pode permitir transições que o domínio deveria validar. Use acesso mais restrito, init para inicialização controlada ou métodos que expressem ações e regras. Evite getters que executem operações inesperadas ou setters que escondam efeitos colaterais relevantes.

Abstração define o que o consumidor precisa

Uma abstração remove detalhes irrelevantes para determinada chamada, não toda complexidade do sistema. Um serviço pode depender de uma interface para gravar um documento sem conhecer o fornecedor de armazenamento. A interface deve expressar a necessidade do consumidor, e não espelhar todas as funções de uma classe concreta por precaução.

public interface IRepositorioPedidos
{
    Task SalvarAsync(Pedido pedido, CancellationToken cancellationToken);
}

Uma implementação SQL, em memória ou remota pode cumprir o contrato. O consumidor passa a depender da operação relevante. Isso é útil quando as implementações podem variar, quando uma fronteira arquitetural precisa ser protegida ou quando o contrato pertence naturalmente a mais de um componente.

Interface ou classe abstrata?

Uma interface descreve um contrato/capacidade que pode ser implementado por tipos de linhagens diferentes. Uma classe abstrata é uma classe-base não instanciável, pode manter estado de instância e compartilhar comportamento, e participa da herança simples de classes. Em C# moderno, interfaces podem conter implementações padrão, mas não substituem o estado compartilhado de instância de uma classe base.

Escolha interface quando importa o que o consumidor pode fazer, e classe abstrata quando há uma especialização real e uma implementação comum intencional. Prefira composição quando um comportamento pode ser delegado a uma estratégia ou colaborador independente. Veja herança e composição para custos e limites das hierarquias.

Não confunda esconder detalhes com adicionar cerimônia

Uma interface por classe sem variação, fronteira ou consumidor separado pode apenas aumentar navegação. Da mesma forma, getters/setters privados demais e métodos sem significado de domínio podem dificultar mais do que proteger. O encapsulamento deve tornar os estados inválidos difíceis de representar e as operações válidas fáceis de reconhecer.

Teste as regras pela API pública do tipo quando possível. Isso valida o comportamento que consumidores realmente usam sem acoplar cada teste à organização privada do código. Os modificadores private, protected, internal e outros ajudam a materializar as fronteiras; veja o guia de modificadores de acesso.

Resumo de decisão

  • Identifique quais condições precisam continuar verdadeiras.
  • Exponha operações que representam transições válidas, em vez de permitir edição arbitrária.
  • Defina contratos pequenos alinhados às necessidades de quem consome.
  • Use classe abstrata para compartilhar implementação em uma hierarquia verdadeira; interface para capacidades; composição para combinar comportamentos.
  • Evite abstrações sem benefício verificável e mantenha detalhes internos substituíveis.

Essas decisões se conectam a responsabilidade única, inversão de dependência e segregação de interfaces dos princípios SOLID.