CTO no Integrar Plus • Publicado em 02 de November de 2026
Toda vez que vejo um método público invocando outro método público dentro do mesmo objeto de serviço ou modelo de domínio, sei que a depuração daquele módulo vai custar caro. O sintoma inicial parece inofensivo. Um desenvolvedor precisa executar uma rotina completa de fechamento financeiro, percebe que já existe um método público para conciliar uma fatura e outro para emitir o recibo, e simplesmente escreve o terceiro chamando os dois primeiros em sequência. Parece reaproveitamento elegante de código. Na prática, você acabou de implodir a barreira de entrada da sua classe.
A discussão técnica recente em torno do princípio de evitar funções públicas chamando outras funções públicas expôs uma divisão clara entre os desenvolvedores. De um lado, defensores de APIs em camadas sustentam que métodos de alto nível devem funcionar como atalhos de conveniência sobre métodos primitivos também públicos. O argumento é que expor os dois níveis documenta o uso esperado da biblioteca e dá flexibilidade a quem precisa compor chamadas customizadas. Do outro lado, engenheiros de compiladores e sistemas notam que quando uma função pública chama outra, os limites operacionais somem. O rastreamento de pilha perde o ponto exato de origem da intenção do usuário, métricas de telemetria disparam duas vezes e o estado intermediário do objeto fica desprotegido.
Eu discordo frontalmente da visão permissiva das camadas abertas no mesmo componente. No desenvolvimento de sistemas web com persistência transacional em PostgreSQL e regras de negócio densas, a tese da conveniência desaba no primeiro incidente de concorrência.
A ideia de que uma classe pode oferecer operações atômicas públicas e operações compostas públicas chamando as primeiras ignora a responsabilidade de manter invariantes. Quando uma requisição atinge um método público, ela atravessa uma fronteira de confiança. Naquele instante exato, o sistema espera validar premissas: autenticar a transação, verificar o estado atual do registro, abrir um bloco de banco de dados e registrar a auditoria.
Se esse método chama outro método público internamente, ocorrem anomalias graves:
Quando você chega ao ponto de usar um dublê de teste para espionar se o método A chamou o método B do mesmo objeto, o design da sua classe faliu. Esse padrão de teste não mede comportamento externo nem protege contra regressões. Ele apenas congela a estrutura interna e impede qualquer refatoração futura.
Existe um argumento comum de que o problema real não é a chamada entre métodos públicos, mas sim o hábito de adulterar a assinatura de um método existente para atender ao novo atalho. Há quem diga que bastaria disciplina para não degradar a API pública.
Essa postura me parece ingênua sobre como bases de código reais envelhecem. Em um time sob pressão de prazos, a disciplina evapora na primeira exceção de regra de negócio. Veja um exemplo clássico em Ruby:
class SubscriptionManager
def activate(account)
validate_billing!(account)
account.update!(status: :active)
notify_user(account)
end
def reactivate(account)
# Atalho perigoso: chamando metodo publico com um parametro improvisado
activate(account)
reset_grace_period(account)
end
end
Em pouco tempo, alguém percebe que reactivate não deveria enviar o e-mail padrão disparado por notify_user dentro de activate. Como activate é público e já é usado em controladores por toda a aplicação, o desenvolvedor raramente quebra a assinatura para extrair a lógica. Em vez disso, ele adiciona um parâmetro opcional: def activate(account, skip_notification: false).
A partir daí, a interface pública virou refém de um detalhe interno. Uma flag booleana foi colocada na porta da frente da classe apenas para atender a uma chamada que veio dos fundos da própria casa. O método primitivo perdeu a coesão semântica e ganhou complexidade acidental.
A saída para esse impasse não exige abstrações complexas nem dezenas de classes intermediárias. A regra é simples: métodos públicos compõem o fluxo de entrada; a execução real pertence a rotinas privadas que expressam intenções atômicas.
Se duas operações públicas compartilham etapas, ambas devem delegar para métodos privados focados, nunca uma depender da outra diretamente.
class SubscriptionManager
def activate(account)
ApplicationRecord.transaction do
validate_billing!(account)
execute_activation(account)
send_activation_notice(account)
end
end
def reactivate(account)
ApplicationRecord.transaction do
validate_billing!(account)
execute_activation(account)
reset_grace_period(account)
send_reactivation_notice(account)
end
end
private
def execute_activation(account)
account.update!(status: :active)
end
def validate_billing!(account)
raise InvalidStateError unless account.payment_method_valid?
end
def send_activation_notice(account)
# despacho de evento assincrono
end
def send_reactivation_notice(account)
# despacho de evento assincrono
end
def reset_grace_period(account)
account.update!(grace_period_ends_at: nil)
end
end
Observe a diferença de postura. Nenhuma das rotinas públicas precisa saber que a outra existe. Se amanhã a reativação exigir um recálculo de cobrança antes da ativação, eu altero o corpo de reactivate sem tocar em uma linha sequer de activate. Os invariantes de cada ponto de entrada permanecem intactos.
Há quem questione se essa abordagem não gera repetição de código. Ela gera uma pequena duplicação de chamadas orquestradoras, o que é muito diferente de duplicar lógica de negócio. A lógica de alteração de estado reside nos métodos privados. A sequência em que esses métodos são convocados é a definição explícita do caso de uso.
Há uma ressalva legítima quando pensamos em bibliotecas de baixo nível, como clientes de rede ou analisadores de protocolo. Nesses cenários, separar uma API em camadas onde módulos utilitários de alto nível encapsulam chamadas a primitivas expostas faz sentido. Mas essa relação deve existir entre objetos ou módulos diferentes, nunca dentro do mesmo escopo fechado.
Se você realmente precisa de um método facilitador que agregue várias operações públicas, construa um coordenador separado. Não faça a classe canônica agir como seu próprio orquestrador informal.
Manter a superfície pública estrita evita a tentação de contornar validações e protege a previsibilidade do seu software em produção. Quando cada método público é uma barreira intransponível e definitiva para o mundo externo, rastrear falhas deixa de ser um exercício de adivinhação.
CTO e arquiteto full stack especializado em Ruby on Rails, Vue.js, sistemas multi-tenant e infraestrutura distribuída.