O Contrato de Biblioteca
Um aplicativo é julgado por seus usuários; uma biblioteca é julgada por programadores, e em um momento específico — quando eles querem algo que o autor não imaginou. Uma boa biblioteca não é aquela que é conveniente quando usada como pretendido. É aquela que não precisa ser descartada quando usada de outra forma.
Cada regra abaixo decorre de uma assimetria. Dentro da biblioteca você pode mudar qualquer coisa, para sempre. Fora dela, tudo o que um chamador pode observar está congelado na primeira vez que alguém depende disso — e com chamadores suficientes, alguém depende de tudo.
O que congela, e quando
A lei de Hyrum é a condição operacional, não uma curiosidade: dados usuários suficientes, todo comportamento observável se torna estrutural — redação de exceção, ordem de enumeração, saída de log, tempo, até mesmo o comportamento que você documentou como indefinido. "Nunca estava no contrato" não desfaz o erro do chamador que lançou uma versão contra ele.
Portanto, a linha de congelamento reside na observabilidade, não na palavra-chave public,
e três consequências se seguem:
- A superfície começa pequena. Um símbolo, uma vez público, é uma promessa que você pode ampliar, mas nunca retirar silenciosamente. "Público por enquanto" é dívida contraída sem benefício; exponha sob demanda.
- Formatos sobrevivem ao código. Um formato de arquivo, um protocolo de rede, um esquema persistido sobrevive a cada reescrita da biblioteca ao seu redor. Serializar a struct interna como está solda a camada congelada à editável.
- Um módulo interno do aplicativo cruza esta linha no segundo consumidor, não no empacotamento. No momento em que existem dois chamadores, sua API tem a mesma economia de congelamento que um pacote publicado — apenas o raio da explosão é menor.
O Versionamento Semântico é a ortografia pública da mesma linha, e começa recusando-se a funcionar sem uma: "Software que usa Versionamento Semântico DEVE declarar uma API pública" (SemVer 2.0.0, cláusula 1). Uma vez declarada, os incrementos são decididos em vez de sentidos — uma alteração incompatível com versões anteriores nessa API DEVE aumentar MAJOR (cláusula 8), nova funcionalidade compatível com versões anteriores DEVE aumentar MINOR (cláusula 7), uma correção compatível com versões anteriores DEVE aumentar PATCH (cláusula 6). A lei de Hyrum é o que torna a declaração difícil: a especificação congela o que você declarou, e os chamadores congelam o que observaram.
A especificação também tem um nome para o módulo que ainda não cruzou a linha. "Versão principal zero (0.y.z) é para desenvolvimento inicial. QUALQUER COISA PODE mudar a qualquer momento" (cláusula 4). Esse é o rótulo honesto para um módulo com um consumidor, e o dia em que ganha um segundo é o dia em que 1.0.0 é devido, não o dia em que é empacotado.
Não assuma controle
Separe mecanismo de política. Executar uma requisição HTTP é mecanismo; repeti-la três vezes com backoff exponencial é política, e política embutida no núcleo restringe a biblioteca aos chamadores que a compartilham. Forneça padrões convenientes para logging, threading, repetições, timeouts, cache, serialização — e torne cada um deles substituível.
O papel do aplicativo é singular, e não é seu. Sair do processo, escrever em stdout, capturar SIGINT, instalar manipuladores de exceção de nível superior, redimensionar o pool de threads: exatamente um componente por processo pode fazer isso, e uma biblioteca que o faz luta contra o componente com direito a fazê-lo. Ler as entradas do processo pertence ao mesmo papel: variáveis de ambiente, arquivos de configuração e argumentos de linha de comando são lidos na raiz do aplicativo, que entrega cada valor à biblioteca como um parâmetro ou um objeto de opções. O chamador então escolhe de onde um valor vem, e um teste o passa em vez de configurar a máquina.
Magia — autodetecção, autoconversão, autoconectividade, autolimpeza — é admissível apenas enquanto o chamador puder reconstruir por que fez o que fez. Conveniência que o usuário não pode depurar é controle assumido, não trabalho economizado.
Não adicione custos ocultos
Quando as coisas acontecem pertence ao chamador. Um import que inicia uma thread, um construtor que abre uma conexão, um inicializador estático que lê variáveis de ambiente — cada um move trabalho real para um momento que o chamador nunca escolheu e não pode mover de volta.
Uma dependência é parte da sua API. Seu tempo de execução, sua cadência de atualização, sua estabilidade ABI, sua licença, seus próprios efeitos colaterais de inicialização são transferidos para cada chamador; você está escolhendo em nome deles.
As características de desempenho são contrato. Transformar uma busca O(1) em O(n) — ou duplicar alocações e cópias — é uma mudança significativa usando uma assinatura compatível, e os chamadores a descobrem em produção.
Onde a entrada chega mais rápido do que sai — streams, eventos, logs, filas — um buffer interno ilimitado é um vazamento de memória adiado. Contrapressão faz parte do design, não um detalhe operacional.
O padrão para a própria abstração é custo zero, em ambas as cláusulas de Stroustrup. O que o chamador não usa, não paga: a camada de conveniência não deve taxar a primitiva com suas alocações, seu dispatch ou sua configuração. E o que eles usam, não poderiam razoavelmente escrever melhor manualmente: uma abstração superada por código feito à mão é contornada, e uma biblioteca contornada é descartada com um número de versão.
Não mantenha estado oculto
Singletons, caches estáticos, loops de eventos globais, configuração em todo o processo: cada um funciona enquanto a sua é a única biblioteca no processo, e colide quando uma segunda biblioteca, um host de plugin, ou um conjunto de testes paralelo a compartilha. O estado vive em uma instância que o chamador cria, possui, e pode criar duas vezes.
Não esconda falhas
A forma da API declara o que pode acontecer e quem é responsável. Erros, cancelamento, timeout, sucesso parcial, ausência — modelados nos tipos, não roteados através de "isso nunca acontece normalmente" e uma exceção. A mesma forma carrega as respostas de propriedade que um comentário não pode impor: quem fecha o stream, se o buffer pode ser reutilizado, em qual thread o callback chega.
Mantenha a causa.
Colapsar falha em false ou null remove exatamente a informação que a camada acima precisava para decidir;
logá-la você mesmo e continuar perde a mesma informação e assume a política de logging do chamador.
A qualidade se separa no caminho anormal. No caminho feliz, as bibliotecas são intercambiáveis; timeout, reconexão, desconexão no meio do stream, leituras parciais, dados corrompidos e repetição são onde as diferenças vivem, então projete para sua composição primeiro.
O cancelamento se adapta mal — um token de cancelamento percorre cada assinatura que toca. Operações de I/O e de longa duração o aceitam desde a primeira versão.
Não bloqueie a fuga
Construa dois andares.
Uma API de alto nível que atende ao caso comum em uma linha,
apoiada em primitivas públicas — ReadAsync() sobre ReadAsync(Memory<byte>) — para que o chamador cujo requisito cai fora do andar superior desça em vez de sair.
Uma biblioteca com apenas o andar superior é descartada no primeiro requisito que não previu.
A observabilidade é injetada, não possuída: loggers, métricas, traces se conectam à pilha do chamador. Um formato de diagnóstico que apenas sua biblioteca lê é telemetria feita refém.
Um imposto, duas moedas
Esta habilidade e a Arquitetura Limpa são a mesma recusa em duas moedas. A abstração de custo zero recusa o imposto sobre o desempenho em tempo de execução; a Regra da Dependência recusa o imposto sobre a capacidade de mudança. Em ambas, uma abstração é julgada pelo que faz o lado dependente abrir mão.
| Dever aqui | Seu nome arquitetural |
|---|---|
| Mecanismo separado da política | Caso de uso mantido separado do framework e driver |
| Padrões substituíveis | Inversão de dependência — o lado estável escolhe a implementação |
| Primitivas sob o andar de conveniência | Detalhes externos trocáveis |
| Sem estado global | Dependências explícitas, para que um limite possa ser traçado |
| Uma dependência é parte da sua API | A Regra da Dependência |
A forma saudável do chamador coloca sua biblioteca na base: um detalhe substituível abaixo de sua política e domínio. A forma invertida — o aplicativo vivendo dentro de um framework, arrastando seu ciclo de vida, seus tipos, sua DI, seu modelo de erro, seu modelo assíncrono — é a biblioteca agindo como o aplicativo, escrita em escala arquitetural.
A forma geral: uma boa arquitetura minimiza não o número de dependências, mas a liberdade que cada dependência tira.
A sentença
Uma abstração em uma biblioteca existe para ampliar as escolhas do chamador. No momento em que ela tira desempenho, controle ou informação em troca, ela parou de abstrair e começou a decidir.
Quando não aplicar
Um módulo com um consumidor, um repositório, uma equipe não cruzou a linha de congelamento — está em 0.y.z nos termos do SemVer, quer carregue ou não um número — e construir todas as costuras de política substituível para ele é especulação com uma conta de manutenção. Mantenha as disciplinas que não custam nada desde o primeiro dia — sem estado global, falhas que mantêm sua causa, nenhum trabalho antes de o chamador pedir — e adicione as costuras quando o segundo consumidor chegar, que é o momento em que elas deixam de ser especulativas.
Fontes
- Versionamento Semântico 2.0.0, semver.org: cláusula 1 (API pública DEVE ser declarada), cláusulas 6–8 (incrementos PATCH, MINOR, MAJOR), cláusula 4 (0.y.z), e a entrada de FAQ sobre deprecação (documente-a, depois emita um lançamento menor com a deprecação em vigor). Redação verificada em relação à página.
- A lei de Hyrum é uma observação, não um padrão, e é citada como tal.