J'écris des cours qui "doivent être utilisés de manière spécifique" (je suppose que tous les cours doivent ...).
Par exemple, je crée la fooManager
classe, ce qui nécessite un appel à, par exemple Initialize(string,string)
. Et, pour pousser l'exemple un peu plus loin, la classe serait inutile si nous n'écoutons pas son ThisHappened
action.
Mon point est que la classe que j'écris nécessite des appels de méthode. Mais il compilera parfaitement si vous n’appelez pas ces méthodes et aboutira à un nouveau FooManager vide. À un moment donné, cela ne fonctionnera pas, ou peut-être tombera en panne, en fonction de la classe et de ce qu’il fait. Le programmeur qui implémente ma classe devrait évidemment regarder à l'intérieur et se rendre compte "Oh, je n'ai pas appelé Initialize!", Et tout irait bien.
Mais je n'aime pas ça. Ce que je voudrais idéalement, c'est que le code ne soit PAS compilé si la méthode n'a pas été appelée; Je suppose que ce n'est tout simplement pas possible. Ou quelque chose qui serait immédiatement visible et clair.
Je me trouve troublé par l'approche actuelle que j'ai ici, qui est la suivante:
Ajoutez une valeur booléenne privée dans la classe et vérifiez si nécessaire que la classe est initialisée. sinon, je vais lancer une exception disant "La classe n'a pas été initialisée, êtes-vous sûr d'appeler .Initialize(string,string)
?".
Je suis un peu d'accord avec cette approche, mais cela conduit à beaucoup de code qui est compilé et, finalement, pas nécessaire pour l'utilisateur final.
En outre, c'est parfois encore plus de code quand il y a plus de méthodes qu'un simple Initiliaze
appel. J'essaie de garder mes cours avec pas trop de méthodes / actions publiques, mais ce n'est pas résoudre le problème, mais le garder raisonnable.
Ce que je cherche ici, c'est:
- Est-ce que mon approche est correcte?
- Y en a t-il un meilleur?
- Que faites-vous / conseillez-vous?
- Est-ce que j'essaie de résoudre un problème qui ne se pose pas? Mes collègues m'ont dit que c'était au programmeur de vérifier le cours avant d'essayer de l'utiliser. Je suis respectueusement en désaccord, mais je pense que c'est un autre problème.
En termes simples, j'essaie de trouver un moyen de ne jamais oublier d'implémenter des appels lorsque cette classe est réutilisée plus tard, ou par quelqu'un d'autre.
CLARIFICATIONS:
Pour clarifier beaucoup de questions ici:
Je ne parle certainement pas seulement de la partie d'initialisation d'une classe, mais plutôt de toute une vie. Empêcher les collègues d'appeler une méthode deux fois, en s'assurant qu'ils appellent X avant Y, etc. Tout ce qui finirait par être obligatoire et dans la documentation, mais que j'aimerais avoir en code et aussi simple et petit que possible. J'ai beaucoup aimé l'idée des assertions, même si je suis sûr que je devrai mélanger d'autres idées, car les assertions ne seront pas toujours possibles.
J'utilise le langage C #! Comment n'ai-je pas mentionné ça?! Je suis dans un environnement Xamarin et je crée des applications mobiles qui utilisent généralement entre 6 et 9 projets dans une solution, notamment des projets PCL, iOS, Android et Windows. Je suis développeur depuis environ un an et demi (études et travail combinés), d’où mes déclarations / questions parfois ridicules. Tout cela n’est probablement pas pertinent ici, mais trop d’informations n’est pas toujours une mauvaise chose.
Je ne peux pas toujours mettre tout ce qui est obligatoire dans le constructeur, en raison des restrictions de plate-forme et de l'utilisation de Dependency Injection, le fait d'avoir des paramètres autres qu'Interfaces n'est pas à la table. Ou peut-être que mes connaissances ne sont pas suffisantes, ce qui est hautement possible. La plupart du temps, ce n'est pas un problème d'initialisation, mais plus
Comment puis-je m'assurer qu'il s'est inscrit à cet événement?
comment puis-je m'assurer qu'il n'a pas oublié d'arrêter le processus à un moment donné?
Ici, je me souviens d’une classe de récupération d’annonces. Tant que la vue dans laquelle l'annonce est visible est visible, la classe récupère une nouvelle annonce toutes les minutes. Cette classe a besoin d’une vue lorsqu’elle est construite où elle peut afficher l’annonce, ce qui pourrait évidemment figurer dans un paramètre. Mais une fois que la vue est partie, StopFetching () doit être appelé. Sinon, la classe continuerait à chercher des annonces pour une vue qui n'y est même pas, et c'est mauvais.
En outre, cette classe a des événements qui doivent être écoutés, comme "AdClicked" par exemple. Tout fonctionne bien s'il n'est pas écouté, mais nous perdons le suivi des analyses si les taps ne sont pas enregistrés. Toutefois, l’annonce fonctionne toujours, de sorte que l’utilisateur et le développeur ne verront aucune différence et que les données analytiques seront erronées. Cela doit être évité, mais je ne suis pas sûr que les développeurs puissent savoir qu'ils doivent s'inscrire à l'événement tao. C’est un exemple simplifié, mais l’idée est là: "assurez-vous qu’il utilise l’Action publique disponible" et au bon moment bien sûr!
initialize
être passé tard après la création de l'objet? Le acteur serait-il trop "risqué" dans le sens où il pourrait jeter des exceptions et briser la chaîne de la création?new
si l'objet n'est pas prêt à être utilisé. S'appuyant sur une méthode d'initialisation reviendra sûrement vous hanter et devrait être évité sauf en cas de nécessité absolue, du moins c'est mon expérience.Réponses:
Dans de tels cas, il est préférable d’utiliser le système de types de votre langue pour vous aider avec une initialisation correcte. Comment pouvons - nous empêcher
FooManager
d'être utilisé sans être initialisé? En empêchant unFooManager
d'être créé sans l'information nécessaire pour initialiser correctement il. En particulier, toute l'initialisation est la responsabilité du constructeur. Vous ne devriez jamais laisser votre constructeur créer un objet dans un état illégal.Mais les appelants doivent créer un
FooManager
avant de pouvoir l'initialiser, par exemple parce que leFooManager
est transmis comme dépendance.Ne créez pas de
FooManager
si vous n'en avez pas. Ce que vous pouvez faire à la place est de passer un objet qui vous permet de récupérer une construction complèteFooManager
, mais uniquement avec les informations d’initialisation. (En langage de programmation fonctionnelle, je vous suggère d'utiliser une application partielle pour le constructeur.) Exemple:Le problème avec ceci est que vous devez fournir les informations init chaque fois que vous accédez à
FooManager
.Si cela est nécessaire dans votre langue, vous pouvez envelopper l'
getFooManager()
opération dans une classe de type usine ou constructeur.Je souhaite vraiment vérifier à l'exécution que la
initialize()
méthode a été appelée plutôt que d'utiliser une solution au niveau du système.Il est possible de trouver un compromis. Nous créons une classe wrapper
MaybeInitializedFooManager
qui a uneget()
méthode qui retourne leFooManager
, mais jette si leFooManager
n'est pas complètement initialisé. Cela ne fonctionne que si l'initialisation est effectuée via l'encapsuleur ou s'il existe uneFooManager#isInitialized()
méthode.Je ne veux pas changer l'API de ma classe.
Dans ce cas, vous voudrez éviter les
if (!initialized) throw;
conditions conditionnelles dans chaque méthode. Heureusement, il existe un modèle simple pour résoudre ce problème.L'objet que vous fournissez aux utilisateurs est simplement un shell vide qui délègue tous les appels à un objet d'implémentation. Par défaut, l'objet implémentation génère une erreur pour chaque méthode qu'il n'a pas été initialisé. Cependant, la
initialize()
méthode remplace l'objet d'implémentation par un objet entièrement construit.Ceci extrait le comportement principal de la classe dans le fichier
MainImpl
.la source
FooManager
and inUninitialisedImpl
soit mieux que de répéterif (!initialized) throw;
.FooManager
a un grand nombre de méthodes, il pourrait être plus facile que potentiellement oublier certains desif (!initialized)
contrôles. Mais dans ce cas, vous devriez probablement préférer diviser la classe.Le moyen le plus efficace et le plus utile d’empêcher les clients d’utiliser "mal" un objet est de le rendre impossible.
La solution la plus simple consiste à fusionner
Initialize
avec le constructeur. De cette façon, l'objet ne sera jamais disponible pour le client dans l'état non initialisé, de sorte que l'erreur n'est pas possible. Si vous ne pouvez pas effectuer l'initialisation dans le constructeur lui-même, vous pouvez créer une méthode fabrique. Par exemple, si votre classe nécessite l'enregistrement de certains événements, vous pouvez exiger l'écouteur d'événements en tant que paramètre dans la signature du constructeur ou de la méthode de fabrique.Si vous devez pouvoir accéder à l'objet non initialisé avant l'initialisation, vous pouvez implémenter les deux états en tant que classes séparées. Vous devez donc commencer avec une
UnitializedFooManager
instance, qui a uneInitialize(...)
méthode qui retourneInitializedFooManager
. Les méthodes qui ne peuvent être appelées que dans l'état initialisé existent uniquementInitializedFooManager
. Cette approche peut être étendue à plusieurs états si vous en avez besoin.Par rapport aux exceptions d’exécution, il est plus fastidieux de représenter les états en tant que classes distinctes, mais cela vous donne également la garantie, lors de la compilation, que vous n’appelez pas de méthodes non valides pour l’état de l’objet, et il documente plus clairement les transitions d’états dans le code.
Mais plus généralement, la solution idéale consiste à concevoir vos classes et méthodes sans contraintes, telles que d’appeler des méthodes à certains moments et dans un certain ordre. Ce n'est peut-être pas toujours possible, mais on peut l'éviter dans de nombreux cas en utilisant un modèle approprié.
Si vous avez un couplage temporel complexe (plusieurs méthodes doivent être appelées dans un certain ordre), une solution pourrait consister à utiliser l' inversion de contrôle . Vous devez donc créer une classe qui appelle les méthodes dans l'ordre approprié, mais utilise des méthodes de modèle ou des événements. pour permettre au client d'effectuer des opérations personnalisées aux étapes appropriées du flux. De cette façon, la responsabilité d'effectuer des opérations dans le bon ordre est transférée du client à la classe elle-même.
Un exemple simple: Vous avez un
File
-objet qui vous permet de lire à partir d’un fichier. Cependant, le client doit appelerOpen
avant que laReadLine
méthode puisse être appelée et doit se rappeler de toujours appelerClose
(même si une exception se produit), après quoi laReadLine
méthode ne doit plus être appelée. C'est un couplage temporel. Cela peut être évité en ayant une seule méthode qui prend un callback ou un délégué en argument. La méthode gère l’ouverture du fichier, l’appel du rappel puis la fermeture du fichier. Il peut passer une interface distincte avec uneRead
méthode au rappel. De cette manière, le client ne peut pas oublier d'appeler des méthodes dans le bon ordre.Interface avec couplage temporel:
Sans couplage temporel:
Une solution plus lourde consisterait à définir en
File
tant que classe abstraite ce qui nécessite d'implémenter une méthode (protégée) qui sera exécutée sur le fichier ouvert. Ceci s'appelle un modèle de méthode de modèle.L'avantage est le même: le client n'a pas à se rappeler d'appeler des méthodes dans un certain ordre. Il est tout simplement impossible d'appeler des méthodes dans le mauvais ordre.
la source
UnitializedFooManager
) ne doit pas partager un état avec les instances (InitializedFooManager
), comme celaInitialize(...)
a été faitInstantiate(...)
sémantique. Sinon, cet exemple entraîne de nouveaux problèmes si le même modèle est utilisé deux fois par un développeur. Et ce n'est pas possible d'empêcher / de valider statiquement si le langage ne prend pas explicitement en charge lamove
sémantique afin de consommer le modèle de manière fiable.Normalement, je vérifierais simplement l’initialisation et jetterais (par exemple) un
IllegalStateException
si vous essayez de l’utiliser sans l’initialiser.Cependant, si vous voulez être sûr de pouvoir vous préparer à la compilation (et que cela soit louable et préférable), pourquoi ne pas traiter l’initialisation comme une méthode usine renvoyant un objet construit et initialisé, par exemple
et ainsi vous contrôlez la création d'objets et de cycle de vie, et vos clients obtenir le
Component
suite de votre initialisation. C'est effectivement une instanciation paresseuse.la source
IllegalStateException
.IllegalStateException
ouInvalidOperationException
à l'improviste, un utilisateur ne comprendra jamais ce qu'il a mal fait. Ces exceptions ne doivent pas devenir une solution de rechange pour corriger le défaut de conception.Je vais me démarquer un peu des autres réponses et me mettre en désaccord: il est impossible de répondre à cette question sans savoir dans quelle langue vous travaillez. Qu'il s'agisse ou non d'un plan valable, et du type d'avertissement à donner vos utilisateurs dépendent entièrement des mécanismes fournis par votre langue et des conventions suivies par les autres développeurs de cette langue.
Si vous avez un
FooManager
fichier dans Haskell, il serait criminel de permettre à vos utilisateurs d'en créer un qui ne soit pas capable de gérer lesFoo
s, car le système de types le rend si facile et ce sont les conventions auxquelles s'attendent les développeurs Haskell.D'autre part, si vous écrivez C, vos collègues de travail seraient tout à fait en droit de vous prendre à l' arrière et vous tirer dessus pour définir différents
struct FooManager
etstruct UninitializedFooManager
types qui prennent en charge les opérations différentes, car cela conduirait à un code inutilement complexe pour très peu d' avantages .Si vous écrivez en Python, il n'y a pas de mécanisme dans le langage pour vous permettre de le faire.
Vous n'écrivez probablement pas Haskell, Python ou C, mais ce sont des exemples illustrant l'ampleur / le peu de travail attendu par le système de types et qu'il est capable de faire.
Suivez les attentes raisonnables d'un développeur dans votre langue et résistez à la tentation de sur-concevoir une solution dont l'implémentation n'est pas naturelle et idiomatique (à moins que l'erreur soit si facile à commettre et si difficile à détecter qu'il vaut la peine d'aller à l'extrême. longueurs pour le rendre impossible). Si vous n'avez pas assez d'expérience dans votre langue pour juger de ce qui est raisonnable, suivez les conseils de quelqu'un qui la connaît mieux que vous.
la source
Comme vous semblez ne pas vouloir envoyer la vérification du code au client (mais semble convenir au programmeur), vous pouvez utiliser des fonctions d' assertion , si elles sont disponibles dans votre langage de programmation.
De cette façon, vous avez les contrôles dans l'environnement de développement (et tous les tests qu'un autre développeur appellerait échoueront de manière prévisible), mais vous n'enverrez pas le code au client car les affirmations (du moins en Java) ne sont compilées que de manière sélective.
Ainsi, une classe Java utilisant ceci ressemblerait à ceci:
Les assertions sont un excellent outil pour vérifier l'état d'exécution de votre programme dont vous avez besoin, mais ne vous attendez pas à ce que quelqu'un agisse de manière erronée dans un environnement réel.
En outre, ils sont plutôt minces et ne prennent pas d'énormes portions de syntaxe if (), ... ils n'ont pas besoin d'être interceptés, etc.
la source
En regardant ce problème plus généralement que ne le font les réponses actuelles, qui se sont principalement concentrées sur l'initialisation. Considérons un objet qui aura deux méthodes,
a()
etb()
. L'exigence est quea()
s'appelle toujours avantb()
. Vous pouvez créer une vérification au moment de la compilation en retournant un nouvel objeta()
et en le déplaçantb()
vers le nouvel objet plutôt que vers l'original. Exemple d'implémentation:Maintenant, il est impossible d'appeler b () sans d'abord appeler a (), car il est implémenté dans une interface masquée jusqu'à l'appel de a (). Certes, cela demande beaucoup d’efforts, je n’utilise donc généralement pas cette approche, mais il existe des situations où cela peut être bénéfique (en particulier si votre classe sera réutilisée dans beaucoup de situations par des programmeurs qui pourraient ne pas l’être. familiarisé avec sa mise en œuvre, ou en code où la fiabilité est critique) Notez également qu'il s'agit d'une généralisation du modèle de générateur, comme suggéré par de nombreuses réponses existantes. Cela fonctionne à peu près de la même manière, la seule différence réelle réside dans le lieu où les données sont stockées (dans l'objet d'origine plutôt que dans l'objet renvoyé) et lorsqu'elles sont destinées à être utilisées (à tout moment par rapport à l'initialisation uniquement).
la source
Lorsque j'implémente une classe de base qui nécessite des informations d'initialisation supplémentaires ou des liens vers d'autres objets avant que cela devienne utile, j'ai tendance à rendre cette classe de base abstraite, puis à définir plusieurs méthodes abstraites de cette classe utilisées dans le flux de la classe de base (comme
abstract Collection<Information> get_extra_information(args);
etabstract Collection<OtherObjects> get_other_objects(args);
qui, par contrat d'héritage, doit être implémenté par la classe concrète, forçant l'utilisateur de cette classe de base à fournir tout le matériel requis par la classe de base.Ainsi, lorsque j'implémente la classe de base, je sais immédiatement et explicitement ce que je dois écrire pour que la classe de base se comporte correctement, car il me suffit d'implémenter les méthodes abstraites et c'est tout.
EDIT: pour clarifier, cela revient presque à fournir des arguments au constructeur de la classe de base, mais les implémentations de méthodes abstraites permettent à l’implémentation de traiter les arguments transmis aux appels de méthodes abstraites. Ou même dans le cas où aucun argument n'est utilisé, il peut toujours être utile si la valeur de retour de la méthode abstraite dépend d'un état que vous pouvez définir dans le corps de la méthode, ce qui n'est pas possible lorsque vous transmettez la variable en tant qu'argument le constructeur. Cela dit, vous pouvez toujours passer un argument qui aura un comportement dynamique basé sur le même principe si vous préférez utiliser la composition plutôt que l'héritage.
la source
La réponse est: oui, non et parfois. :-)
Certains des problèmes que vous décrivez sont facilement résolus, du moins dans de nombreux cas.
La vérification au moment de la compilation est certainement préférable à la vérification au moment de l'exécution, qui est préférable à l'absence de vérification du tout.
RE run-time:
Il est au moins simple sur le plan conceptuel de forcer les fonctions à être appelées dans un certain ordre, etc., avec des vérifications à l'exécution. Il suffit de mettre des indicateurs qui indiquent quelle fonction a été exécutée et que chaque fonction commence par quelque chose du type "sinon pré-condition-1 ou non pré-condition-2 puis levée exception". Si les contrôles deviennent complexes, vous pouvez les insérer dans une fonction privée.
Vous pouvez faire en sorte que certaines choses se produisent automatiquement. Pour prendre un exemple simple, les programmeurs parlent souvent de "population paresseuse" d'un objet. Lorsque l'instance est créée, vous définissez un indicateur rempli par false, ou une référence d'objet clé par la valeur null. Ensuite, lorsqu'une fonction nécessitant les données pertinentes est appelée, elle vérifie l'indicateur. S'il est faux, les données sont renseignées et le drapeau est défini sur true. Si c'est vrai, on suppose que les données sont là. Vous pouvez faire la même chose avec d'autres conditions préalables. Définissez un indicateur dans le constructeur ou comme valeur par défaut. Puis, lorsque vous atteignez un point où une fonction de pré-condition aurait dû être appelée, si elle ne l’a pas encore été, appelez-la. Bien sûr, cela ne fonctionne que si vous avez les données pour l'appeler à ce moment-là, mais dans de nombreux cas, vous pouvez inclure les données requises dans les paramètres de fonction du "
RE temps de compilation:
Comme d’autres l’ont dit, si vous devez initialiser un objet avant de pouvoir l’utiliser, placez l’initialisation dans le constructeur. C'est un conseil plutôt typique pour la programmation orientée objet. Si possible, demandez au constructeur de créer une instance valide et utilisable de l'objet.
Je vois des discussions sur Que faire si vous devez transmettre des références à l'objet avant qu'il ne soit initialisé. Je ne peux pas penser à un exemple concret de cela, mais je suppose que cela pourrait arriver. Des solutions ont été suggérées, mais les solutions que j'ai vues ici et celles auxquelles je peux penser sont compliquées et compliquent le code. À un moment donné, vous devez vous demander: est-il utile de créer un code laid et difficile à comprendre afin que nous puissions avoir une vérification au moment de la compilation plutôt qu'une vérification à l'exécution? Ou est-ce que je crée beaucoup de travail pour moi-même et pour les autres dans un but agréable mais non requis?
Si deux fonctions doivent toujours être exécutées ensemble, la solution simple consiste à n'en faire qu'une. Comme si chaque fois que vous ajoutez une publication à une page, vous devez également ajouter un gestionnaire de métriques pour cette page, puis placez le code permettant d'ajouter le gestionnaire de métriques à l'intérieur de la fonction "ajouter une publication".
Certaines choses seraient presque impossibles à vérifier au moment de la compilation. Comme si on exigeait qu'une certaine fonction ne puisse être appelée plus d'une fois. (J'ai écrit de nombreuses fonctions de ce type. Je viens tout juste d'écrire une fonction qui recherche les fichiers dans un répertoire magique, traite ceux qu'ils trouvent, puis les supprime. Bien sûr, une fois le fichier supprimé, il est impossible de l'exécuter à nouveau. Etc.) Je ne connais pas de langage qui possède une fonctionnalité qui vous permet d'empêcher une fonction d'être appelée deux fois sur la même instance lors de la compilation. Je ne sais pas comment le compilateur pourrait définitivement comprendre que cela se produisait. Tout ce que vous pouvez faire est de mettre en place des vérifications de temps d'exécution. Cette vérification du temps d'exécution est évidente, n'est-ce pas? msgstr "si déjà-été-ici = true, alors lève une exception, sinon, set-been-here = true".
RE impossible à appliquer
Il n'est pas rare pour une classe d'exiger une sorte de nettoyage lorsque vous avez terminé: fermeture de fichiers ou de connexions, libération de mémoire, écriture des résultats finaux dans la base de données, etc. Il n'y a pas de moyen facile d'imposer cela à la compilation ou le temps d'exécution. Je pense que la plupart des langages POO ont une sorte de disposition pour une fonction de "finaliseur" qui est appelée quand une instance est collectée, mais je pense que la plupart disent aussi qu'ils ne peuvent pas garantir que cela sera jamais exécuté. Les langages POO incluent souvent une fonction "dispose" avec une sorte de disposition pour définir une portée sur l'utilisation d'une instance, une clause "utiliser" ou "avec", et lorsque le programme quitte cette portée, la disposition est exécutée. Mais cela nécessite que le programmeur utilise le spécificateur de portée approprié. Cela ne force pas le programmeur à le faire correctement,
Dans les cas où il est impossible de forcer le programmeur à utiliser la classe correctement, j'essaie de faire en sorte que le programme explose s'il ne le fait pas correctement, plutôt que de donner des résultats incorrects. Exemple simple: j'ai vu de nombreux programmes initialiser une série de données sur des valeurs factices afin que l'utilisateur ne reçoive jamais d'exception null-pointeur même s'il ne parvient pas à appeler les fonctions pour remplir correctement les données. Et je me demande toujours pourquoi. Vous vous efforcez de rendre les bugs plus difficiles à trouver. Si, lorsqu'un programmeur ne parvenait pas à appeler la fonction "charger les données" et essayait ensuite d'alléguer les données, il obtenait une exception de pointeur null, celle-ci lui indiquait rapidement où se trouvait le problème. Mais si vous le masquez en rendant les données vides ou nulles dans de tels cas, le programme peut s'exécuter complètement mais produit des résultats inexacts. Dans certains cas, le programmeur peut même ne pas se rendre compte de l'existence d'un problème. S'il le remarque, il devra peut-être suivre une longue piste pour trouver où il s'est trompé. Mieux vaut échouer tôt que d'essayer de continuer courageusement.
En général, bien sûr, il est toujours bon de rendre impossible l’utilisation incorrecte d’un objet. C'est bien si les fonctions sont structurées de telle sorte qu'il n'y a tout simplement aucun moyen pour le programmeur de faire des appels non valides, ou si des appels non valides génèrent des erreurs de compilation.
Mais il y a aussi un moment où vous devez dire, Le programmeur devrait lire la documentation sur la fonction.
la source
Oui, votre instinct est parfait. Il y a de nombreuses années, j'ai écrit des articles dans des magazines (un peu comme ici, mais sur un arbre mort et publiés une fois par mois) sur le débogage , l' abuging et l' antibiothérapie .
Si vous pouvez obtenir que le compilateur attrape le mauvais usage, c'est le meilleur moyen. Les fonctionnalités de langue disponibles dépendent de la langue et vous ne l'avez pas spécifiée. Des techniques spécifiques seraient quand même détaillées pour des questions individuelles sur ce site.
Mais avoir un test pour détecter l'utilisation au moment de la compilation au lieu de l'exécution est vraiment le même type de test: vous devez toujours savoir appeler d'abord les fonctions appropriées et les utiliser de la bonne manière.
Concevoir le composant pour éviter que cela ne soit un problème au départ est beaucoup plus subtile, et j'aime bien que Buffer ressemble à l' exemple de l'élément . La partie 4 (publiée il y a vingt ans! Wow!) Contient un exemple de discussion assez similaire à votre cas: Cette classe doit être utilisée avec précaution, car tampon () ne peut pas être appelé après avoir commencé à utiliser read (). Au contraire, il ne doit être utilisé qu'une seule fois, immédiatement après la construction. Demander à la classe de gérer le tampon automatiquement et de manière invisible pour l’appelant éviterait le problème de manière conceptuelle.
Vous demandez si des tests peuvent être effectués au moment de l'exécution pour garantir une utilisation correcte, devez-vous le faire?
Je dirais oui . Cela économisera beaucoup de travail de débogage à votre équipe un jour, lorsque le code sera maintenu et que l'utilisation spécifique sera perturbée. Le test peut être configuré comme un ensemble formel de contraintes et d' invariants pouvant être testés. Cela ne signifie pas que l'espace supplémentaire nécessaire pour suivre l'état et le travail de vérification sont toujours laissés à chaque appel. Il est dans la classe pour qu'il puisse être appelé et le code documente les contraintes et les hypothèses réelles.
Peut-être que la vérification n’est effectuée que dans une version de débogage.
La vérification est peut-être complexe (pensez à heapcheck, par exemple), mais elle est présente et peut être effectuée de temps en temps, ou des appels sont ajoutés ici et là lorsque le code est en cours d’élaboration et qu'un problème est apparu, ou pour effectuer des tests spécifiques. Assurez-vous qu'aucun problème de ce type ne se produit dans un programme de tests unitaires ou d' intégration liant la même classe.
Vous pouvez décider que les frais généraux sont minimes après tout. Si la classe effectue un fichier d'E / S, un octet d'état supplémentaire et un test de ce type ne servent à rien. Les classes communes iostream vérifient si le flux est en mauvais état.
Vos instincts sont bons. Continuez!
la source
Le principe de masquage des informations (encapsulation) est que les entités extérieures à la classe ne doivent pas savoir plus que ce qui est nécessaire pour utiliser cette classe correctement.
Votre cas semble indiquer que vous essayez de faire fonctionner correctement un objet d'une classe sans en dire assez aux utilisateurs extérieurs. Vous avez caché plus d'informations que vous n'auriez dû.
Paroles de sagesse:
* Dans tous les cas, vous devez redéfinir la classe et / ou le constructeur et / ou les méthodes.
* En ne concevant pas correctement la classe et en privant la classe d'informations correctes, vous risquez que votre classe ne se sépare pas à un, mais potentiellement à plusieurs endroits.
* Si vous avez mal écrit les exceptions et les messages d'erreur, votre classe en dévoilera encore plus sur elle-même.
* Enfin, si l’étranger est censé savoir qu’il doit initialiser a, b et c, puis appeler d (), e (), f (int), il y a une fuite dans votre abstraction.
la source
Puisque vous avez spécifié que vous utilisiez C #, une solution viable à votre situation consiste à utiliser les analyseurs de code Roslyn . Cela vous permettra de détecter immédiatement les violations et même de suggérer des corrections de code .
Une façon de le mettre en œuvre serait de décorer les méthodes couplées temporellement avec un attribut spécifiant l'ordre dans lequel les méthodes doivent être appelées 1 . Lorsque l'analyseur trouve ces attributs dans une classe, il valide que les méthodes sont appelées dans l'ordre. Cela ferait ressembler votre classe à quelque chose comme ce qui suit:
1: Je n’ai jamais écrit d’analyseur de code Roslyn, ce n’est donc peut-être pas la meilleure implémentation Cependant, l'idée d'utiliser Roslyn Code Analyzer pour vérifier que votre API est utilisée correctement est à 100% valable.
la source
La façon dont j'ai résolu ce problème auparavant était avec un constructeur privé et une
MakeFooManager()
méthode statique dans la classe. Exemple:Étant donné qu'un constructeur est implémenté, il est impossible pour quiconque de créer une instance de
FooManager
sans passer parMakeFooManager()
.la source
Il y a plusieurs approches:
Faites en sorte que la classe soit facile à utiliser correctement et difficile à utiliser mal. Certaines des autres réponses se concentrent exclusivement sur ce point, je vais donc simplement mentionner RAII et le modèle de constructeur .
Soyez conscient de la façon d'utiliser les commentaires. Si la classe s'explique d'elle-même, n'écrivez pas de commentaires *. Ainsi, les gens seront plus susceptibles de lire vos commentaires lorsque la classe en a réellement besoin. Et si vous rencontrez l'un de ces rares cas où une classe est difficile à utiliser correctement et a besoin de commentaires, incluez des exemples.
Utilisez des assertions dans vos versions de débogage.
Lance des exceptions si l'utilisation de la classe n'est pas valide.
* Voici le genre de commentaires que vous ne devriez pas écrire:
la source