Dans mon équipe, nous avons nettoyé beaucoup d'anciennes choses dans un grand projet monolithique (classes entières, méthodes, etc.).
Pendant ces tâches de nettoyage, je me demandais s'il y avait une sorte d'annotation ou de bibliothèque plus sophistiquée que l'habituelle @Deprecated
. Cette @FancyDeprecated
devrait empêcher la construction du projet de réussir si vous n'avez pas nettoyé l'ancien code inutilisé après une date particulière.
J'ai recherché sur Internet et je n'ai rien trouvé qui ait les capacités décrites ci-dessous:
Je pense que je suis à la recherche d'une licorne ... Existe-t-il une technologie similaire pour n'importe quel langage de programme?
En tant que plan B je pense à la possibilité de faire de la magie avec des tests unitaires du code qui est destiné à être supprimé et qui commencent à échouer à la "date limite". Que penses-tu de cela? Une meilleure idée?
Je ne pense pas que ce serait une fonctionnalité utile quand elle interdit vraiment la compilation. Lorsqu'au 01/06/2018 de grandes parties du code ne seront pas compilées, ce qui a été compilé la veille, votre équipe supprimera rapidement à nouveau cette annotation, code nettoyé ou non.
Cependant, vous pouvez ajouter une annotation personnalisée au code comme
@Deprecated_after_2018_07_31
et créer un petit outil pour rechercher ces annotations. (Un simple liner dans grep le fera, si vous ne voulez pas utiliser la réflexion). Dans d'autres langages que Java, un commentaire standardisé adapté au "grepping" ou une définition de préprocesseur peuvent être utilisés.
Ensuite, exécutez cet outil peu de temps avant ou après la date particulière, et s'il trouve toujours cette annotation, rappelez à l'équipe de nettoyer ces parties de code de toute urgence.
Cela constituerait une caractéristique connue sous le nom de bombe à retardement . NE PAS CRÉER DE BOMBES À TEMPS.
Le code, peu importe la façon dont vous le structurez et le documentez, se transformera se transformera en une boîte noire quasi mythique mal comprise s'il vit au-delà d'un certain âge. La dernière chose dont tout le monde aura besoin dans le futur est encore un autre étrange mode d'échec qui les surprend totalement par surprise, au pire moment possible, et sans remède évident . Il n'y a absolument aucune excuse pour produire intentionnellement un tel problème.
Regardez-le de cette façon: si vous êtes suffisamment organisé et conscient de votre base de code pour vous soucier de l'obsolescence et le suivre, alors vous n'avez pas besoin d'un mécanisme à l'intérieur de le code pour vous le rappeler. Si vous ne l'êtes pas, il est probable que vous ne soyez pas à jour sur les autres aspects de la base de code et que vous ne pourrez probablement pas répondre à l'alarme en temps opportun et correctement. En d'autres termes, les bombes à retardement ne servent à rien pour personne. Dis juste non!
En C #, vous utiliseriez le ObsoleteAttribute
de la manière suivante:
L'idée ici est de faire un changement de rupture aussi indolore que possible pour les utilisateurs concernés et de s'assurer qu'ils peuvent continuer à utiliser la fonctionnalité pour au moins une version de la bibliothèque.
Vous avez mal compris ce que signifie "obsolète". Obsolète signifie:
être utilisable mais considéré comme obsolète et qu'il vaut mieux éviter, généralement parce qu'il a été remplacé.
Par définition, une fonctionnalité obsolète sera toujours compilée.
Vous cherchez à supprimer la fonctionnalité à une date spécifique. C'est très bien. La façon dont vous faites cela vous le supprimez à cette date .
Jusque-là, marquez comme obsolète, obsolète ou quel que soit votre langage de programmation. Dans le message, incluez la date à laquelle il sera supprimé et l'élément qui le remplacera. Cela générera des avertissements, indiquant que les autres développeurs devraient éviter de nouvelles utilisations et remplacer les anciennes utilisations dans la mesure du possible. Ces développeurs se conformeront ou l'ignoreront, et quelqu'un devra faire face aux conséquences de cela lorsqu'il sera supprimé. (Selon la situation, il peut s'agir de vous ou des développeurs qui l'utilisent.)
N'oubliez pas que vous devez conserver la possibilité de créer et de déboguer des versions antérieures du code afin de prendre en charge les versions du logiciel qui ont déjà été publiées. Saboter une version après une certaine date signifie que vous risquez également de vous empêcher de faire des travaux de maintenance et d'assistance légitimes à l'avenir.
En outre, il semble être une solution de contournement triviale de remettre l'horloge de ma machine à un an ou deux avant la compilation.
Rappelez-vous, "obsolète" est un avertissement que quelque chose va disparaître à l'avenir. Lorsque vous souhaitez empêcher de force les utilisateurs d'utiliser cette API, supprimez simplement le code associé. Il est inutile de laisser du code dans la base de code si un mécanisme le rend inutilisable. La suppression du code vous donne les vérifications de compilation que vous recherchez et n'a pas de solution de contournement triviale.
Edit: Je vois que vous vous référez à "l'ancien code inutilisé" dans votre question. Si le code est vraiment inutilisé, il est inutile de le déprécier. Il suffit de le supprimer.
Je n'ai jamais vu une telle fonctionnalité auparavant - une annotation qui commence à prendre effet après une date particulière.
Le @Deprecated
peut cependant suffire. Attrapez les avertissements dans CI et faites-le refuser d'accepter la construction si elle est présente. Cela déplace la responsabilité du compilateur vers votre pipeline de build, mais présente l'avantage que vous pouvez (semi) facilement modifier le pipeline de build en ajoutant des étapes supplémentaires.
Notez que cette réponse ne le fait pas résout complètement votre problème (par exemple, les builds locaux sur les machines des développeurs réussiraient toujours, bien qu'avec des avertissements) et supposent que vous avez un pipeline CI configuré et en cours d'exécution.
Vous recherchez des calendriers ou listes de tâches .
Une autre alternative consiste à utiliser des avertissements de compilateur personnalisés ou des messages de compilateur, si vous parvenez à avoir peu ou pas d'avertissements dans votre base de code. Si vous avez trop d'avertissements, vous devrez consacrer des efforts supplémentaires (environ 15 minutes?) Et récupérer l'avertissement du compilateur dans le rapport de génération que votre intégration continue délivre sur chaque génération.
Les rappels que le code doit être corrigé sont bons et nécessaires. Parfois, ces rappels ont des délais stricts dans le monde réel, donc les mettre sur une minuterie peut également être nécessaire.
L'objectif est de rappeler continuellement aux gens que le problème existe et doit être résolu dans un délai donné - une fonctionnalité qui rompt simplement la construction à un moment spécifique non seulement ne fait pas cela, mais cette fonctionnalité est elle-même un problème qui doit être fixé dans un délai donné.
Une façon d'y penser est ce que vous entendez par heure/date ? Les ordinateurs ne savent pas ce que sont ces concepts: ils doivent être programmés d'une manière ou d'une autre. Il est assez courant de représenter fois au format UNIX de "secondes depuis l'époque", et il est courant d'introduire une valeur particulière dans un programme via des appels OS . Cependant, quelle que soit la fréquence de cette utilisation, il est important de garder à l'esprit que ce n'est pas l'heure "réelle": c'est juste une représentation logique.
Comme d'autres l'ont souligné, si vous avez fixé un "délai" à l'aide de ce mécanisme, il est trivial de se nourrir à une heure différente et de dépasser ce "délai". Il en va de même pour des mécanismes plus élaborés comme demander un serveur NTP (même sur une connexion "sécurisée", car nous pouvons substituer nos propres certificats, autorités de certification ou même patcher les bibliothèques de chiffrement). Au début il peut sembler que de telles personnes sont en faute pour contourner votre mécanisme, mais il se peut que cela soit fait automatiquement et pour bonnes raisons . Par exemple, c'est une bonne idée d'avoir versions reproductibles , et des outils pour aider cela pourraient réinitialiser/intercepter automatiquement un tel système non déterministe appelle. libfaketime fait exactement cela, Nix définit tous les horodatages du fichier sur 1970-01-01 00:00:01
, Qemu's fonction d'enregistrement/relecture simule toute interaction matérielle, etc.
Ceci est similaire à loi de Goodhart : si vous faites dépendre le comportement d'un programme du temps logique, alors le temps logique cesse d'être une bonne mesure du temps "réel". En d'autres termes, les gens ne gâchent généralement pas l'horloge système, mais ils le feront si vous leur en donnez une raison.
Il existe d'autres représentations logiques du temps: l'une d'entre elles est la version du logiciel (soit votre application, soit certaines dépendances). Il s'agit d'une représentation plus souhaitable pour un "délai" que par ex. Temps UNIX, car il est plus spécifique à la chose qui vous intéresse (modification des ensembles de fonctionnalités/API) et donc moins susceptible de piétiner des problèmes orthogonaux (par exemple, jouer avec le temps UNIX pour contourner votre échéance pourrait finir par casser des fichiers journaux, des tâches cron) , caches, etc.).
Comme d'autres l'ont dit, si vous contrôlez la bibliothèque et que vous souhaitez "pousser" ce changement, vous pouvez pousser une nouvelle version qui déconseille les fonctionnalités (provoquant des avertissements, pour aider les consommateurs à trouver et à mettre à jour leur utilisation), puis une autre nouvelle version qui supprime le fonctionnalités entièrement. Vous pouvez les publier immédiatement les uns après les autres si vous le souhaitez, car (encore une fois) les versions ne sont qu'une représentation logique du temps, elles n'ont pas besoin d'être liées au temps "réel". Le versioning sémantique peut aider ici.
Le modèle alternatif consiste à "tirer" le changement. C'est comme votre "plan B": ajoutez un test à l'application consommatrice, qui vérifie que la version de cette dépendance est au moins la nouvelle valeur. Comme d'habitude, red/green/refactor pour propager ce changement à travers la base de code. Cela peut être plus approprié si la fonctionnalité n'est pas "mauvaise" ou "mauvaise", mais juste "un mauvais ajustement pour ce cas d'utilisation".
Une question importante avec l'approche "pull" est de savoir si la version de dépendance compte comme une "unité" ( de fonctionnalité ), et mérite donc d'être testée; ou s'il s'agit simplement d'un détail d'implémentation "privé", qui ne doit être exercé que dans le cadre de tests unitaires réels ( de fonctionnalité ). Je dirais: si la distinction entre les versions de la dépendance compte vraiment comme une fonctionnalité de votre application, faites le test (par exemple, vérifiez que la version Python est> = 3.x Sinon, alors n'ajoutez pas le test (car il sera fragile, non informatif et trop restrictif); si vous contrôlez la bibliothèque, allez Si vous ne contrôlez pas la bibliothèque, utilisez simplement la version fournie: si vos tests réussissent, cela ne vaut pas la peine de vous restreindre; s'ils ne réussissent pas, c'est votre "date limite"!
Il existe une autre approche, si vous voulez décourager certaines utilisations des fonctionnalités d'une dépendance (par exemple, appeler certaines fonctions qui ne fonctionnent pas bien avec le reste de votre code), surtout si vous ne contrôlez pas la dépendance: faites interdire vos normes de codage/découragez l'utilisation de ces fonctionnalités et ajoutez-les à votre linter.
Chacun de ceux-ci sera applicable dans différentes circonstances.
Une exigence est d'introduire une notion de temps dans la construction. En C, C++ ou dans d'autres langages/systèmes de construction qui utilisent un préprocesseur de type C1, on pourrait introduire un horodatage à travers les définitions du préprocesseur au moment de la construction: CPPFLAGS=-DTIMESTAMP()=$(date '+%s')
. Cela se produirait probablement dans un makefile.
Dans le code, on comparerait ce jeton et provoquerait une erreur si le temps était écoulé. Notez que l'utilisation d'une macro de fonction intercepte le cas où quelqu'un n'a pas défini TIMESTAMP
.
#if TIMESTAMP() == 0 || TIMESTAMP() > 1520616626
# error "The time for this feature has run out, sorry"
#endif
Alternativement, on pourrait simplement "définir" le code en question le moment venu. Cela permettrait au programme de compiler, à condition que personne ne l'utilise. Disons, nous avons un en-tête définissant une api, "api.h", et nous ne permettons pas d'appeler old()
après un certain temps:
//...
void new1();
void new2();
#if TIMESTAMP() < 1520616626
void old();
#endif
//...
Une construction similaire éliminerait probablement le corps de la fonction de old()
d'un fichier source.
Bien sûr, ce n'est pas à toute épreuve; on peut simplement définir un ancien TIMESTAMP
dans le cas du build d'urgence du vendredi soir mentionné ailleurs. Mais c'est, je pense, plutôt avantageux.
Cela ne fonctionne évidemment que lorsque la bibliothèque est recompilée - après cela, le code obsolète n'existe tout simplement plus dans la bibliothèque. Cela pas empêcherait cependant le code client de se lier à des binaires obsolètes.
Vous gérez cela au niveau du package ou de la bibliothèque. Vous contrôlez un package et contrôlez sa visibilité. Vous êtes libre de rétracter la visibilité. J'ai vu cela en interne dans de grandes entreprises et cela n'a de sens que dans les cultures qui respectent la propriété des packages même si les packages sont open source ou gratuits.
C'est toujours compliqué parce que les équipes clientes ne veulent tout simplement rien changer, vous avez donc souvent besoin de quelques tours de liste blanche uniquement lorsque vous travaillez avec les clients spécifiques pour convenir d'une date limite de migration, en leur offrant éventuellement une assistance.
Dans Visual Studio, vous pouvez configurer un script de pré-génération qui génère une erreur après une certaine date. Cela empêchera la compilation. Voici un script qui génère une erreur le 12 mars 2018 ou après ( tiré d'ici ):
@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.
)
Assurez-vous de lire les autres réponses sur cette page avant d'utiliser ce script.