Quando uma biblioteca 'básica' é uma má ideia?


8

Ao desenvolver software, geralmente tenho uma biblioteca 'central' centralizada contendo código útil que pode ser compartilhado e referenciado por diferentes projetos.

Exemplos:

  • um conjunto de funções para manipular strings
  • expressões regulares comumente usadas
  • código de implantação comum

No entanto, alguns dos meus colegas parecem estar se afastando dessa abordagem. Eles têm preocupações como a sobrecarga de manutenção do código de reteste usado por muitos projetos depois que um bug é corrigido. Agora estou reconsiderando quando deveria estar fazendo isso.

Quais são os problemas que tornam uma má ideia usar uma biblioteca 'principal'?


Ter uma biblioteca principal é uma boa idéia quando o código é geralmente reutilizado, mas precisa ser testado religiosamente, incluindo testes de unidade e outras tecnologias espaciais.
Job

É uma boa ideia quando estabilizada e não muda.
Martin York

A preocupação de testar novamente é muito válida. Deseja descobrir que você quebrou um projeto de manutenção há 6 meses?

Não consigo imaginar reescrever todo o meu código de utilitário cada vez que eu precisava.

Respostas:


12

As bibliotecas principais são ruins quando começam a sofrer com o surgimento de recursos e muito ruins quando não são bem mantidas.

Você pode achar este artigo interessante para um ponto de vista estendido (com o qual concordo plenamente):

http://www.yosefk.com/blog/redundancy-vs-dependencies-which-is-worse.html


Don Knuth: "Para mim, 'código reeditável' é muito, muito melhor do que uma caixa preta ou um kit de ferramentas intocável ... você nunca vai me convencer de que o código reutilizável não é principalmente uma ameaça".


3

Usar a idéia de que uma biblioteca principal é ruim quando vários projetos dependem dela é como dizer que você não deve usar o jQuery para a Web, libxml nos aplicativos * * nix ou qualquer outra estrutura ou biblioteca. Observe todo o ecossistema do desenvolvimento moderno (DRY, OOP, etc) e cada aplicativo é criado a partir de um conjunto de bibliotecas e estruturas.

O que pode ser ruim é se você não possui nenhum tipo de teste de unidade, não faz teste de regressão e não usa nenhum tipo de API / ABI com sua biblioteca. Se todos os seus aplicativos tiverem testes adequados, sua biblioteca terá testes adequados e, se você interromper as chamadas de função, atualize o número da versão da API adequadamente.

Para uma cobertura completa, o que você provavelmente desejaria é quando forem feitas alterações na Biblioteca, você pode executar um conjunto de testes que verificarão se a API não foi quebrada e se a execução de todo o código está livre de erros. Em seguida, você pode inserir a atualização mais recente da biblioteca em seu aplicativo e executar o mesmo conjunto de testes. Se você atualizar a API, ela deverá ser documentada para que você saiba o que precisa fazer no seu aplicativo para atualizá-lo. De qualquer forma, quando você executa os testes para o seu aplicativo, pode estar tão confiante quanto nos testes que nada quebrou.

Ao usar jquery, mootools, qualquer biblioteca ou estrutura javascript, você não pode usar cegamente a nova versão; infelizmente, nem mesmo com um lançamento menor do 1.6.z às vezes.


3

Eles têm preocupações como a sobrecarga de manutenção do código de reteste usado por muitos projetos depois que um bug é corrigido.

Se você possui um conjunto abrangente de testes de unidade para a biblioteca principal; isso não é um problema. Nenhum código será verificado a menos que todos os testes passem. Se você apresentar um defeito, escreva um teste com falha para reproduzir e corrigir o defeito; então você sempre testará esse erro também. Para sempre.

Além disso, a funcionalidade que você descreve é ​​muito fácil de criar testes de unidade.

Como um problema secundário, convém ter mais de uma biblioteca principal para não precisar incluir o código RegEx, a menos que queira.


2

Vou oferecer uma visão ligeiramente diferente sobre isso. Uma biblioteca principal, em muitos casos, é uma excelente ideia!

Se você tiver dois projetos separados, eles deverão estar em dois repositórios de código separados. Agora eles dependem da funcionalidade comum. Vamos considerar, por exemplo, aplicativos de processamento de pacotes. A funcionalidade comum pode incluir:

  • Alocadores de memória
  • Protocolo de Resolução de Endereço
  • Árvore AVL
  • Código de serialização para protocolos binários
  • Matriz dinâmica
  • Lista de hash no estilo do kernel Linux com cabeçalho vinculado individualmente e nós do meio duplamente vinculados
  • Tabela de hash
  • Código de processamento do cabeçalho TCP / IP
  • Lista vinculada regular com cabeça duplamente vinculada e nós médios duplamente vinculados
  • Biblioteca de registro
  • Diversos (confie em mim, você precisa disso para coisas pequenas e triviais ou o número de módulos diferentes será tão grande quanto 100!)
  • Biblioteca de captura de pacotes
  • Biblioteca de interface de E / S de pacotes
  • Estrutura de dados por pacote
  • Fila de bloqueio para comunicação entre threads
  • Geradores de números aleatórios
  • Árvore vermelho-preta
  • Algum tipo de implementação de timer

Agora, aplicativos de processamento de pacotes diferentes podem precisar de um subconjunto diferente deles. Você deve implementar uma biblioteca principal com um repositório de código-fonte ou deve ter 18 repositórios diferentes para cada um desses módulos? Lembre-se de que esses módulos podem ter interdependências; portanto, a maioria desses módulos pode depender, por exemplo, do módulo diverso.

Afirmo que ter uma biblioteca principal é a melhor abordagem. Reduz a sobrecarga de muitos repositórios de código-fonte. Isso reduz o inferno das dependências: uma versão específica dos alocadores de memória pode precisar de uma versão específica do módulo diverso. E se você quiser a versão 1.7 do alocador de memória, dependendo da versão 2.5 e da árvore 1.2 do AVL, dependendo da versão 2.6? Talvez você não consiga vincular diversos 2.5 e 2.6 ao mesmo tempo ao seu programa.

Então, vá em frente e implemente a seguinte estrutura:

  • Repositório da biblioteca principal
  • Repositório do projeto nº 1
  • Repositório do projeto nº 2
  • ...
  • Repositório do projeto #N

Eu vi que mudar para esse tipo de estrutura a partir da estrutura:

  • Repositório do projeto nº 1
  • Repositório do projeto nº 2
  • ...
  • Repositório do projeto #N

Levou à redução da manutenção e ao aumento do compartilhamento de código por meio de mecanismos que não sejam de copipaste.

Também vi projetos usando a seguinte estrutura:

  • Repositório de alocadores de memória
  • Repositório do protocolo de resolução de endereços
  • Repositório da árvore AVL
  • Código de serialização para repositório de protocolos binários
  • Repositório de matriz dinâmica
  • Lista de hash no estilo do kernel Linux com cabeçalho vinculado individualmente e repositório de nós intermediários duplamente vinculado
  • Repositório da tabela de hash
  • Repositório de código de processamento do cabeçalho TCP / IP
  • Lista vinculada regular com cabeçalho duplamente vinculado e repositório de nós médios duplamente vinculado
  • Repositório da biblioteca de log
  • Repositório diverso (confie em mim, você precisa disso para coisas pequenas e triviais ou o número de módulos diferentes será tão grande quanto 100!)
  • Repositório da biblioteca de captura de pacotes
  • Repositório da biblioteca de interface de E / S de pacotes
  • Repositório da estrutura de dados em pacotes
  • Fila de bloqueio para o repositório de comunicação entre encadeamentos
  • Repositório de geradores de números aleatórios
  • Repositório de árvore vermelho-preto
  • Algum tipo de repositório de implementação de timer
  • Repositório do projeto nº 1
  • Repositório do projeto nº 2
  • ...
  • Repositório do projeto #N

... e a dependência do inferno e a proliferação de números de repositórios têm sido problemas genuínos.

Agora, você deve usar uma biblioteca de código aberto existente em vez de escrever sua própria? Você precisa considerar:

  • Problemas de licença. Às vezes, o mero requisito de dar crédito ao autor na documentação fornecida pode ser demais, pois 20 bibliotecas geralmente terão 20 autores distintos.
  • Suporte de versão diferente do sistema operacional
  • Dependências da biblioteca específica
  • Tamanho da biblioteca específica: é muito grande para a funcionalidade fornecida? Ele fornece muitos recursos?
  • É possível vincular estática? A vinculação dinâmica é desejável?
  • A interface da biblioteca é o que você deseja? Observe que, em alguns casos, escrever um wrapper para fornecer a interface desejada pode ser mais fácil do que reescrever todo o componente.
  • ... e muitas outras coisas que não mencionei nesta lista

Eu costumo usar a regra de que tudo abaixo de 1000 linhas de código que não exija algo além do conhecimento do programador deve ser implementado por conta própria. Nota: as 1000 linhas incluem testes de unidade. Portanto, certamente não defenderei a criação de 1000 linhas de código por conta própria, se exigir 10.000 linhas adicionais para testes de unidade. Para meus programas de processamento de pacotes, isso significa que os únicos componentes externos que usei são:

  • Tudo fornecido por uma distribuição Linux padrão, porque são tantas linhas de código que não faz sentido reimplementar o Linux. Partes da reimplementação do Linux também estariam além do meu nível de conhecimento.
  • Bison / flex porque a análise LALR está além do meu nível de conhecimento e mais de 1000 linhas de código. Eu certamente poderia escrever um analisador de descida recursivo por conta própria, mas o Bison / flex é tão útil que eu os vejo como úteis.
  • Netmap, porque tem mais de 1000 linhas e está além do meu nível de especialização
  • Ignorar a implementação do cronômetro baseada em lista do DPDK, porque está além do meu nível de conhecimento, embora seja menor que 1000 linhas de código (embora eu tenha implementações alternativas de cronômetro que não usem listas de ignorados)

Algumas coisas que eu implementei por conta própria porque são simples incluem até coisas como:

  • MurMurHash
  • SipHash
  • Mersenne Twister

... porque implementações personalizadas deles podem permitir inlining pesado, levando a um desempenho aprimorado.

Eu não faço criptografia; se o fizesse, adicionaria algum tipo de biblioteca de criptografia na lista, pois a escrita de algoritmos de criptografia por conta própria pode ser suscetível a ataques de tempo de cache, mesmo que você possa, com um teste de unidade completo, mostrar que são compatíveis com os algoritmos oficiais.


1

Uma biblioteca principal pode ser ruim quando vários projetos dependem dela, não apenas é necessário testar quaisquer alterações em seu núcleo, mas também é necessário testar a regressão para cada projeto dependente. Em segundo lugar, suas principais APIs nunca podem ser alteradas porque você precisará refatorar todos os projetos dependentes. Quanto mais projetos usarem sua biblioteca, mais profunda será a armadilha.

Outro problema é a tendência de começar a jogar tudo "comum" em sua biblioteca principal, inchando e dificultando a inserção de pequenos pedaços. Vou apenas dizer que uma vez ouvi falar de um lugar que ficou com medo de tocar em qualquer uma das suas numerosas bibliotecas principais, a sobrecarga dos testes de regressão ao controle de qualidade foi tão grande.

Em vez disso, talvez você possa criar um recurso de trecho de código para permitir que as equipes de projeto pesquisem e obtenham o código necessário e se afastem de quaisquer problemas de manutenção ou regressão? De qualquer maneira, é o que eu faço em casa.


4
É muito mais difícil corrigir um bug nos trechos de código que foram copiados e colados em vários lugares, não é?
Alex Angas

Uma citação de Donald Knuth: "Eu também devo confessar um forte viés contra a moda de códigos reutilizáveis. Para mim," código reeditável "é muito, muito melhor do que uma caixa preta ou um kit de ferramentas intocável. Eu poderia continuar e continuar Se você está totalmente convencido de que o código reutilizável é maravilhoso, provavelmente não será capaz de influenciá-lo, mas você nunca vai me convencer de que o código reutilizável não é uma ameaça. "
Patrick Hughes

@AlexAngas: Isso é verdade, mas pode haver casos em que uma biblioteca está com bugs, mas funciona corretamente apenas porque alguma outra biblioteca possui erros sutis que compensam os erros no primeiro. Embora ambos os conjuntos de bugs devam ser corrigidos quando for prático, ter uma cópia do código-fonte da segunda biblioteca como parte do projeto; a primeira significaria que uma correção de bug aplicada a esse código seria uma mudança reconhecível no projeto, o que pode ser temporariamente revertida se quebrar as coisas (permitindo que seja identificada como a causa da quebra).
Supercat 18/11

@AlexAngas: É claro que identificar a correção da segunda rotina como a causa da quebra não significa que o remédio não é corrigir a segunda, mas sim apontar para o fato de que algum código depende erroneamente do comportamento incorreto dessa rotina ; essa descoberta será a chave para resolver com eficiência os problemas reais. Por outro lado, se tudo o que se sabe é que o código que costumava trabalhar espontaneamente parou de funcionar, será muito difícil rastrear o que fazer a respeito.
Supercat 18/11

1

Um ponto ainda não mencionado é que qualquer código terá dependências de algo , mesmo que seja literalmente a única coisa executando na ROM de um microcontrolador incorporado; se o fabricante do controlador alterar algum comportamento em que o código se baseia, o código precisará ser modificado para funcionar com chips fabricados após a alteração, ou os fabricantes do dispositivo que usa o código precisarão, de alguma forma, adquirir chips que não incorporar a alteração - possivelmente pagando um prêmio por eles.

O uso de uma biblioteca para executar várias funções de hardware pode significar que o código agora depende de uma biblioteca, embora não tivesse sido anteriormente, mas também pode eliminar as dependências entre o código e o hardware. Por exemplo, um fabricante de chips pode prometer fornecer uma biblioteca para todos os chips presentes e futuros que sempre executam determinadas funções de E / S de uma certa maneira. O código que usa essa biblioteca para executar essas funções de E / S se tornaria dependente do fabricante para fornecer versões apropriadas dessa biblioteca, mas não seria mais dependente do fabricante para usar a mesma implementação de hardware dessas funções.

Infelizmente, muitas vezes é difícil saber qual é a abordagem correta para o código à prova de futuro. Vi casos em que um fornecedor de chips mudou a maneira como uma biblioteca funcionava (para acomodar novos chips), mesmo quando estava sendo usado para acessar um chip que havia mudado. Também vi casos em que um fabricante de chips alterava a maneira como seu hardware funcionava, mas as bibliotecas fornecidas eram ajustadas adequadamente, de modo que o código que usava as rotinas de uma biblioteca continuava funcionando sem alterações, enquanto o código que acessava o hardware diretamente precisava ser ajustado.

Situações semelhantes existem com aplicativos do Windows. Às vezes, a Microsoft gosta de mudar a maneira como os aplicativos são obrigados a fazer as coisas; o código que usa determinadas bibliotecas para tais coisas pode ser atualizado simplesmente atualizando a biblioteca, enquanto o código que não usa bibliotecas que são atualizadas para elas deve ser atualizado manualmente.


1

Eu queria abordar isso com uma abordagem um pouco diferente, apesar de adorar Denis de Bernardyresponder e vincular artigos sobre minimizar dependências e minimizar redundâncias (eles refletem muito meus próprios pensamentos sobre esse assunto, onde acredito que a reutilização de código é um ato de equilíbrio).

O maior problema que tenho com uma corebiblioteca é este:

Quando está completo? Quando alcançará um ponto de estabilidade em que fará tudo o que precisa e efetivamente será "feito"?

E acho que é muito provável que a resposta possa ser " nunca ". As pessoas sempre podem ser tentadas a adicionar a ela, uma vez que modela uma idéia tão nebulosa, especialmente se essa biblioteca está apenas evoluindo durante o desenvolvimento do software, em vez de ter objetivos antecipados. E talvez adicionar à biblioteca não seja a pior coisa do mundo, pois não quebrará as dependências existentes da biblioteca, mas, dados objetivos tão nebulosos, a biblioteca pode se tornar cada vez mais eclética e feia, fornecendo funcionalidades díspares das quais alguém está interessado o uso da biblioteca pode encontrar apenas uma pequena parte aplicável às suas necessidades.

As dependências na sua base de código devem idealmente fluir para pacotes muito estáveis. Um corepacote pode facilmente se tornar muito instável, enquanto grandes partes da sua base de código têm dependências fluindo em direção a ele.

Então, acho que vale a pena dividir a biblioteca em bibliotecas mais uniformes, dedicadas a fazer algo mais específico do que apenas "biblioteca principal de tudo o que as pessoas possam precisar com frequência", para que ela cresça em uma direção mais uniforme e com melhor coordenação entre os colegas de equipe. sobre exatamente o que deveria e, o que é mais importante, não deveria fazer, e potencialmente alcançar um ponto de estabilidade onde é bem testado e você não sente que há mais alguma coisa que precisa ser adicionada a ele para que seja relativamente " completo "e estável (como inalterável).


0

Escrever bibliotecas para coisas básicas como strings e listas vinculadas é bastante tolo neste milênio. Use uma linguagem de programação incluída nas baterias que possua as principais funcionalidades.

Se você gosta de escrever as principais bibliotecas de suporte em tempo de execução apenas por diversão, crie uma nova linguagem de programação. Se você faz isso em um aplicativo, basicamente está desenvolvendo um idioma para o lado.

Além disso, alguém já não escreveu N bibliotecas principais diferentes no idioma que você está usando? Pesquisando estruturas existentes e escolhendo a mais adequada pode ser um uso melhor do tempo do que fazê-lo do zero.


No meu campo, o processamento de pacotes de alto desempenho, certamente usando uma linguagem de programação incluída em baterias não é uma opção. C é a escolha óbvia. E não, as N diferentes bibliotecas principais disponíveis para, por exemplo, tabelas de hash, são piores que a implementação do kernel do Linux. A implementação do kernel Linux, sendo GPL, exige que você implemente manualmente uma implementação semelhante sem consultar o código-fonte do kernel Linux, mas sabendo que a tabela de hash avançada apresenta os recursos da implementação do kernel Linux. Isso pode variar no campo, no entanto.
21717 juhist
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.