Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Du mauvais usage de DateTime.Now dans les applications

[new:30/06/2014]Qui pourrait s’interroger sur le bienfondé de l’utilisation de DateTime.Now pour obtenir la date et l’heure courantes ? Il y a pourtant matière à réfléchir !

Une solution simple à un besoin simple

Utiliser DateTime.Now pour connaitre l’heure ou la date courantes dans une application est une merveilleuse solution simple à un besoin qui l’est tout autant. La solution est simple car elle utilise le framework .NET correctement, c’est à dire comme des API rationalisant et objectivant des fonctions système (heure, fichiers, mémoire…).

Le problème lui-même est souvent d’une simplicité tout aussi déconcertante :

  • Effectuer une opération à une heure ou un jour particulier (type prélèvement bancaire par exemple)
  • Gérer un système de réservation et d’expiration d’entités
  • Lancer une lettre d’information par e-mail ou un traitement particulier uniquement la nuit
  • Gérer des dates anniversaires (début, fin de contrats, date de relance…)
  • Etc, etc,

 

Dans de tel cas on écrira le plus souvent un code qui peut se généraliser à  :

if (DateTime.Now>LaDateTest) { … action }

Un besoin moins simple qu’il n’y parait…

En réalité tout cela est idyllique, donc irréel…

Une véritable application sera soumise à deux cas d’utilisation principaux que DateTime.Now ne permet pas d’adresser :

  • Si on souhaite globalement (ou non) appliquer un “offset” à la date ou l’heure courante (ie: “avancer” ou “retarder” sur l’heure légale locale)
  • Quand on doit tester l’application !

 

Dans le premier cas il faut revoir toutes les séquences de code utilisant directement (c’est assez facile) ou indirectement (beaucoup plus dur) la propriété DateTime.Now. La charge de travail sera lourde et le résultat incertain, entendez plein de petits bogues difficiles à déceler. D’autant plus qu’il n’y a aucun moyen de tester tout cela proprement justement…

Dans le second cas rien n’est possible tout simplement.

Imaginons juste un instant une fonctionnalité qui dans l’application considérée effectue une clôture comptable à 18h00 précises avec déclenchement de plusieurs actions (somme des mouvements de la journée de chaque compte, à nouveau pour la journée suivante, alerte sur comptes débiteurs, etc).

Vous êtes en train de coder cette application. Et vous devez vous assurer que tout marche bien. C’est un peu le minimum syndical du développeur…

Allez-vous rester assis jusqu’à 18h00 pour voir si tout se met en route comme prévu alors que vous venez à peine d’arriver au bureau et que le café fume encore dans son gobelet ? Et si cela ne marche pas et qu’il faut effectuer une correction, allez-vous rester planté là 24h jusqu’a temps qu’il soit de nouveau 18h00 pile ?

je n’y crois pas… Donc la fonction sera testée en production n’est-ce pas ?

Ne rougissez pas, et ne niez pas non plus, je sais que ça se passe comme ça ! Sourire

Du bon usage de la date et de l’heure dans une application bien conçue et bien testée

Constater une faiblesse c’est déjà progresser, mais encore faut-il avancer un pas de plus et proposer une solution…

Et cette dernière est toute simple et tient en une règle : n’utilisez jamais DateTime.Now !

C’est radical, absolu, donc clair et facile à respecter !

C’est bien joli d’inventer des règles de ce genre, mais en réalité on fait comment ?

… On remplace la fichue propriété de la classe DateTime par une autre propriété d’une autre classe que nous allons créer pour cet usage.

Là aussi c’est simple, radical et donc facile à mettre en œuvre.

Imaginons une classe Horloge, statique (qui peut être vue comme un service), qui expose une propriété Maintenant, de type DateTime. Propriété statique aussi.

C’est cette propriété que nous allons utiliser systématiquement dans notre application à la place de DateTime.Now

En quoi cette propriété et cette nouvelle classe peuvent elles résoudre ce que la classe du framework ne peut pas faire ?

C’est très simple là encore. La propriété Maintenant retournera soit DateTime.Now, par défaut, sans avoir rien à faire de spécial, soit le résultat d’une fonction de type DateTime que l’application pourra initialiser à tout moment.

Pour tester si tel morceau de l’application se déclenche à 18h00 pile il sera facile de retourner directement cette heure là. Quelle que soit la véritable heure, pour l’application il sera 18h00 pile… On peut aussi complexifier un peu et se débrouiller pour qu’il soit 18h00 moins une minute pour voir le passage entre “l’avant” et “l’après” 18h00 pile s’effectuer (souvent un bogue peut se cacher dans des transitions de ce type, là où un test sur une valeur précise ne détecte pas le problème).

Bref, la nouvelle classe et sa nouvelle propriété vont permettre d’écrire un code testable à toute heure en fixant arbitrairement l’heure et la date de façon globale (donc cohérente pour toute l’application, ce qui est conforme à une situation réelle en production).

Bien entendu cette classe pourra être complétée à volonté selon le projet, l’utilisation par celui-ci de l’heure UTC ou non, etc. C’est le principe qui nous intéresse ici. Le détail de l’implémentation vous appartenant.

La classe Horloge

Dans l’esprit qui nous habite ici nous ne voulons que modifier l’accès à DateTime.Now, les autres fonctions et propriétés de la classe DateTime restant ce qu’elles sont. Encore une fois si d’autres besoins du même type doivent être couverts par une solution identique, le développeur ajoutera ce qu’il faut à la classe Horloge.

public static class Horloge
{
    private static Func<DateTime> fonction;
 
    static Horloge()
    {
        fonction = () => DateTime.Now;
    }
 
    public static DateTime Maintenant
    {
        get
        {
            return fonction();
        }
    }
 
    public static Func<DateTime> FonctionMaintenant
    {
        set
        {
            fonction = value ?? (() => DateTime.Now);
        }
    }
 
    public static void Reset()
    {
        FonctionMaintenant = null;
    }
}

 

Il suffit désormais de remplacer systématiquement dans l’application tous les appels à DateTime.Now par Horloge.Maintenant (chacun choisira d’angliciser ou non ces noms).

Jusque là nous aurons une stricte équivalence fonctionnelle sans rien de moins et sans rien de plus que l’appel à la classe du framework.

Mais si nous décidons de tester notre application pour voir ce qu’il se passe quand il est 18h00, alors il sera facile en début d’exécution ou n’importe où dans le code où cela prend un sens, d’ajouter quelque chose comme :

// nota : attention à la closure !
var simulation = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, 18, 0, 0);
Horloge.FonctionMaintenant = () => simulation;

 

Comme je l’indique en commentaire il faut faire attention aux closures dans ce type de code.

Naturellement la fonction retournée peut être beaucoup plus complexe que de retourner une variable. C’est une expression Lambda qui peut contenir tout ce qu’on désire (décalage temporel glissant, heure ou date aléatoires disposant de leur propre timer, incrémentation à chaque appel pour simuler le temps qui passe dans une série de données et la fabrication de statistiques ou de graphiques, etc…).

Conclusion

Penser à ce genre de choses en début d’écriture d’une application apporte un confort qu’on ne regrette jamais. C’est un peu comme utiliser des Interfaces pour découpler des parties de code ou créer une classe mère pour ses ViewModels. Au début cela parait parfois un peut lourd pour pas grand chose, mais le jour où apparait un traitement qui diffère de celui de base ou qu’il faut appliquer systématiquement partout on se félicite d’avoir fait un tel choix !

Faut-il donc systématiquement remplacer DateTime.Now par quelque chose du type de Horloge.Maintenant démontré ici ? Je pense que oui. Je n’ai trouvé aucun argument contre qui tienne longtemps, en revanche j’ai trouvé quelques arguments favorables qui incitent à suivre cette guideline.

J’ai trouvé intéressant de vous soumettre ce petit problème histoire que vous aussi vous dormiez moins bien ce soir en pensant à tout le code que vous avez écrit sans prendre la précaution d’isoler Now  Sourire.

Stay Tuned !

blog comments powered by Disqus