Evitar que o código reprovado seja compilado após atingir um prazo final [fechado]

67

Na minha equipe, limpamos muitas coisas antigas em um grande projeto monolítico (classes inteiras, métodos, etc.).

Durante as tarefas de limpeza, fiquei me perguntando se existe algum tipo de anotação ou biblioteca mais sofisticada do que a usual @Deprecated . Esse @FancyDeprecated deve impedir que a criação do projeto seja bem-sucedida se você não tiver limpo o código não utilizado antigo após uma determinada data.

Eu tenho pesquisado na Internet e não encontrei nada que tenha os recursos descritos abaixo:

  • deve ser uma anotação ou algo semelhante para colocar no código que você deseja excluir antes de uma data específica
  • antes dessa data, o código será compilado e tudo funcionará normalmente
  • após essa data, o código não será compilado e você receberá uma mensagem avisando sobre o problema

Acho que estou procurando por um unicórnio ... Existe alguma tecnologia similar para qualquer linguagem de programa?

Como um plano B Estou pensando na possibilidade de fazer a mágica com alguns testes unitários do código que deve ser removido e começar a falhar no "prazo final". O que você pensa sobre isso? Alguma idéia melhor?

    
por Arcones 08.03.2018 / 09:31
fonte

12 respostas

61

Eu não acho que isso seria um recurso útil quando realmente proíbe a compilação. Quando em 01/06/2018 grandes partes do código não compilam o compilado no dia anterior, sua equipe removerá rapidamente essa anotação novamente, código limpo ou não.

No entanto, você pode adicionar algumas anotações personalizadas ao código como

@Deprecated_after_2018_07_31

e crie uma pequena ferramenta para procurar essas anotações. (Um simples liner no grep fará isso, se você não quiser utilizar a reflexão). Em outros idiomas além do Java, um comentário padronizado adequado para "grepping" ou uma definição de pré-processador pode ser usada.

Em seguida, execute essa ferramenta logo antes ou depois da data específica e, se ela ainda encontrar essa anotação, lembre a equipe de limpar essas partes do código com urgência.

    
por 08.03.2018 / 10:31
fonte
284

Isso constituiria um recurso conhecido como bomba-relógio . NÃO CRIE BOMBAS DE TEMPO.

Código, não importa o quão bem você estrutura e documente, irá se transformar em uma caixa negra quase mítica e mal entendida se ela viver além de uma certa idade. A última coisa que alguém no futuro precisa é ainda outro modo de falha estranho que os surpreenda totalmente, no pior momento possível, e sem um remédio óbvio. Não há absolutamente nenhuma desculpa para produzir intencionalmente tal problema.

Veja da seguinte forma: se você é organizado e consciente da sua base de código o suficiente para se importar com a obsolescência e segui-la, então você não precisa de um mecanismo dentro do código para lembrá-lo. Se você não for, as chances são de que você também não esteja atualizado sobre outros aspectos da base de código e provavelmente não conseguirá responder ao alarme de maneira oportuna e correta. Em outras palavras, as bombas-relógio não servem a nenhum bom propósito para ninguém. Apenas diga não!

    
por 08.03.2018 / 10:21
fonte
69

Em C # você usaria o ObsoleteAttribute da seguinte maneira:

  • Na versão 1, você envia o recurso. Um método, classe, qualquer coisa.
  • Na versão 2, você envia um recurso melhor para substituir o recurso original. Você coloca um atributo Obsoleto no recurso, configura-o para "aviso" e fornece uma mensagem que diz "Este recurso foi descontinuado. Use o melhor recurso. Na versão 3 desta biblioteca, que será lançada em tal e tal uma data, o uso deste recurso será um erro ". Agora os usuários do recurso ainda podem usá-lo, mas têm tempo de atualizar seu código para usar o novo recurso.
  • Na versão 3, você atualiza o atributo para ser um erro, em vez de um aviso, e atualiza a mensagem para dizer "Este recurso foi preterido. Use o melhor recurso. Na versão 4 dessa biblioteca, que será lançada em tal e tal data, este recurso irá lançar ". Os usuários que não prestaram atenção no aviso anterior ainda recebem uma mensagem útil que informa como corrigir o problema, e devem corrigi-lo, porque agora o código não é compilado.
  • Na versão 4, você altera o recurso para gerar uma exceção fatal e altera a mensagem para dizer que o recurso será removido inteiramente na próxima versão.
  • Na versão 5, você remove o recurso completamente e, se os usuários reclamam, bem, ei, você deu a eles três ciclos de lançamento de aviso justo, e eles podem sempre continuar usando a versão 2 se tiverem uma strong impressão sobre isso.

A ideia aqui é fazer com que uma alteração de quebra seja o mais simples possível para os usuários afetados e garantir que eles continuem usando o recurso em pelo menos uma versão da biblioteca.

    
por 08.03.2018 / 18:36
fonte
23

Você entendeu mal o que significa "reprovado". Reprovado significa:

be usable but regarded as obsolete and best avoided, typically because it has been superseded.

Dicionários de Oxford

Por definição, um recurso obsoleto ainda será compilado.

Você está procurando remover o recurso em uma data específica. Isso é bom. A maneira como você faz isso é removê-lo nessa data .

Até lá, marque como obsoleto, obsoleto ou qualquer que seja sua linguagem de programação. Na mensagem, inclua a data em que será removida e a coisa que o substituirá. Isso gerará avisos, indicando que outros desenvolvedores devem evitar o novo uso e devem substituir o uso antigo sempre que possível. Esses desenvolvedores obedecerão ou ignorarão, e alguém terá que lidar com as conseqüências disso quando for removido. (Dependendo da situação, pode ser você ou os desenvolvedores que o usam.)

    
por 08.03.2018 / 20:20
fonte
12

Não se esqueça de que você precisa manter a capacidade de criar e depurar versões mais antigas do código para suportar versões do software que já foram lançadas. Sabotar uma compilação após uma determinada data significa que você também corre o risco de impedir a manutenção legítima e o trabalho de suporte no futuro.

Além disso, parece uma solução trivial para definir o relógio da minha máquina de volta um ano ou dois antes de compilar.

Lembre-se, "depreciado" é um aviso de que alguma coisa vai desaparecer no futuro. Quando você quer forçar as pessoas a usar essa API, basta remover o código associado . Não faz sentido deixar código na base de código se algum mecanismo o inutilizar. Remover o código fornece as verificações em tempo de compilação que você está procurando e não tem uma solução trivial.

Editar: vejo você se referir a "código antigo não utilizado" em sua pergunta. Se o código realmente for não usado , não há sentido em desaprová-lo. Basta apagá-lo.

    
por 08.03.2018 / 18:20
fonte
6

Eu nunca vi esse recurso antes - uma anotação que começa a produzir efeitos após uma data específica.

O @Deprecated pode ser suficiente, no entanto. Pegue avisos no CI e faça com que ele se recuse a aceitar a compilação, se houver alguma presente. Isso desloca a responsabilidade do compilador para o seu pipeline de construção, mas tem a vantagem de que você pode (semi) alterar facilmente o pipeline de construção adicionando etapas adicionais.

Observe que esta resposta não resolve totalmente o seu problema (por exemplo, compilações locais em máquinas de desenvolvedores ainda seriam bem-sucedidas, embora com avisos) e presume que você tenha um pipeline de IC configurado e funcionando. / p>     

por 08.03.2018 / 09:44
fonte
3

Uma maneira de pensar sobre isso é o que você quer dizer com hora / data ? Os computadores não sabem o que são esses conceitos: eles precisam ser programados de alguma forma. É bastante comum representar vezes no formato UNIX de "segundos desde a época", e é comum alimentar um determinado valor em um programa por meio de chamadas do sistema operacional. No entanto, não importa quão comum seja esse uso, é importante ter em mente que não é o tempo "real": é apenas uma representação lógica.

Como outros apontaram, se você fez um "prazo" usando este mecanismo, é trivial para alimentar em um horário diferente e quebrar esse "prazo". O mesmo vale para mecanismos mais elaborados, como perguntar a um servidor NTP (mesmo através de uma conexão "segura", já que podemos substituir nossos próprios certificados, autoridades de certificação ou até mesmo corrigir as bibliotecas de criptografia). A princípio, pode parecer que esses indivíduos são culpados por trabalhar em torno de seu mecanismo, mas pode ser o caso que é feito automaticamente e por boas razões . Por exemplo, é uma boa ideia ter compilações reproduzíveis e ferramentas para ajudar a redefinir / interceptar automaticamente essas chamadas de sistema não determinísticas. libfaketime faz exatamente isso, Nix define todos timestamps do arquivo para 1970-01-01 00:00:01 , o recurso de gravação / reprodução do Qemu falsifica toda a interação de hardware, etc. .

Isto é similar a Lei de Goodhart : se você fizer o comportamento de um programa depender do tempo lógico, então o o tempo lógico deixa de ser uma boa medida do tempo "real". Em outras palavras, as pessoas geralmente não vão mexer no relógio do sistema, mas sim se você der um motivo para elas.

Existem outras representações lógicas do tempo: uma delas é a versão do software (seu aplicativo ou alguma dependência). Esta é uma representação mais desejável para um "prazo" do que, e. Tempo no UNIX, já que é mais específico do que você gosta (alterar conjuntos de recursos / APIs) e, portanto, menos propenso a atropelar preocupações ortogonais (por exemplo, mexer com o tempo do UNIX para trabalhar no prazo pode acabar quebrando arquivos de log , caches, etc.).

Como outros já disseram, se você controlar a biblioteca e quiser "empurrar" essa alteração, poderá enviar uma nova versão que deprecia os recursos (causando avisos, ajudar os consumidores a encontrar e atualizar seu uso) e, em seguida, outra nova versão que remove completamente os recursos. Você poderia publicá-las imediatamente uma após a outra, se quiser, já que (novamente) as versões são apenas uma representação lógica do tempo, elas não precisam estar relacionadas ao tempo "real". Versões semânticas podem ajudar aqui.

O modelo alternativo é "puxar" a mudança. Isso é como seu "plano B": inclua um teste no aplicativo consumidor, que verifica se a versão dessa dependência é pelo menos o novo valor. Como de costume, red / green / refactor para propagar essa mudança através da base de código. Isso pode ser mais apropriado se a funcionalidade não for "ruim" ou "errada", mas apenas "um ajuste inadequado para este caso de uso".

Uma questão importante com a abordagem "pull" é se a versão de dependência conta ou não como uma "unidade" ( da funcionalidade ) e, portanto, merece testes; ou se é apenas um detalhe de implementação "particular", que deve ser exercido apenas como parte da unidade real ( da funcionalidade ) testes. Eu diria: se a distinção entre as versões da dependência realmente contar como um recurso do seu aplicativo, faça o teste (por exemplo, verificando se a versão do Python é > = 3.x). Se não, então não adicione o teste (pois ele será frágil, não-informativo e excessivamente restritivo); se você controlar a biblioteca, desça a rota "push". Se você não controla a biblioteca, basta usar qualquer versão que seja fornecida: se seus testes forem aprovados, não vale a pena se restringir; se eles não passarem, então esse é o seu "prazo" ali mesmo!

Existe outra abordagem, se você quiser desencorajar certos usos dos recursos de uma dependência (por exemplo, chamar certas funções que não funcionam bem com o resto do seu código), especialmente se você não controlar a dependência: os padrões de codificação proíbem / desencorajam o uso desses recursos e adicionam verificações para eles no seu linter.

Cada um deles será aplicável em diferentes circunstâncias.

    
por 09.03.2018 / 15:52
fonte
1

Você gerencia isso no nível de pacote ou biblioteca. Você controla um pacote e controla sua visibilidade. Você é livre para retrair a visibilidade. Eu vi isso internamente em grandes empresas e isso só faz sentido em culturas que respeitam a propriedade de pacotes, mesmo que os pacotes sejam de código aberto ou de uso livre.

Isso é sempre confuso porque as equipes de clientes simplesmente não querem alterar nada. Por isso, você precisa de algumas rodadas de lista de permissões apenas para trabalhar com clientes específicos para chegar a um prazo para migrar, possivelmente oferecendo suporte a elas.

    
por 08.03.2018 / 18:16
fonte
1

Um requisito é introduzir uma noção de tempo na construção. Em C, C ++ ou outras linguagens / sistemas de compilação que usam um pré-processador tipo C 1 , pode-se introduzir um registro de data e hora através de definições para o pré-processador no momento da compilação: CPPFLAGS=-DTIMESTAMP()=$(date '+%s') . Isso provavelmente aconteceria em um makefile.

No código, um deve comparar esse token e causar um erro se o tempo acabar. Note que usar uma macro de função detecta que alguém não definiu TIMESTAMP .

#if TIMESTAMP() == 0 || TIMESTAMP() > 1520616626
#   error "The time for this feature has run out, sorry"
#endif

Alternativamente, pode-se simplesmente "definir" o código em questão quando a hora chegar. Isso permitiria que o programa compilasse, desde que ninguém o utilizasse. Digamos que tenhamos um cabeçalho definindo uma api, "api.h", e não permitimos chamar old() depois de um certo tempo:

//...
void new1();
void new2();
#if TIMESTAMP() < 1520616626
   void old();
#endif
//...

Uma construção similar provavelmente eliminaria o corpo da função old() de algum arquivo de origem.

Claro que isso não é à prova de erros; pode-se simplesmente definir um antigo TIMESTAMP no caso da construção de emergência de sexta-feira à noite mencionada em outro lugar. Mas isso é, eu acho, bastante vantajoso.

Isso obviamente funciona somente quando a biblioteca é re-compilada - depois disso, o código obsoleto simplesmente não existe mais na biblioteca. Não não impediria que o código do cliente fosse vinculado a binários obsoletos.

O 1 C # suporta apenas a definição simples de símbolos do pré-processador, sem valores numéricos, o que torna esta estratégia inviável.     
por 09.03.2018 / 13:09
fonte
0

No Visual Studio, você pode configurar um script de pré-compilação que gera um erro após uma determinada data. Isso impedirá a compilação. Aqui está um script que gera um erro a partir de 12 de março de 2018 ( tirado daqui ):

@ECHO OFF

SET CutOffDate=2018-03-12

REM These indexes assume %DATE% is in format:
REM   Abr MM/DD/YYYY - ex. Sun 01/25/2015
SET TodayYear=%DATE:~10,4%
SET TodayMonth=%DATE:~4,2%
SET TodayDay=%DATE:~7,2%

REM Construct today's date to be in the same format as the CutOffDate.
REM Since the format is a comparable string, it will evaluate date orders.
IF %TodayYear%-%TodayMonth%-%TodayDay% GTR %CutOffDate% (
    ECHO Today is after the cut-off date.
    REM throw an error to prevent compilation
    EXIT /B 2
) ELSE (
    ECHO Today is on or before the cut-off date.
)

Certifique-se de ler as outras respostas nesta página antes de usar este script.

    
por 12.03.2018 / 16:02
fonte
-1

Eu entendo o objetivo do que você está tentando fazer. Mas, como outros já mencionaram, o sistema / compilador de compilação provavelmente não é o lugar certo para impor isso. Eu sugiro que a camada mais natural para impor essa política seja o SCM ou variáveis de ambiente.

Se você fizer o último, basicamente adicione um sinalizador de recurso que marca uma execução pré-depreciativa. Toda vez que você construir a classe reprovada ou chamar um método reprovado, verifique o sinalizador de recurso. Basta definir uma única função estática assertPreDeprecated() e incluí-la em cada caminho de código reprovado. Se estiver definido, ignore as chamadas de declaração. Se não for lançar uma exceção. Depois que a data passar, desmarque o sinalizador de recurso no ambiente de tempo de execução. Quaisquer chamadas reprovadas atrasadas para o código aparecerão nos logs de tempo de execução.

Para uma solução baseada em SCM, suponho que você esteja usando git e git-flow. (Se não, a lógica é facilmente adaptável a outros VC's). Crie uma nova filial postDeprecated . Nesse ramo, elimine todo o código obsoleto e comece a trabalhar na remoção de quaisquer referências até que seja compilado. Quaisquer alterações normais continuam sendo feitas na ramificação master . Continue mesclando as alterações de código relacionadas não-reprovadas em master de volta em postDeprecated para minimizar os desafios de integração.

Depois que a data de descontinuação terminar, crie uma nova ramificação preDeprecated a partir de master . Em seguida, mescle postDeprecated de volta em master . Supondo que sua versão seja desativada na ramificação master , você deve estar usando a ramificação pós-reprovada após a data. Se houver uma emergência, ou você não puder entregar os resultados a tempo, poderá sempre reverter para preDeprecated e fazer as alterações necessárias nessa ramificação.

    
por 08.03.2018 / 14:46
fonte