Aula 10 · 50–70 minutos
Input Ports: a nomenclatura de Uncle Bob vs. a do projeto
Aula 10 — nove aulas construindo Clean Architecture sem usar o vocabulário formal de Robert C. Martin (Interactor, Input/Output Boundary, Request/Response Model); o vocabulário irmão da Arquitetura Hexagonal (Port); e a peça que este projeto, de propósito, ainda não tem.
Resultado e mapa da aula
Nas últimas nove aulas, o aprovador-pedidos já tem quase todas as peças que os diagramas formais de Clean Architecture desenham — um caso de uso que orquestra, uma interface de saída para persistência, um dado de entrada moldado especificamente para o caso de uso, um dado de saída representando o resultado. Só que nenhuma aula usou o vocabulário que Robert C. Martin (Uncle Bob) usa para nomear essas peças. Esta aula não adiciona nenhum conceito novo — ela é uma tradução, termo a termo, entre o que este projeto já construiu e o vocabulário formal que você vai encontrar em qualquer discussão de mercado sobre Clean Architecture. E, no processo, ela revela uma peça que Uncle Bob desenha, mas que este projeto, de propósito, ainda não tem.
- O problema: saber construir Clean Architecture não é o mesmo que saber conversar sobre ela.
- Fonte primária: o vocabulário de Uncle Bob — Interactor, Input Boundary, Output Boundary, Request Model, Response Model.
- Um vocabulário irmão: a Arquitetura Hexagonal, de Alistair Cockburn, e o termo "Port" — por que o título desta aula usa essa palavra.
- A tabela de tradução completa, linha por linha, ligada ao código já escrito.
- A peça que falta: por que este projeto não tem uma Input Boundary, e quando isso deixaria de ser uma escolha aceitável.
- Uma nuance honesta: o Response Model desta aula está fazendo dupla função como Domain Event, desde a Lição 7 — e por que isso está certo, no tamanho atual do projeto.
O problema: construir não é o mesmo que conversar
Imagine a seguinte situação, real em qualquer processo seletivo técnico: você já sabe escrever exatamente o tipo de código que este curso construiu — um caso de uso que declara sua própria interface de repositório, um tipo de entrada específico, sem estado impossível. Na entrevista, o entrevistador desenha o diagrama clássico de Clean Architecture e pergunta: "onde fica o Interactor no seu projeto? E o Input Boundary?" Se você nunca ouviu esses termos, a conversa trava — não porque você não saiba Clean Architecture, mas porque você não sabe o vocabulário que o mercado usa para falar sobre ela. Isso é um risco real e evitável: o conhecimento técnico já existe nas nove aulas anteriores; falta só a tradução.
Fonte primária: o vocabulário de Uncle Bob
Os cinco termos, com a definição de cada um:
- Interactor: o nome que Uncle Bob dá ao que este curso sempre chamou de "caso de uso" — o objeto que orquestra a regra de negócio para uma operação específica.
- Input Boundary: a interface que o Controller (quem inicia a operação) chama para entrar no Interactor. O Controller depende dessa interface, não do Interactor concreto.
- Output Boundary: a interface que o Interactor chama para sair — tipicamente para persistência ou para apresentar um resultado. O Interactor depende dessa interface, não de uma implementação concreta.
- Request Model: o dado de entrada, moldado especificamente para a operação — sem mais nem menos campos do que ela precisa.
- Response Model: o dado de saída, moldado especificamente para o resultado da operação.
- Controller: quem inicia a operação — recebe um comando de fora (uma requisição HTTP, um comando de CLI, um evento de UI), monta o Request Model, e chama o Interactor através da Input Boundary.
- Presenter: quem recebe o Response Model, do outro lado, e o transforma no formato que a interface externa (uma resposta HTTP, uma tela) realmente precisa exibir.
Vale ser honesto sobre onde este projeto está em relação aos dois últimos termos. main.go foi apresentado, na Lição 4, como o Composition Root — o único lugar que monta as implementações concretas e as injeta nos casos de uso. Um Controller, na definição de Uncle Bob, é uma responsabilidade diferente: montar o Request Model a partir de um comando externo e disparar o Interactor. Hoje, main.go acumula as duas responsabilidades ao mesmo tempo, porque não há distinção real entre "montar o sistema" e "receber um comando" — o próprio código de main.go já é o comando. Isso muda quando a Lição 15 introduzir um handler HTTP: nesse momento, o handler assume o papel de Controller (recebe a requisição, monta o Request Model, chama o Interactor), e main.go volta a ser só o Composition Root, sem se confundir com nenhuma requisição específica. O mesmo vale para Presenter: este projeto ainda não tem um, porque main.go hoje só imprime o Response Model diretamente — a Lição 18 (Presenters e ViewModels) é onde essa peça formalmente aparece, quando existir uma saída de verdade (uma resposta HTTP) que precisa de formatação específica, distinta do dado bruto que o Interactor produz.
Um vocabulário irmão: a Arquitetura Hexagonal e o termo "Port"
O título desta aula usa a palavra "Port", não "Boundary" — e vale explicar por quê, em vez de tratar os dois termos como sinônimos por acidente.
"Input Boundary" (Uncle Bob) e "Input Port" (Cockburn) descrevem a mesma coisa: uma interface que algo de fora usa para entrar na aplicação. "Output Boundary" e "Output Port" também coincidem: uma interface que a aplicação usa para sair. A diferença é de linhagem histórica, não de conceito — Hexagonal Architecture é anterior e é uma das influências reconhecidas de Clean Architecture. No mercado, principalmente na comunidade Go, "port" tende a aparecer com mais frequência do que "boundary" — o artigo How to implement Clean Architecture in Go da Three Dots Labs, já citado na Lição 4, chega a dizer explicitamente "our ports are Hexagonal Architecture's Primary Adapters", misturando os dois vocabulários na prática, exatamente como acontece em conversas reais de equipe. Por isso, o título desta aula usa "Port": é o termo que você tem mais chance de ouvir num code review ou numa entrevista, mesmo sabendo que a origem formal e mais precisa, para quem cita Uncle Bob especificamente, é "Boundary".
A tabela de tradução
| Conceito | Termo de Uncle Bob | Termo de Cockburn (Hexagonal) | Neste projeto |
|---|---|---|---|
| Quem orquestra a regra de negócio | Interactor | (Application Service) | AprovarPedido, RejeitarPedido, ConsultarPedido — struct + método Executar |
| Interface para entrar no caso de uso | Input Boundary | Input Port | Não existe ainda — ver a seção seguinte |
| Dado de entrada, moldado para o caso de uso | Request Model | (Command) | AprovarPedidoInput, RejeitarPedidoInput (Lição 9) |
| Interface para sair do caso de uso | Output Boundary | Output Port | PedidoRepository, PedidoFinder (Lições 4, 8) |
| Dado de saída do caso de uso | Response Model | (Result) | PedidoAprovado, PedidoRejeitado (Lição 7) — ver a nuance abaixo |
| Quem implementa a interface de saída | (Presenter, se for para exibição) | Secondary Adapter | memoria.PedidoRepository (Lição 4) |
main.go (Controller) chama AprovarPedido (Interactor) diretamente pelo tipo concreto — a Input Boundary/Port que Uncle Bob e Cockburn desenham ainda não existe aqui. AprovarPedidoInput é o Request Model; PedidoAprovado é o Response Model, fazendo dupla função como Domain Event. PedidoRepository já é a Output Boundary/Port, satisfeita por memoria.PedidoRepository, o Secondary Adapter.
Quatro das seis linhas da tabela já têm uma peça correspondente, escrita há várias aulas, sem que o nome formal tivesse sido usado ainda. As duas linhas que precisam de mais atenção — a Input Boundary que falta, e o Response Model que faz dupla função — são o resto desta aula.
A peça que falta: por que não existe uma Input Boundary aqui
Releia como main.go chama AprovarPedido, desde a Lição 9:
// main.go
aprovarPedido := usecase.NovoAprovarPedido(repo)
evento, err := aprovarPedido.Executar(usecase.AprovarPedidoInput{ID: "1", Total: 750})
aprovarPedido é *usecase.AprovarPedido — o tipo concreto do Interactor, não uma interface. Isso significa que main.go depende diretamente da implementação, não de uma abstração. Comparando com PedidoRepository: AprovarPedido (o Interactor) depende de uma interface para sair — a Output Boundary — mas main.go não depende de uma interface para entrar em AprovarPedido. Uma Input Boundary formal ficaria assim:
// internal/usecase/aprovar_pedido.go — Input Boundary, se existisse
type AprovarPedidoBoundary interface {
Executar(input AprovarPedidoInput) (*domain.PedidoAprovado, error)
}
var _ AprovarPedidoBoundary = (*AprovarPedido)(nil) // confirma a satisfação, opcionalmente
Isso muda quando um segundo consumidor de AprovarPedido aparecer — por exemplo, um handler HTTP, na Lição 15. Nesse momento, se o handler precisar ser testado sem executar a lógica de negócio real (um teste de unidade do handler, isolado do use case), ele vai precisar depender de uma interface substituível por um fake — exatamente a razão pela qual PedidoRepository já é uma interface desde a Lição 4: AprovarPedido precisa de uma abstração para Salvar porque quer testar sem banco de dados de verdade. A mesma lógica, aplicada ao lado de entrada, é o critério que decide quando criar AprovarPedidoBoundary: não "porque o diagrama de Uncle Bob tem essa caixa", mas "porque agora existe um consumidor real que precisa de um substituto para teste ou para trocar a implementação".
Uma nuance honesta: o Response Model está fazendo dupla função
A tabela marcou PedidoAprovado como Response Model — mas a Lição 7 o apresentou como um Domain Event, citando Martin Fowler, não como "o dado de saída do caso de uso", citando Uncle Bob. As duas descrições não competem — elas descrevem papéis diferentes que o mesmo valor Go está cumprindo ao mesmo tempo:
- Como Response Model,
PedidoAprovadoé o queExecutardevolve para quem chamou — o resultado da operação, paramain.godecidir o que fazer com ele. - Como Domain Event,
PedidoAprovadoé o registro de um fato de negócio que já aconteceu — útil para auditoria, notificação, ou (na Lição 12) persistência transacional.
Neste projeto, um único tipo Go ocupa as duas funções ao mesmo tempo, sem conflito, porque as duas descrevem o mesmo dado sob ângulos diferentes: "o que aconteceu" e "o que a chamada devolveu" coincidem aqui, porque Executar não faz mais nada além de decidir e relatar essa decisão. Isso é uma escolha válida no tamanho atual do projeto — não uma confusão a ser corrigida. Ela deixaria de fazer sentido se um dia o Response Model precisasse carregar informação que não faz parte do fato de negócio (por exemplo, metadados de paginação, ou campos calculados só para exibição) — nesse ponto, a Lição 18 (Presenters e ViewModels) introduz um terceiro tipo, específico para exibição, sem reaproveitar o Domain Event para isso.
Prática guiada: decida com critério, não por reflexo
Raciocínio antes do código
A parte (1) é mecânica — o padrão já foi mostrado. A parte (2) é o teste real: aplicar o critério da aula ("existe um segundo consumidor real que precisa de substituição?") em vez de responder "sim, sempre crie a interface" ou "não, nunca crie a interface" por reflexo. A parte (3) pede um cenário específico deste projeto, não um genérico "quando o sistema crescer" — evitando a mesma armadilha de justificativa vaga que a Lição 9 já cobrou no exercício sobre DTOs.
Uma abordagem plausível, mas incorreta: decidir pelo diagrama, não pelo consumidor
// raciocínio incorreto: "Uncle Bob desenha essa caixa, então ela deveria existir"
type ConsultarPedidoBoundary interface {
Executar(id string) (domain.Pedido, error)
}
Por que falha como justificativa (o código em si não está errado — a razão para escrevê-lo agora está): decidir introduzir uma abstração porque um diagrama de referência a inclui, sem verificar se este projeto, neste momento, tem um segundo consumidor ou uma necessidade real de substituição, é o mesmo erro que a Lição 9 nomeou para DTOs locais — pagar o custo de uma camada a mais (mais um tipo para manter, mais um nível de indireção para seguir ao navegar o código) sem nenhum ganho concreto. main.go continua sendo o único chamador de ConsultarPedido, e ele já testa esse caso de uso perfeitamente bem chamando o tipo concreto direto.
A resposta correta: a interface é válida, mas a introdução ainda não é necessária
ConsultarPedidoBoundary (parte 1) é exatamente o código acima — não há nada de errado na interface em si, só na motivação de criá-la agora. Para a parte (2), a resposta segue o mesmo critério de AprovarPedido: main.go é o único consumidor hoje, então introduzir a interface agora não muda nenhuma capacidade real do sistema — só adiciona indireção. Para a parte (3), um cenário concreto: quando a Lição 15 adicionar um handler HTTP que chama ConsultarPedido.Executar, esse handler vai precisar de testes de unidade que não dependam de uma implementação real de ConsultarPedido (que, por sua vez, dependeria de PedidoFinder, que dependeria de uma implementação de banco de dados) — nesse momento, o handler passa a depender de ConsultarPedidoBoundary, não do tipo concreto, e um fake que implementa só essa interface substitui ConsultarPedido de verdade nos testes do handler. Esse é o gatilho real, específico, não genérico: "um segundo consumidor que precisa testar sem a implementação real por trás".
Verificação: para confirmar que a interface, quando finalmente for criada, está do tamanho certo, rode o mesmo teste da Lição 8 — escreva um fake que implemente só ConsultarPedidoBoundary e confirme que ele não precisa de nenhum método além de Executar para compilar:
// internal/usecase/consultar_pedido_test.go — confirma o tamanho da interface
type consultarPedidoBoundaryFake struct {
pedido domain.Pedido
err error
}
func (f *consultarPedidoBoundaryFake) Executar(id string) (domain.Pedido, error) {
return f.pedido, f.err
}
// Se este arquivo compila, consultarPedidoBoundaryFake satisfaz
// ConsultarPedidoBoundary só com Executar — nenhum outro método de
// PedidoFinder ou PedidoRepository é exigido, confirmando que a
// interface está do tamanho certo para quem a consumiria (um handler
// futuro), não maior do que isso.
var _ ConsultarPedidoBoundary = (*consultarPedidoBoundaryFake)(nil)
Perguntas de entrevista e de mercado
"O que é um Interactor, na terminologia de Clean Architecture, e qual peça deste projeto corresponde a ele?" Defina: o Interactor é o objeto que orquestra a regra de negócio de uma operação específica — neste projeto, cada struct de caso de uso (AprovarPedido, RejeitarPedido, ConsultarPedido) com seu método Executar é um Interactor. Evite confundir Interactor com a entidade de domínio (Pedido) — o Interactor orquestra, a entidade contém a regra em si.
"Qual é a diferença entre 'Input Port' e 'Input Boundary'? São termos concorrentes, ou a mesma coisa?" A resposta forte reconhece a linhagem: "Boundary" é o termo de Uncle Bob (Clean Architecture); "Port" é o termo de Alistair Cockburn (Arquitetura Hexagonal, anterior e citada como influência da Clean Architecture). Descrevem o mesmo conceito — uma interface que separa quem chama de quem implementa — com nomes diferentes por causa de origens diferentes. No mercado, principalmente em Go, "port" é mais comum na conversa do dia a dia, mesmo em times que seguem Uncle Bob à risca.
"Este projeto tem uma Output Boundary, mas não tem uma Input Boundary. Isso é uma violação de Clean Architecture?" Não é uma violação — é uma aplicação proporcional do princípio. PedidoRepository (Output Boundary) existe porque AprovarPedido precisa testar sem banco de dados real, uma necessidade concreta desde a Lição 4. Uma Input Boundary existiria pela mesma razão, do lado de entrada — e essa necessidade ainda não apareceu, porque main.go é o único chamador. Cite o critério: introduzir a abstração quando o segundo consumidor real aparecer, não antes.
"O Response Model deste projeto (PedidoAprovado) também é chamado de Domain Event, numa aula anterior. Isso não é uma inconsistência de nomenclatura?" Não — são dois papéis que o mesmo valor cumpre ao mesmo tempo, não dois nomes concorrentes para a mesma coisa. Como Domain Event, ele representa um fato de negócio que aconteceu; como Response Model, ele é o dado que a chamada de Executar devolve. As duas descrições coincidem porque, no tamanho atual do projeto, o resultado da operação e o fato de negócio são exatamente a mesma informação. Cite quando isso mudaria: se o Response Model precisasse de campos que não fazem parte do fato de negócio em si (dados de exibição, por exemplo), aí sim valeria separar os dois tipos.
"main.go, neste projeto, é o Controller ou o Composition Root, na terminologia de Uncle Bob?" A resposta honesta é "os dois, acumulados, hoje" — e uma boa resposta de entrevista reconhece essa acumulação em vez de forçar uma categoria só. main.go monta as implementações concretas (papel de Composition Root, desde a Lição 4) e também é quem dispara a chamada ao Interactor com dados fixos no código (uma aproximação rudimentar do papel de Controller, que normalmente reagiria a um comando externo real). As duas responsabilidades só se separam de verdade quando existir um comando externo genuíno — uma requisição HTTP, por exemplo — que um Controller de verdade (um handler) processe, enquanto main.go volta a ser exclusivamente o Composition Root.
Recupere da memória: 10 questões
Recupere da memória: 10 questões
Na terminologia de Uncle Bob, o que é um Interactor?
Qual peça deste projeto corresponde ao Request Model de Uncle Bob?
Segundo a citação de Alistair Cockburn nesta aula, o que a palavra 'port' evoca?
Por que o título desta aula usa 'Port' em vez de 'Boundary', já que Boundary é o termo mais preciso de Uncle Bob?
Neste projeto, existe hoje uma Input Boundary para AprovarPedido?
Segundo o critério desta aula, quando faria sentido introduzir AprovarPedidoBoundary?
Por que introduzir AprovarPedidoBoundary agora, sem um segundo consumidor, seria uma escolha questionável?
PedidoAprovado é descrito, nesta trilha, tanto como Domain Event (Lição 7) quanto como Response Model (Lição 10). Isso é uma contradição de nomenclatura?
Segundo esta aula, quando a dupla função de PedidoAprovado (Domain Event e Response Model) deixaria de fazer sentido?
Qual peça deste projeto corresponde à Output Boundary (ou Output Port) de Uncle Bob e Cockburn?
Segundo esta aula, main.go acumula qual par de papéis da terminologia de Uncle Bob, hoje?
Por que este projeto ainda não tem um Presenter, na terminologia de Uncle Bob?
Feche o modelo mental
Este projeto já constrói Clean Architecture há nove aulas — o que faltava era o vocabulário formal para conversar sobre isso. Interactor é o caso de uso (AprovarPedido, RejeitarPedido, ConsultarPedido); Request Model é o Input (Lição 9); Output Boundary (ou Output Port, na linguagem irmã de Cockburn) é PedidoRepository/PedidoFinder (Lições 4 e 8); Response Model é o evento que Executar devolve — fazendo dupla função como Domain Event, uma coincidência válida no tamanho atual do projeto, não um erro de nomenclatura. A única peça que realmente falta é a Input Boundary: main.go chama os casos de uso pelo tipo concreto, uma escolha deliberada e correta enquanto ele for o único consumidor — e uma escolha que muda assim que um segundo consumidor real, como um handler HTTP, precisar de uma abstração para se testar sem a implementação de verdade por trás.
Próxima aula planejada: Repositório real com banco de dados — a implementação em memória se aposenta, e PedidoRepository ganha sua primeira implementação de verdade.
Referências primárias para aprofundar
O vocabulário central desta aula vem de Robert C. Martin, Clean Architecture: A Craftsman's Guide to Software Structure and Design (2017), com o princípio de fluxo confirmado em The Clean Architecture (2012), já citado na Lição 1. O vocabulário irmão de Port vem de Hexagonal Architecture, Alistair Cockburn (2005), já citado na Lição 5. A aplicação prática ao Go, incluindo a mistura natural dos dois vocabulários no dia a dia, é discutida em How to implement Clean Architecture in Go, Three Dots Labs, já citado na Lição 4.
Consulte também o glossário desta trilha.