Si vous utilisez le mot clé «statique» sans le mot clé «final», cela devrait être un signal pour examiner attentivement votre conception. Même la présence d'un «final» n'est pas un laissez-passer, puisqu'un objet final statique mutable peut être tout aussi dangereux.
J'évaluerais quelque part environ 85% du temps où je vois un «statique» sans un «final», c'est FAUX. Souvent, je trouverai d'étranges solutions de contournement pour masquer ou masquer ces problèmes.
Veuillez ne pas créer de mutables statiques. Surtout les collections. En général, les collections doivent être initialisées lorsque leur objet conteneur est initialisé et doivent être conçues de manière à être réinitialisées ou oubliées lorsque leur objet conteneur est oublié.
L'utilisation de la statique peut créer des bogues très subtils qui causeront des douleurs aux ingénieurs. Je sais, parce que j'ai à la fois créé et chassé ces bugs.
Si vous souhaitez plus de détails, lisez la suite…
Pourquoi ne pas utiliser la statique?
Il existe de nombreux problèmes avec la statique, y compris l'écriture et l'exécution de tests, ainsi que des bogues subtils qui ne sont pas immédiatement évidents.
Le code qui repose sur des objets statiques ne peut pas être facilement testé à l'unité, et les statistiques ne peuvent pas être facilement moquées (généralement).
Si vous utilisez de la statique, il n'est pas possible d'échanger l'implémentation de la classe afin de tester des composants de niveau supérieur. Par exemple, imaginez un CustomerDAO statique qui renvoie les objets Customer qu'il charge à partir de la base de données. J'ai maintenant une classe CustomerFilter, qui doit accéder à certains objets Customer. Si CustomerDAO est statique, je ne peux pas écrire de test pour CustomerFilter sans d'abord initialiser ma base de données et remplir des informations utiles.
Et le remplissage et l'initialisation de la base de données prennent beaucoup de temps. Et d'après mon expérience, votre cadre d'initialisation de base de données changera au fil du temps, ce qui signifie que les données se transformeront et que les tests pourraient se casser. IE, imaginez que le client 1 était un VIP, mais le cadre d'initialisation de la base de données a changé, et maintenant le client 1 n'est plus VIP, mais votre test a été codé en dur pour charger le client 1…
Une meilleure approche consiste à instancier un CustomerDAO et à le passer dans le CustomerFilter lors de sa construction. (Une approche encore meilleure consisterait à utiliser Spring ou un autre cadre d'inversion de contrôle.
Une fois cette opération effectuée, vous pouvez rapidement simuler ou supprimer un autre DAO dans votre CustomerFilterTest, ce qui vous permet d'avoir plus de contrôle sur le test,
Sans le DAO statique, le test sera plus rapide (pas d'initialisation db) et plus fiable (car il n'échouera pas lorsque le code d'initialisation db change). Par exemple, dans ce cas, s'assurer que le client 1 est et sera toujours un VIP, en ce qui concerne le test.
Exécution de tests
La statique pose un vrai problème lors de l'exécution simultanée de suites de tests unitaires (par exemple, avec votre serveur d'intégration continue). Imaginez une carte statique d'objets Socket réseau qui reste ouverte d'un test à l'autre. Le premier test peut ouvrir un socket sur le port 8080, mais vous avez oublié de vider la carte lorsque le test est détruit. Désormais, lorsqu'un deuxième test se lance, il risque de se bloquer lorsqu'il essaie de créer un nouveau socket pour le port 8080, car le port est toujours occupé. Imaginez également que les références de socket dans votre collection statique ne soient pas supprimées et (à l'exception de WeakHashMap) ne soient jamais éligibles pour être récupérées, provoquant une fuite de mémoire.
Il s'agit d'un exemple trop généralisé, mais dans les grands systèmes, ce problème se produit tout le temps. Les gens ne pensent pas aux tests unitaires démarrant et arrêtant leur logiciel à plusieurs reprises dans la même JVM, mais c'est un bon test de la conception de votre logiciel, et si vous avez des aspirations à une haute disponibilité, c'est quelque chose dont vous devez être conscient.
Ces problèmes surviennent souvent avec les objets d'infrastructure, par exemple, vos couches d'accès à la base de données, de mise en cache, de messagerie et de journalisation. Si vous utilisez Java EE ou certains des meilleurs frameworks, ils gèrent probablement beaucoup de cela pour vous, mais si comme moi vous avez affaire à un système hérité, vous pourriez avoir beaucoup de frameworks personnalisés pour accéder à ces couches.
Si la configuration système qui s'applique à ces composants de structure change entre les tests unitaires et que la structure de test unitaire ne détruit pas et ne reconstruit pas les composants, ces modifications ne peuvent pas prendre effet et lorsqu'un test s'appuie sur ces modifications, elles échouent .
Même les composants non-framework sont sujets à ce problème. Imaginez une carte statique appelée OpenOrders. Vous écrivez un test qui crée quelques commandes ouvertes, et vous vérifiez qu'elles sont toutes dans le bon état, puis le test se termine. Un autre développeur écrit un deuxième test qui place les commandes dont il a besoin dans la carte OpenOrders, puis affirme que le nombre de commandes est précis. Exécutés individuellement, ces tests réussiraient tous les deux, mais lorsqu'ils s'exécuteraient ensemble dans une suite, ils échoueraient.
Pire, l'échec peut être basé sur l'ordre dans lequel les tests ont été exécutés.
Dans ce cas, en évitant la statique, vous évitez le risque de persister les données entre les instances de test, garantissant ainsi une meilleure fiabilité des tests.
Bugs subtils
Si vous travaillez dans un environnement à haute disponibilité, ou n'importe où où les threads peuvent être démarrés et arrêtés, le même problème mentionné ci-dessus avec les suites de tests unitaires peut également s'appliquer lorsque votre code s'exécute en production.
Lorsqu'il s'agit de threads, plutôt que d'utiliser un objet statique pour stocker des données, il est préférable d'utiliser un objet initialisé pendant la phase de démarrage du thread. De cette façon, chaque fois que le thread est démarré, une nouvelle instance de l'objet (avec une configuration potentiellement nouvelle) est créée et vous évitez que les données d'une instance du thread ne saignent jusqu'à l'instance suivante.
Lorsqu'un thread meurt, un objet statique n'est pas réinitialisé ni récupéré. Imaginez que vous ayez un thread appelé «EmailCustomers», et quand il démarre, il remplit une collection de chaînes statiques avec une liste d'adresses e-mail, puis commence à envoyer par e-mail chacune des adresses. Supposons que le thread soit interrompu ou annulé d'une manière ou d'une autre, de sorte que votre infrastructure à haute disponibilité redémarre le thread. Ensuite, lorsque le thread démarre, il recharge la liste des clients. Mais comme la collection est statique, elle peut conserver la liste des adresses e-mail de la collection précédente. Maintenant, certains clients peuvent recevoir des e-mails en double.
An Side: finale statique
L'utilisation de «final statique» est effectivement l'équivalent Java d'un C #define, bien qu'il existe des différences de mise en œuvre technique. AC / C ++ #define est échangé hors du code par le pré-processeur, avant la compilation. Une «finale statique» Java terminera la mémoire résidente sur la pile. De cette façon, elle est plus semblable à une variable «const statique» en C ++ qu'à une #define.
Sommaire
J'espère que cela aide à expliquer quelques raisons fondamentales pour lesquelles la statique est problématique. Si vous utilisez un framework Java moderne comme Java EE ou Spring, etc., vous ne rencontrerez peut-être pas beaucoup de ces situations, mais si vous travaillez avec un grand corps de code hérité, elles peuvent devenir beaucoup plus fréquentes.