Lei de Demeter na prática: encapsulamento real em objetos

1. Fundamentos da Lei de Demeter

A Lei de Demeter, também conhecida como Princípio do Mínimo Conhecimento, foi formulada no final dos anos 1980 no projeto Demeter da Northeastern University. Sua essência é simples: um objeto deve ter conhecimento limitado sobre a estrutura interna de outros objetos. Na prática, isso se traduz na regra "fale apenas com seus amigos imediatos".

A diferença fundamental entre acoplamento legítimo e violação de encapsulamento está na profundidade do conhecimento que um objeto possui sobre outro. Acoplamento legítimo ocorre quando objetos colaboram através de interfaces bem definidas. Violação de encapsulamento acontece quando um objeto precisa navegar pela estrutura interna de outro para realizar seu trabalho.

A regra prática pode ser resumida assim: um método de um objeto deve chamar apenas métodos de:
- Ele mesmo
- Objetos passados como parâmetros
- Objetos que ele cria diretamente
- Objetos armazenados em seus próprios atributos

2. Identificando violações da Lei de Demeter

O padrão mais comum de violação é o "trem de métodos" (method chaining excessivo), onde vemos sequências longas de chamadas encadeadas. Considere este exemplo:

// Violação da Lei de Demeter
public class RelatorioVendas {
    public void gerarRelatorio(Pedido pedido) {
        String cidade = pedido.getCliente().getEndereco().getCidade();
        String estado = pedido.getCliente().getEndereco().getEstado();
        // processamento...
    }
}

Este código expõe detalhes internos: o método gerarRelatorio precisa saber que Pedido tem um Cliente, que tem um Endereco, que tem cidade e estado. Qualquer mudança nessa estrutura quebra o código em múltiplos lugares.

As consequências são graves:
- Fragilidade: mudanças na estrutura interna propagam-se por todo o sistema
- Dificuldade de manutenção: cada novo requisito exige conhecimento profundo da hierarquia
- Quebra de encapsulamento: objetos expõem sua implementação interna

3. Estratégias de refatoração para encapsulamento real

A principal estratégia é substituir cadeias de chamadas por métodos delegados no objeto intermediário. Vejamos a refatoração do exemplo anterior:

// Versão refatorada - encapsulamento real
public class Pedido {
    private Cliente cliente;

    public String getCidadeDoCliente() {
        return cliente.getCidadeDoEndereco();
    }

    public String getEstadoDoCliente() {
        return cliente.getEstadoDoEndereco();
    }
}

public class Cliente {
    private Endereco endereco;

    public String getCidadeDoEndereco() {
        return endereco.getCidade();
    }

    public String getEstadoDoEndereco() {
        return endereco.getEstado();
    }
}

public class RelatorioVendas {
    public void gerarRelatorio(Pedido pedido) {
        String cidade = pedido.getCidadeDoCliente();
        String estado = pedido.getEstadoDoCliente();
        // processamento...
    }
}

O princípio "Tell, Don't Ask" guia essa transformação: em vez de perguntar sobre a estrutura interna, dizemos ao objeto o que queremos.

4. Aplicação prática com objetos de domínio

Vamos considerar um sistema de pedidos mais complexo:

// Antes - violação evidente
public class ProcessadorEntrega {
    public double calcularFrete(Pedido pedido) {
        String cidade = pedido.getCliente().getEndereco().getCidade();
        String estado = pedido.getCliente().getEndereco().getEstado();
        double pesoTotal = 0;
        for (Item item : pedido.getItens()) {
            pesoTotal += item.getProduto().getPeso() * item.getQuantidade();
        }
        return calcularPorRegiao(cidade, estado, pesoTotal);
    }
}

Refatoração completa:

// Depois - encapsulamento real
public class Pedido {
    private Cliente cliente;
    private List<Item> itens;

    public String getCidadeDoCliente() {
        return cliente.getCidadeDoEndereco();
    }

    public String getEstadoDoCliente() {
        return cliente.getEstadoDoEndereco();
    }

    public double getPesoTotal() {
        return itens.stream()
            .mapToDouble(Item::getPesoTotal)
            .sum();
    }
}

public class Item {
    private Produto produto;
    private int quantidade;

    public double getPesoTotal() {
        return produto.getPeso() * quantidade;
    }
}

public class ProcessadorEntrega {
    public double calcularFrete(Pedido pedido) {
        return calcularPorRegiao(
            pedido.getCidadeDoCliente(),
            pedido.getEstadoDoCliente(),
            pedido.getPesoTotal()
        );
    }
}

Para manter a coesão sem criar "objetos deus", cada objeto gerencia apenas operações diretamente relacionadas aos seus próprios dados.

5. Limites e exceções da Lei de Demeter

Existem contextos onde cadeias de métodos são aceitáveis e até desejáveis:

Builders:

Pizza pizza = new PizzaBuilder()
    .comMassa("fina")
    .comQueijo("mussarela")
    .comMolho("tomate")
    .build();

Streams:

List<String> nomes = pessoas.stream()
    .filter(p -> p.getIdade() > 18)
    .map(Pessoa::getNome)
    .collect(Collectors.toList());

DSLs:

query.select("nome")
     .from("usuarios")
     .where("ativo = true");

A distinção crucial é entre "acessar" e "comandar". Em builders, streams e DSLs, estamos construindo uma configuração ou pipeline, não violando encapsulamento de objetos de domínio.

6. Relação com outros princípios de design

A Lei de Demeter tem forte sinergia com o Single Responsibility Principle (SRP). Quando um objeto navega pela estrutura interna de outro, ele assume responsabilidades que não são suas. A refatoração que aplicamos realinha as responsabilidades.

A inversão de dependência reduz a necessidade de navegação entre objetos. Em vez de um serviço navegar por uma hierarquia para obter dados, podemos injetar abstrações:

public interface EnderecoProvider {
    String getCidade();
    String getEstado();
}

public class Pedido implements EnderecoProvider {
    // implementação...
}

O encapsulamento e a ocultação de informação são os pilares que sustentam a Lei de Demeter. Cada objeto deve esconder sua implementação e expor apenas o necessário.

7. Ferramentas e práticas de revisão de código

Ferramentas como Checkstyle, PMD e SonarQube possuem regras específicas para detectar violações da Lei de Demeter. Exemplo de configuração Checkstyle:

<module name="LawOfDemeter">
    <property name="violateMethods" value="get*"/>
</module>

Check-list para revisão de código:
- [ ] Cada método chama apenas métodos de seus "amigos imediatos"?
- [ ] Existem cadeias com mais de dois níveis de chamadas?
- [ ] O código revela detalhes de implementação de objetos internos?
- [ ] As mudanças em uma classe exigem alterações em várias outras?

Exemplo de feedback construtivo:

"Sugiro criar um método delegado em Pedido para expor a cidade do cliente,
em vez de navegar por pedido.cliente.endereco.cidade. Isso isola o
conhecimento sobre a estrutura interna e facilita futuras mudanças."

A adoção consistente da Lei de Demeter resulta em código mais resiliente, com acoplamento reduzido e manutenção significativamente mais simples. O investimento inicial na refatoração paga-se rapidamente à medida que o sistema evolui.

Referências