Pular para o conteúdo

Separated Interface

“Define an interface in a separate package from its implementation.”

O ponto não é apenas “ter uma interface”. É onde ela mora. A interface fica junto do cliente que a consome; a implementação fica em outro pacote, que depende do primeiro. O sentido da dependência de código-fonte se inverte em relação ao fluxo de execução.

Sem essa separação, o módulo de alto nível precisa importar o de baixo nível para conseguir chamá-lo — e passa a depender de banco, framework ou serviço externo em tempo de compilação. Isso torna a política impossível de compilar, testar ou reusar isoladamente, e qualquer troca de tecnologia obriga a recompilar (e frequentemente reescrever) o núcleo.

  • Quando o cliente precisa ser independente do fornecedor em tempo de compilação.
  • Quando há múltiplas implementações previstas, ou uma delas é volátil (banco, gateway externo, mensageria).
  • Quando se deseja testar a política sem a infraestrutura.
  • Quando o cliente e o implementador pertencem a times ou módulos de build diferentes.

Fowler discute duas opções, e a escolha muda o significado do padrão:

  • No pacote do cliente — a interface é propriedade de quem a usa; ela expressa o que o cliente precisa. É esta variante que produz a inversão de dependência.
  • Num pacote terceiro, separado de ambos — útil quando há vários clientes ou quando o contrato é compartilhado, mas a inversão é mais fraca: ninguém “possui” o contrato em nome da política.

Interface no pacote do domínio, no vocabulário do domínio:

package com.exemplo.domain.pedido;
public interface Pedidos {
void registrar(Pedido pedido);
Optional<Pedido> porId(PedidoId id);
List<Pedido> aguardandoPagamento();
}

Implementação em outro pacote, importando o domínio:

package com.exemplo.infrastructure.persistence;
import com.exemplo.domain.pedido.Pedido;
import com.exemplo.domain.pedido.PedidoId;
import com.exemplo.domain.pedido.Pedidos;
@Repository
public class JdbcPedidos implements Pedidos {
private final JdbcTemplate jdbcTemplate;
public JdbcPedidos(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void registrar(Pedido pedido) {
jdbcTemplate.update("INSERT INTO pedido (id, total) VALUES (?, ?)",
pedido.id().valor(), pedido.total());
}
// demais métodos
}

O pacote domain não contém nenhum import de infrastructure. O teste é mecânico: apague infrastructure e o domínio ainda compila. O caminho inverso não é verdadeiro — e é exatamente essa assimetria que caracteriza o padrão.

graph LR
    subgraph domain
        Svc[ProcessadorPedido]
        Port[[interface Pedidos]]
    end
    subgraph infrastructure
        Impl[JdbcPedidos]
    end

    Svc -->|usa| Port
    Impl -.implementa / importa.-> Port
    Impl --> DB[(PostgreSQL)]

A seta de execução vai de ProcessadorPedido até o banco; a seta de compilação vai de JdbcPedidos para domain. Sentidos opostos.

Se o cliente não pode importar o pacote da implementação, alguém de fora precisa fornecê-la. Daí a associação frequente com injeção de dependência: um Plugin (outro padrão de Fowler), um container como o Spring, ou uma fábrica configurada no composition root da aplicação — sempre num módulo que pode depender dos dois lados.

Vale a distinção: Separated Interface resolve onde a interface mora; injeção de dependência resolve como a implementação é entregue. São complementares, e usar injeção sem separar os pacotes não produz inversão.

  • Interface no pacote da implementação — o erro central: o cliente continua importando o pacote do detalhe, e não há inversão.
  • Interface extraída da classe concreta — mesmos métodos, mesmo vocabulário de tecnologia. A dependência sintática inverte, mas a semântica não: qualquer mudança no detalhe ainda força mudança no contrato.
  • Uma interface para cada classe — aplicar o padrão a colaboradores internos e estáveis gera cerimônia sem retorno. Separe nas fronteiras que variam.
  • Contrato genérico demais — interfaces como Repositorio<T> com CRUD genérico não expressam necessidade do cliente; voltam a ser vocabulário de persistência.

1. O que distingue Separated Interface de simplesmente “programar para uma interface”?

Resposta

A localização. O padrão exige que a interface fique num pacote separado da implementação — idealmente no pacote do cliente. Ter uma interface que mora junto da implementação não muda a direção da dependência.

2. Qual teste mecânico confirma que o padrão foi aplicado?

Resposta

Apagar (ou remover do classpath) o módulo da implementação e verificar se o módulo do cliente ainda compila. Se compila, a dependência aponta do detalhe para a política.

3. Por que o padrão costuma vir acompanhado de injeção de dependência?

Resposta

Porque o cliente não pode importar a implementação — logo, não pode instanciá-la. Alguém externo (container, fábrica, composition root) precisa fornecer a instância concreta em tempo de execução.

4. Uma interface com vocabulário de persistência colocada no pacote do domínio satisfaz o padrão?

Resposta

Satisfaz a letra — a dependência de compilação está invertida. Mas o contrato continua descrevendo o detalhe, então mudanças de tecnologia ainda atravessam a fronteira. A separação de pacotes é necessária, não suficiente.