web-dev-qa-db-fra.com

Dans quelle mesure est-il nécessaire de suivre des pratiques de programmation défensive pour du code qui ne sera jamais rendu public?

J'écris une implémentation Java d'un jeu de cartes, j'ai donc créé un type spécial de collection que j'appelle une zone. Toutes les méthodes de modification de la collection Java ne sont pas prises en charge, mais il y a une méthode dans l'API de zone, move(Zone, Card), qui déplace une carte de la zone donnée vers elle-même (accomplie par des techniques de paquetage privé). De cette façon, je peux m'assurer qu'aucune carte n'est retirée d'une zone et simplement disparaître; ils ne peuvent être déplacés que dans une autre zone.

Ma question est la suivante: dans quelle mesure ce type de codage défensif est-il nécessaire? C'est "correct", et cela ressemble à la bonne pratique, mais ce n'est pas comme si l'API de zone allait faire partie d'une bibliothèque publique. C'est juste pour moi, donc c'est un peu comme si je protégeais mon code de moi-même alors que je pourrais probablement être plus efficace en utilisant simplement des collections standard.

Jusqu'où dois-je prendre cette idée de zone? Quelqu'un peut-il me donner des conseils sur la façon dont je devrais penser à préserver les contrats dans les classes que j'écris, en particulier pour ceux qui ne seront pas vraiment accessibles au public?

45
codebreaker

Je ne vais pas aborder le problème de conception - juste la question de savoir si faire les choses "correctement" dans une API non publique.

c'est juste pour moi, donc c'est un peu comme si je protégeais mon propre code de moi-même

C'est exactement le point. Peut-être y a-t-il des codeurs qui se souviennent des nuances de chaque classe et méthode qu'ils ont jamais écrites et ne les appellent jamais par erreur avec le mauvais contrat. Je n'en fais pas partie. J'oublie souvent comment le code que j'ai écrit est censé fonctionner quelques heures après l'avoir écrit. Une fois que vous pensez avoir bien compris, votre esprit aura tendance à changer de vitesse pour le problème sur lequel vous travaillez maintenant.

Vous avez des outils pour lutter contre cela. Ces outils incluent (sans ordre particulier) les conventions, les tests unitaires et autres tests automatisés, la vérification des conditions préalables et la documentation. J'ai moi-même trouvé que les tests unitaires étaient inestimables car ils vous obligent à la fois à réfléchir à la façon dont votre contrat sera utilisé et à fournir plus tard de la documentation sur la conception de l'interface.

72
Michael K

J'ai l'habitude de suivre quelques règles simples:

  • Essayez de toujours programmer par contrat.
  • Si une méthode est accessible au public ou reçoit des contributions de le monde extérieur, appliquez des mesures défensives (par exemple IllegalArgumentException).
  • Pour tout ce qui n'est accessible qu'en interne, utilisez des assertions (par exemple assert input != null).

Si un client est vraiment dedans, il trouvera toujours un moyen de faire mal se comporter votre code. Ils peuvent toujours le faire par réflexion, au moins. Mais c'est la beauté de conception par contrat. Vous n'approuvez pas une telle utilisation de votre code et vous ne pouvez donc pas garantir qu'il fonctionnera dans de tels scénarios.

En ce qui concerne votre cas spécifique, si Zone n'est pas censé être utilisé et/ou accessible par des tiers, rendez la classe package-private (et éventuellement final), ou de préférence, utilisez le collections Java vous fournit déjà. Ils sont testés, et vous n'avez pas à réinventer la roue. Notez que cela ne vous empêche pas d'utiliser des assertions dans votre code pour vous assurer que tout fonctionne comme prévu.

25
afsantos

La programmation défensive est une très bonne chose.
Jusqu'à ce qu'il commence à gêner l'écriture de code. Alors ce n'est pas une si bonne chose.


Parlant un peu plus pragmatique ...

On dirait que vous êtes sur le point d'aller trop loin. Le défi (et la réponse à votre question) réside dans la compréhension des règles commerciales ou des exigences du programme.

En utilisant votre API de jeu de cartes à titre d'exemple, il existe des environnements où tout ce qui peut être fait pour empêcher la tricherie est critique. De grandes quantités d'argent réel peuvent être impliquées, il est donc logique de mettre en place un grand nombre de contrôles pour éviter toute fraude.

D'un autre côté, vous devez garder à l'esprit les principes SOLID, en particulier la responsabilité unique. Demander à la classe de conteneur de vérifier efficacement où vont les cartes peut être un peu trop. Il peut être préférable de avoir une couche d'audit/contrôleur entre le conteneur de cartes et la fonction qui reçoit les demandes de déplacement.


En rapport avec ces préoccupations, vous devez comprendre quels composants de votre API sont exposés publiquement (et donc vulnérables) par rapport à ce qui est privé et moins exposé. Je ne suis pas un fervent partisan d'un "revêtement extérieur dur avec un intérieur doux", mais le meilleur retour de votre effort est de durcir l'extérieur de votre API.

Je ne pense pas que l'utilisateur final prévu d'une bibliothèque soit aussi critique quant à la détermination de la quantité de programmation défensive que vous mettez en place. Même avec les modules que j'écris pour mon usage personnel, j'ai quand même mis en place une mesure de vérification pour m'assurer que l'avenir ne me fasse pas d'erreur par inadvertance en appelant la bibliothèque.

16
user53019

Le codage défensif n'est pas seulement une bonne idée pour le code public. C'est une excellente idée pour tout code qui n'est pas immédiatement jeté. Bien sûr, vous savez comment il est censé s'appeler maintenant, mais vous ne savez pas dans quelle mesure vous vous souviendrez de ces six mois à partir de maintenant lorsque vous reviendrez au projet.

La syntaxe de base de Java vous donne beaucoup de défense intégrée par rapport à un langage de niveau inférieur ou interprété comme C ou Javascript respectivement. En supposant que vous nommez clairement vos méthodes et que vous n'avez pas de "séquençage de méthodes" externe, vous pouvez probablement vous en sortir en spécifiant simplement les arguments comme un type de données correct et en incluant un comportement sensible si les données correctement saisies peuvent toujours être invalides.

(À part, si les cartes doivent toujours être dans la zone, je pense que vous obtenez un meilleur rapport qualité-prix en faisant référencer toutes les cartes en jeu par une collection globale à votre objet de jeu, et que la zone soit une propriété de chaque carte. Mais comme je ne sais pas ce que font vos zones en dehors de la possession de cartes, il est difficile de savoir si c'est approprié.)

13
DougM

Créez d'abord une classe qui conserve une liste de zones afin de ne pas perdre une zone ou les cartes qu'elle contient. Vous pouvez ensuite vérifier qu'un transfert se trouve dans votre ZoneList. Cette classe sera probablement une sorte de singleton, car vous n'aurez besoin que d'une seule instance, mais vous voudrez peut-être des ensembles de zones plus tard, alors gardez vos options ouvertes.

Deuxièmement, ne disposez pas de Collection d'implémentation Zone ou ZoneList ou autre chose, sauf si vous vous attendez à en avoir besoin. Autrement dit, si une Zone ou ZoneList est passée à quelque chose qui attend une Collection, alors l'implémente. Vous pouvez désactiver un tas de méthodes en leur faisant lever une exception (UnimplementedException, ou quelque chose comme ça) ou en les faisant simplement ne rien faire. (Réfléchissez bien avant d'utiliser la deuxième option. Si vous le faites parce que c'est facile, vous constaterez qu'il vous manque des bogues que vous auriez pu détecter tôt.)

Il y a de vraies questions sur ce qui est "correct". Mais une fois que vous aurez compris ce que c'est, vous voudrez faire les choses de cette façon. En deux ans, vous aurez oublié tout cela, et si vous essayez d'utiliser le code, vous serez vraiment ennuyé par le gars qui l'a écrit de manière si contre-intuitive et n'a rien expliqué.

1
RalphChapin

Le codage défensif dans la conception d'API consiste généralement à valider les entrées et à sélectionner soigneusement un mécanisme de gestion des erreurs approprié. Les choses que d'autres réponses mentionnent méritent également d'être notées.

Ce n'est en fait pas le sujet de votre exemple. Vous êtes là en train de limiter votre surface API, pour une raison bien précise. Comme GlenH7 le mentionne, lorsque le jeu de cartes doit être utilisé dans un jeu réel, avec un jeu (`` utilisé '' et `` non utilisé ''), une table et des mains par exemple, vous voulez vraiment mettre vérifie que chaque carte du jeu est présente une seule fois.

Que vous ayez conçu ceci avec des "zones", est un choix arbitraire. Selon la mise en œuvre (une zone ne peut être qu'une main, un deck ou une table dans l'exemple ci-dessus), il pourrait très bien s'agir d'une conception approfondie.

Cependant, cette implémentation ressemble à un type dérivé d'un jeu de cartes plus semblable à Collection<Card>, Avec une API moins restrictive. Par exemple, lorsque vous voulez construire une calculatrice de valeur de main, ou une IA, vous voulez sûrement être libre de choisir laquelle et combien de chaque carte sur laquelle vous répétez.

Il est donc bon d'exposer une API aussi restrictive, si le seul objectif de cette API est de s'assurer que chaque carte est toujours dans une zone.

1
CodeCaster