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 !

Faites des heureux, PARTAGEZ l'article !

Commentaires (13) -

  • Saïd

    19/06/2014 19:34:50 | Répondre

    Autant signaler que ça peut s'appliquer à d'autres langages ...

    Smile

    • Olivier

      20/06/2014 20:32:53 | Répondre

      Tous les langages .NET sous concernés bien entendu...

  • Olivier

    20/06/2014 20:44:56 | Répondre

    MS Fake est une solution plus lourde que la simple petite classe montrée ici qui fonctionne à volonté sans toucher le code.
    Je pense que MS Fake n'est pas étudié pour ce gfenre de choses, d'autant que la solution de la classe permet aussi des choses utiles même en production (faire avancer ou retarder l'heure, on peut supposer que ça puisse servir).

    • Krimog

      17/07/2014 18:18:07 | Répondre

      MS Fakes est plus lourd, c'est clair. Ce n'est pas adapté pour des choses en production, c'est évident.

      Je ne suis en revanche pas d'accord quand tu dis que ce n'est pas étudié pour ce genre de choses. Pour moi, j'ai l'impression que c'est justement la cible principale de MS Fakes. Il suffit d'ailleurs de regarder la MSDN (msdn.microsoft.com/en-us/library/hh549175.aspx) pour voir qu'ils prennent justement comme exemple la surcharge de DateTime.Now !
      De plus, il suffit que tu utilises une DLL faite par un tiers qui utilise DateTime et tout ton principe de classe Horloge tombe à l'eau.

      PS : Voilà plusieurs messages où je donne un avis différent du tien, j'espère que tu ne prends pas ça pour du troll. Je viens de découvrir ton blog et il est globalement génial. Bravo.

      • Olivier

        18/07/2014 20:22:23 | Répondre

        Tous les avis sont intéressants et discuter n'est justement pas forcément troller.
        MS Fake reste pour moi une solution trop lourde juste pour le problème du DateTime.Now.
        Pire, MS Fake est une instrumentalisation du code en vue de le tester, comme les frameworks de Mock.
        Ici on règle un problème d'implémentation, on ne teste rien.
        Ce serait un changement de destination ayant d'éventuelles conséquences graves que d'utiliser MS Fake juste pour régler un problème d'implémentation qui n'a rien à voir avec les tests du code.
        Quant à l'exemple du mauvais code extérieur qui écroulerait la logique de la correction proposée... Tu es un peu de mauvaise foi Smile L'argument est vraiment spécieux. C'est un cas très rare. Et si tu n'as pas accès au code source et que la librairie est foireuse et bien il ne faut pas l'utiliser Smile
        Sincèrement je reste sur ma position qui me semble plus cohérente.
        Mais tu fais comme tu veux dans ton code, je ne suis pas un censeur. Mais que je ne vienne pas faire un audit chez toi c'est le genre de truc que je relève facilement en sabrant à mort ! (je plaisante, j'y mets toujours les formes pour ne vexer personne Wink )

  • Phobias

    23/06/2014 13:08:29 | Répondre

    Bonjour Olivier,

    Aucun doute quand au fait que j'utiliserai régulièrement cette astuce dans mes prochains projets.

    Sa simplicité d'utilisation en fait une classe que chaque développeur devrait avoir dans son projet Tools.

    Merci

    • Olivier

      25/06/2014 00:21:04 | Répondre

      Sers-toi c'est là pour ça Smile

  • Jacques Narcisse

    25/06/2014 13:00:02 | Répondre

    Toujours aussi intéressant! Merci! Mais une question? Que fait-on pour être comme toi! Just for fun!!!!

    • Olivier

      27/06/2014 07:36:51 | Répondre

      Quand on aime on ne compte pas... Smile

  • Rémi Aubert

    20/02/2015 20:48:18 | Répondre

    Je suis pas forcément d'accord avec la faco' de prendre le problème et la solution de bannir le Datetime.Now. Si la clôture comptable doit se faire à une heure précise et qu'il serait débile en effet de le faire en temps réel. Pourquoi ne pas simplement prendre le problème à l'envers ? Laisser l'utilisation de Datetime.Now et simplement rendre paramétrable dans un fichier de configuration l'heure de la clôture comptable. Sur aucun de mes projets je n'ai rencontrer de problème à l'utilisation de Date time le reste des dates ou Time San comparés avec celui-ci étant paramétrables.

    • Olivier

      21/02/2015 17:50:30 | Répondre

      S'il s'agissait juste de rendre paramétrable la date/heure d'une action dans un logiciel il y a en effet juste à gérer des paramètres...

      Mais ici je mets en lumière des problèmes beaucoup plus vastes qui concernent tous les appels à datetime.now dans une application et non un cas particulier.

      Mais pour revenir à ton exemple, quand tu testes le logiciel, si l'opération doit se passer à 18h00, tu fais comment ? Tu vas créer un fichier de config inutile (donc ajouter du code et des bugs et de la maintenance future) puis changer le fichier de configuration et tu mets 15:35 (exemple) parce que tu veux que ça se passe maintenant, c'est ça ?
      Du coup tu ne testes plus ton logiciel en condition normale mais dans un contexte exceptionnel, celui du test.

      Avec la classe Horloge, c'est à elle que tu fais croire qu'il est 18:00, et tu testes ton logiciels ET ses éventuels paramètres dans leurs contexte réel.
      L'ajout d'une gestion de paramètres juste pour ça est d'ailleurs inutile. Moins de code, moins de bugs, moins de maintenance.

      On peut voir aussi les choses autrement, imaginons que tu as un traitement qui doit se passer toutes les heures. Disons qu'il faut environ 10 tests pour s'assurer que ça marche. Donc 10 heures d'attente.
      Tu le gères comment avec ton fichier paramètre ? là ça ne marche plus...
      Alors que fournir une fonction lambda à Horloge qui fait avancer fictivement le temps d'une heure toutes les 10 secondes par exemple fera que ton test ne réclamera plus que 100 minutes au lieu de 600...

      Ce type de besoin n'est pas couvert par l'utilisation directe de datetime.now, et on sait qu'en pratique ce type de code est donc testé en production... En adoptant une classe de type Horloge tous ces tests peuvent avoir lieu dans un temps raisonnable tout en laissant le logiciel dans son état normal sans avoir à le bricoler pour les tests (ce qui est un mauvais réflexe, car en bricolant pour les tests on ne teste plus le "vrai" code mais un code bricolé qui sera rebricolé pour la production, une hérésie donc).

      • Olivier

        21/02/2015 17:53:46 | Répondre

        Lire "ne réclamera plus que 100 secondes"
        et non "minutes".

        Donc 100 secondes au lieu de 10hx60minx60sec = 36000 secondes...

Ajouter un commentaire