Previene la compilazione del codice deprecato dopo aver raggiunto una scadenza [chiusa]

67

Nel mio team abbiamo pulito un sacco di cose vecchie in un grande progetto monolitico (intere classi, metodi, ecc.).

Durante le attività di pulizia mi stavo chiedendo se esiste un tipo di annotazione o di una libreria più elaborata rispetto al solito @Deprecated . Questo @FancyDeprecated dovrebbe impedire il completamento della generazione del progetto se non hai pulito il vecchio codice inutilizzato dopo che è stata passata una determinata data.

Ho cercato su Internet e non ho trovato nulla che abbia le capacità descritte di seguito:

  • dovrebbe essere un'annotazione o qualcosa di simile da inserire nel codice che si desidera eliminare prima di una data specifica
  • prima di quella data il codice verrà compilato e tutto funzionerà normalmente
  • dopo quella data il codice non verrà compilato e ti darà un messaggio che ti avviserà del problema

Penso di essere alla ricerca di un unicorno ... Esiste una tecnologia simile per qualsiasi linguaggio di programmazione?

Come piano B sto pensando alla possibilità di creare la magia con alcuni test unitari del codice che si intende rimuovere che iniziano a fallire alla "scadenza". Cosa ne pensi di questo? Qualche idea migliore?

    
posta Arcones 08.03.2018 - 09:31
fonte

12 risposte

61

Non penso che questa sarebbe una caratteristica utile quando proibisce davvero la compilazione. Quando al 01/06/2018 non verranno compilate grandi parti del codice compilate il giorno precedente, il tuo team rimuoverà rapidamente quell'annotazione, il codice ripulito o meno.

Tuttavia, potresti aggiungere alcune annotazioni personalizzate al codice come

@Deprecated_after_2018_07_31

e crea un piccolo strumento per scansionare quelle annotazioni. (Una semplice fodera in grep lo farà, se non vuoi utilizzare la riflessione). In altre lingue oltre a Java, è possibile utilizzare un commento standardizzato adatto per "grepping" o una definizione del preprocessore.

Quindi esegui lo strumento poco prima o dopo la data specifica e, se trova ancora quell'annotazione, ricorda al team di pulire urgentemente quelle parti del codice.

    
risposta data 08.03.2018 - 10:31
fonte
284

Ciò costituirebbe una caratteristica nota come bomba a orologeria . NON CREA BOMBI DI ORA.

Codice, non importa quanto bene lo struttura e lo documenti, sarà trasformarsi in una scatola nera quasi mistica mal compresa se sopravvive oltre una certa età. L'ultima cosa di cui in futuro c'è bisogno è la ancora un'altra strana modalità di errore che li colga totalmente di sorpresa, nel peggior momento possibile e senza un ovvio rimedio. Non c'è assolutamente alcuna scusa per produrre intenzionalmente un problema del genere.

Consideralo in questo modo: se sei organizzato e consapevole della tua base di codice abbastanza da preoccuparti dell'obsolescenza e di seguirla, allora non hai bisogno di un meccanismo entro il codice ricordarti. Se non lo sei, è probabile che anche tu non sia aggiornato su altri aspetti della base di codice e probabilmente non sarai in grado di rispondere alla sveglia in modo tempestivo e corretto. In altre parole, le bombe a orologeria non servono a nessuno scopo. Basta dire no!

    
risposta data 08.03.2018 - 10:21
fonte
69

In C # utilizzeresti ObsoleteAttribute nel modo seguente:

  • Nella versione 1, la funzionalità viene spedita. Un metodo, classe, qualunque cosa.
  • Nella versione 2, spedisci una funzione migliore destinata a sostituire la funzione originale. Hai messo un attributo Obsoleta sulla funzione, lo hai impostato su "warning" e gli hai dato un messaggio che diceva "Questa funzione è deprecata." Usa invece la funzione migliore. Nella versione 3 di questa libreria, che verrà rilasciata su tale e tale una data, l'uso di questa funzione sarà un errore. " Ora gli utenti della funzione possono ancora usarlo, ma hanno il tempo di aggiornare il loro codice per utilizzare la nuova funzionalità.
  • Nella versione 3, si aggiorna l'attributo come un errore piuttosto che un avvertimento e si aggiorna il messaggio per dire "Questa funzionalità è deprecata." Utilizzare invece la funzione migliore. Nella versione 4 di questa libreria, che verrà rilasciata su tale e una tale data, questa funzione verrà lanciata. " Gli utenti che non hanno ascoltato il tuo precedente avvertimento ricevono comunque un messaggio utile che spiega loro come risolvere il problema e devono correggerlo, perché ora il loro codice non viene compilato.
  • Nella versione 4, cambi la funzionalità in modo che generi qualche eccezione fatale e cambia il messaggio per dire che la funzione verrà completamente rimossa nella prossima versione.
  • Nella versione 5, la funzionalità viene completamente rimossa e, se gli utenti si lamentano, beh, hai dato loro tre cicli di rilascio di un avvertimento equo e possono sempre continuare a utilizzare la versione 2 se ne sentono strongmente.

L'idea qui è di apportare una modifica al più semplice e indolore possibile per gli utenti interessati e di garantire che possano continuare a utilizzare la funzione per almeno una versione della libreria.

    
risposta data 08.03.2018 - 18:36
fonte
23

Hai frainteso cosa significa "deprecato". Obsoleto significa:

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

Oxford Dizionari

Per definizione, una funzione deprecata verrà comunque compilata.

Stai cercando di rimuovere la funzione in una data specifica. Va bene. Il modo in cui lo fai è lo rimuovi in quella data .

Fino ad allora, segna come deprecato, obsoleto o qualunque sia il linguaggio di programmazione che lo chiama. Nel messaggio, includi la data in cui verrà rimosso e la cosa che lo sostituisce. Questo genererà avvertimenti, indicando che altri sviluppatori dovrebbero evitare il nuovo utilizzo e dovrebbero sostituire il vecchio utilizzo laddove possibile. Questi sviluppatori lo rispetteranno o lo ignoreranno e qualcuno dovrà affrontare le conseguenze di ciò quando verrà rimosso. (A seconda della situazione, potrebbe essere tu o potrebbero essere gli sviluppatori che lo utilizzano.)

    
risposta data 08.03.2018 - 20:20
fonte
12

Non dimenticare che è necessario conservare la possibilità di creare ed eseguire il debug di versioni precedenti del codice per supportare versioni del software che sono già state rilasciate. Il sabotaggio di una build dopo una certa data significa che rischi anche di impedirti di fare manutenzione legittima e supportare il lavoro in futuro.

Inoltre, sembra una soluzione banale per impostare l'orologio della mia macchina indietro di un anno o due prima della compilazione.

Ricorda che "deprecato" è un avvertimento che qualcosa andrà via in futuro. Quando vuoi impedire con forza alle persone di utilizzare quell'API, rimuovi il codice associato . Non ha senso lasciare il codice nel codice di base se alcuni meccanismi lo rendono inutilizzabile. La rimozione del codice ti offre i controlli in fase di compilazione che stai cercando e non ha una soluzione banale.

Modifica: vedo che ti riferisci a "vecchio codice inutilizzato" nella tua domanda. Se il codice è veramente inutilizzato , non ha senso depretterlo. Basta cancellarlo.

    
risposta data 08.03.2018 - 18:20
fonte
6

Non ho mai visto prima una funzione del genere - un'annotazione che ha effetto dopo una data specifica.

Il @Deprecated può essere sufficiente, tuttavia. Cattura gli avvertimenti in CI e fallo rifiutare di accettare la build se sono presenti. Ciò sposta la responsabilità dal compilatore alla tua pipeline di build, ma ha il vantaggio che puoi (semi) modificare facilmente la pipeline di build aggiungendo ulteriori passaggi.

Tieni presente che questa risposta non risolve completamente il tuo problema (ad esempio, le build locali sulle macchine degli sviluppatori continuerebbero, sebbene con avvertimenti) e presuppongono che tu abbia una pipeline CI configurata e in esecuzione.

    
risposta data 08.03.2018 - 09:44
fonte
4

Stai cercando calendari o elenchi di cose da fare .

Un'altra alternativa è usare avvisi di compilazione personalizzati o messaggi del compilatore, se si riesce ad avere pochi avvisi se presenti nella base di codice. Se si dispone di troppi avvisi, è necessario dedicare uno sforzo aggiuntivo (circa 15 minuti?) E raccogliere l'avviso del compilatore nel report di costruzione che è continuous integration fornisce su ogni build.

I promemoria che il codice deve essere corretto sono buoni e necessari. A volte questi promemoria hanno rigide scadenze nel mondo reale, quindi è anche necessario metterli su un timer.

L'obiettivo è di ricordare continuamente alle persone che il problema esiste e deve essere risolto con un determinato intervallo di tempo: una funzionalità che interrompe semplicemente la generazione in un momento specifico non solo non lo fa, ma quella funzionalità è di per sé un problema che deve essere risolto con un determinato periodo di tempo.

    
risposta data 08.03.2018 - 12:46
fonte
3

Un modo per pensarci è cosa intendi per data / ora ? I computer non sanno cosa siano questi concetti: devono essere programmati in qualche modo. È abbastanza comune per rappresentare volte nel formato UNIX di "secondi dall'epoca", ed è comune inserire un particolare valore in un programma tramite chiamate al sistema operativo. Tuttavia, non importa quanto sia comune questo utilizzo, è importante tenere presente che non è il tempo "effettivo": è solo una rappresentazione logica.

Come altri hanno sottolineato, se hai fatto una "scadenza" usando questo meccanismo, è banale dare da mangiare in un altro momento e interrompere quella "scadenza". Lo stesso vale per i meccanismi più elaborati come chiedere un server NTP (anche su una connessione "sicura", dato che possiamo sostituire i nostri certificati, le autorità di certificazione o anche le patch delle librerie crittografiche). All'inizio potrebbe sembrare che tali individui siano in errore per aggirare il meccanismo, ma potrebbe essere il caso di automaticamente e per buoni motivi . Ad esempio, è una buona idea avere build riproducibili e strumenti per aiutare questo a ripristinare / intercettare automaticamente tali chiamate di sistema non deterministiche. libfaketime fa esattamente questo, Nix imposta tutto data / ora del file in 1970-01-01 00:00:01 , la funzione di registrazione / riproduzione di Qemu simula tutte le interazioni hardware, ecc. .

Questo è simile alla legge di Goodhart : se fai il comportamento di un programma dipende dal tempo logico, allora il il tempo logico cessa di essere una buona misura del tempo "effettivo". In altre parole, le persone generalmente non si scherzano con l'orologio di sistema, ma lo faranno se gli dai un motivo.

Ci sono altre rappresentazioni logiche del tempo: una di queste è la versione del software (la tua app o qualche dipendenza). Questa è una rappresentazione più desiderabile per una "scadenza" che ad es. Tempo UNIX, dal momento che è più specifico della cosa a cui tieni (cambio di set di funzioni / API) e quindi meno probabilità di calpestare preoccupazioni ortogonali (ad esempio giocherellare con il tempo UNIX per aggirare la tua scadenza potrebbe finire per rompere i file di registro, cron jobs , cache, ecc.).

Come altri hanno detto, se controlli la libreria e vuoi "spingere" questa modifica, puoi spingere una nuova versione che depreca le funzionalità (causando avvertimenti, per aiutare gli utenti a trovare e aggiornare il loro utilizzo), quindi un'altra nuova versione che rimuove completamente le funzionalità. Potresti pubblicarli immediatamente uno dopo l'altro, se vuoi, dato che le (di nuovo) versioni sono semplicemente una rappresentazione logica del tempo, non devono essere correlate al tempo "reale". Il controllo delle versioni semantiche può aiutarti qui.

Il modello alternativo è quello di "tirare" il cambiamento. Questo è come il tuo "piano B": aggiungi un test all'applicazione che consuma, che verifica che la versione di questa dipendenza sia almeno il nuovo valore. Come al solito, red / green / refactor per propagare questa modifica attraverso il codebase. Questo potrebbe essere più appropriato se la funzionalità non è "cattiva" o "sbagliata", ma solo "inadatta per questo caso d'uso".

Una domanda importante con l'approccio "pull" è se la versione di dipendenza conta come una "unità" ( di funzionalità ), e quindi merita il test; o se si tratta solo di un dettaglio di implementazione "privato", che deve essere esercitato solo come parte dell'unità reale ( di funzionalità ) test. Direi: se la distinzione tra le versioni della dipendenza conta davvero come una caratteristica dell'applicazione, quindi esegui il test (ad esempio, verificando che la versione di Python sia > = 3.x). In caso contrario, non aggiungere il test (poiché sarà fragile, non informativo e eccessivamente restrittivo); se controlli la libreria, scorri la rotta "push". Se non controlli la libreria, usa solo la versione fornita: se i tuoi test passano, non vale la pena limitarti; se non passano, quella è la tua "scadenza" proprio lì!

Esiste un altro approccio, se si desidera scoraggiare determinati usi delle funzioni di una dipendenza (ad es. chiamare determinate funzioni che non funzionano bene con il resto del codice), specialmente se non si controlla la dipendenza: avere il proprio gli standard di codifica vietano / scoraggiano l'uso di queste funzionalità e aggiungono controlli per loro al proprio linter.

Ciascuno di questi sarà applicabile in diverse circostanze.

    
risposta data 09.03.2018 - 15:52
fonte
1

Lo gestisci a livello di pacchetto o di libreria. Tu controlli un pacchetto e controlli la sua visibilità. Sei libero di ritrattare la visibilità. L'ho visto internamente presso grandi aziende e ha senso solo nelle culture che rispettano la proprietà dei pacchetti anche se i pacchetti sono open source o liberi di usare.

Questo è sempre disordinato perché i team del cliente semplicemente non vogliono cambiare nulla, quindi spesso hai bisogno di qualche giro di whitelist-only mentre lavori con i client specifici per concordare una scadenza da migrare, eventualmente offrendogli supporto.

    
risposta data 08.03.2018 - 18:16
fonte
1

Un requisito è introdurre una nozione di tempo nella build. In C, C ++, o in altri linguaggi / sistemi di compilazione che usano un preprocessore 1 simile a C, si potrebbe introdurre un time stamp attraverso le definizioni per il preprocessore al momento della compilazione: CPPFLAGS=-DTIMESTAMP()=$(date '+%s') . Questo probabilmente si verifica in un makefile.

Nel codice si confronta quel token e si genera un errore se il tempo è scaduto. Nota che l'uso di una funzione macro rileva che qualcuno non ha definito TIMESTAMP .

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

In alternativa, si potrebbe semplicemente "definire" il codice in questione quando è giunto il momento. Ciò consentirebbe la compilazione del programma, a condizione che nessuno lo utilizzi. Diciamo, abbiamo un'intestazione che definisce un API, "api.h", e non permettiamo di chiamare old() dopo un certo tempo:

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

Un costrutto simile probabilmente eliminerebbe il corpo della funzione old() da un file sorgente.

Naturalmente questo non è infallibile; si può semplicemente definire una vecchia TIMESTAMP nel caso della build di emergenza del venerdì sera menzionata altrove. Ma è, penso, piuttosto vantaggioso.

Questo ovviamente funziona solo quando la libreria viene ricompilata - dopo che il codice obsoleto semplicemente non esiste più nella libreria. Tuttavia non impedisce al codice client di collegarsi ai binari obsoleti.

1 C # supporta solo la semplice definizione di simboli di preprocessore, senza valori numerici, il che rende questa strategia non praticabile.     
risposta data 09.03.2018 - 13:09
fonte
0

In Visual Studio, puoi impostare uno script pre-compilazione che genera un errore dopo una certa data. Ciò impedirà la compilazione. Ecco uno script che genera un errore a partire dal 12 marzo 2018 ( preso da qui ):

@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.
)

Assicurati di leggere le altre risposte su questa pagina prima di utilizzare questo script.

    
risposta data 12.03.2018 - 16:02
fonte
-1

Capisco l'obiettivo di ciò che stai cercando di fare. Ma come altri hanno già detto, il sistema di compilazione / compilatore probabilmente non è il posto giusto per far rispettare questo. Suggerirei che il livello più naturale per applicare questo criterio sia l'SCM o le variabili di ambiente.

Se si fa il secondo, in pratica si aggiunge un flag di funzionalità che segna un'esecuzione pre-deprecazione. Ogni volta che costruisci la classe deprecata o chiama un metodo deprecato, controlla il flag di funzionalità. Basta definire una singola funzione statica assertPreDeprecated() e aggiungerla a ogni percorso di codice deprecato. Se è impostato, ignora asserisci chiamate. Se non si lancia in un'eccezione. Una volta trascorsa la data, disattiva il flag di funzionalità nell'ambiente di runtime. Tutte le chiamate obsolete persistenti al codice verranno visualizzate nei registri di runtime.

Per una soluzione basata su SCM, presumo che tu stia utilizzando git e git-flow. (In caso contrario, la logica è facilmente adattabile ad altri VCS). Crea un nuovo ramo postDeprecated . In quel ramo elimina tutto il codice deprecato e inizia a lavorare sulla rimozione di qualsiasi riferimento finché non viene compilato. Qualsiasi normale modifica continua al ramo master . Continua a unire tutte le modifiche del codice correlate non deprecate in master in postDeprecated per ridurre al minimo le sfide di integrazione.

Al termine della data di ritiro, crea un nuovo ramo preDeprecated da master . Quindi unisci postDeprecated di nuovo in master . Supponendo che il tuo rilascio vada fuori dal ramo master , dovresti ora utilizzare il ramo post-deprecato dopo la data. Se c'è un'emergenza o non puoi fornire risultati in tempo, puoi sempre eseguire il rollback su preDeprecated e apportare eventuali modifiche necessarie su quel ramo.

    
risposta data 08.03.2018 - 14:46
fonte

Leggi altre domande sui tag