Question

Je voudrais savoir quelle technique vous utilisez pour valider l'état interne d'un objet lors d'une opération qui, de son point de vue, ne peut échouer qu'en raison d'un mauvais état interne ou d'une violation invariante.

Mon objectif principal est le C ++, car en C #, la méthode officielle consiste à lever une exception. En C ++, il n’existe pas une seule seule manière de procéder (ok, pas vraiment en C #). soit, je le sais).

Notez que je ne parle pas de la validation des paramètres de fonction, mais plutôt des contrôles d'intégrité invariants de classe.

Par exemple, supposons que nous voulions un objet Printer pour Mettre en file d'attente un travail d'impression de manière asynchrone. Pour l'utilisateur de Imprimante , cette opération ne peut aboutir, car une file d'attente asynchrone aboutit à un autre moment. Donc, il n'y a pas de code d'erreur pertinent à transmettre à l'appelant.

Mais pour l’objet Imprimante , cette opération peut échouer si l’état interne est mauvais, c’est-à-dire si l’invariant de classe est cassé, ce qui signifie essentiellement: un bogue. Cette condition ne présente aucun intérêt pour l'utilisateur de l'objet Imprimante .

Personnellement, j’ai tendance à mélanger trois styles de validation d’état interne et je ne peux pas vraiment décider lequel est le meilleur, sinon le meilleur. J'aimerais entendre votre point de vue à ce sujet et partager votre expérience et vos réflexions sur ce sujet.

Le premier style que j'utilise - mieux vaut échouer de manière contrôlable que des données corrompues:

void Printer::Queue(const PrintJob& job)
{
    // Validate the state in both release and debug builds.
    // Never proceed with the queuing in a bad state.
    if(!IsValidState())
    {
        throw InvalidOperationException();
    }

    // Continue with queuing, parameter checking, etc.
    // Internal state is guaranteed to be good.
}

Le deuxième style que j'utilise - mieux vaut planter sans contrôle que les données corrompues:

void Printer::Queue(const PrintJob& job)
{
    // Validate the state in debug builds only.
    // Break into the debugger in debug builds.
    // Always proceed with the queuing, also in a bad state.
    DebugAssert(IsValidState());

    // Continue with queuing, parameter checking, etc.
    // Generally, behavior is now undefined, because of bad internal state.
    // But, specifically, this often means an access violation when
    // a NULL pointer is dereferenced, or something similar, and that crash will
    // generate a dump file that can be used to find the error cause during
    // testing before shipping the product.
}

Le troisième style que j'utilise - il est préférable de supprimer les données corrompues de manière silencieuse et défensive:

void Printer::Queue(const PrintJob& job)
{
    // Validate the state in both release and debug builds.
    // Break into the debugger in debug builds.
    // Never proceed with the queuing in a bad state.
    // This object will likely never again succeed in queuing anything.
    if(!IsValidState())
    {
        DebugBreak();
        return;
    }

    // Continue with defenestration.
    // Internal state is guaranteed to be good.
}

Mes commentaires sur les styles:

  1. Je pense que je préfère le second style, dans lequel l'échec n'est pas masqué, à condition qu'une violation d'accès provoque réellement un blocage.
  2. Si ce n'est pas un pointeur NULL impliqué dans l'invariant, j'ai tendance à me pencher vers le premier style.
  3. Je n'aime vraiment pas le troisième style, car il cache beaucoup de bogues, mais je connais des gens qui le préfèrent dans le code de production, car il crée l'illusion d'un logiciel robuste qui ne plante pas (les fonctionnalités cesseront de fonctionner comme dans la mise en file d'attente sur l'objet Printer cassé).

Préférez-vous ces solutions ou avez-vous d'autres moyens d'y parvenir?

Était-ce utile?

La solution

La question est mieux considérée en combinaison avec la façon dont vous testez votre logiciel.

Il est important que frapper un invariant cassé au cours des tests soit classé comme un bogue de gravité élevée, tout comme le serait un crash. Des générations de tests en cours de développement peuvent être réalisées pour arrêter les diagnostics morts et en sortie.

Il peut être approprié d’ajouter du code défensif, un peu comme votre style 3: votre DebugBreak afficherait des diagnostics dans des versions de test, mais constituerait simplement un point de rupture pour les développeurs. Cela rend moins probable la situation dans laquelle un développeur est empêché de travailler par un bogue dans un code non lié.

Malheureusement, je l’ai souvent vu faire l’inverse, les développeurs obtenant tous les inconvénients, mais les versions de test navigant à travers des invariants brisés. Un grand nombre de bogues de comportement étranges sont classés, où un seul bogue en est la cause.

Autres conseils

Vous pouvez utiliser une technique appelée NVI ( interface non virtuelle ) avec le modèle template method . C’est probablement ce que je ferais (bien sûr, c’est seulement mon opinion personnelle, ce qui est effectivement discutable):

class Printer {
public:
    // checks invariant, and calls the actual queuing
    void Queue(const PrintJob&);
private:
    virtual void DoQueue(const PringJob&);
};


void Printer::Queue(const PrintJob& job) // not virtual
{
    // Validate the state in both release and debug builds.
    // Never proceed with the queuing in a bad state.
    if(!IsValidState()) {
        throw std::logic_error("Printer not ready");
    }

    // call virtual method DoQueue which does the job
    DoQueue(job);
}

void Printer::DoQueue(const PrintJob& job) // virtual
{
    // Do the actual Queuing. State is guaranteed to be valid.
}

Etant donné que File d'attente n'est pas virtuel, l'invariant est toujours vérifié si une classe dérivée remplace DoQueue pour un traitement spécial.

Selon vos options: je pense que cela dépend de la condition que vous souhaitez vérifier.

S'il s'agit d'un invariant interne

  

S'il s'agit d'un invariant, il ne devrait pas   être possible pour un utilisateur de votre classe   le violer. La classe devrait s'occuper   à propos de son invariant lui-même. Pour cela,   Je voudrais assert (CheckInvariant ()); dans   un tel cas.

C'est simplement une condition préalable à une méthode

  

S'il ne s'agit que d'une condition préalable   l'utilisateur de la classe devrait   garantie (par exemple, impression uniquement après   l'imprimante est prête), je jetterais    std :: logic_error comme indiqué ci-dessus.

Je découragerais vraiment de vérifier une condition, mais ne rien faire ensuite.

L'utilisateur de la classe peut lui-même affirmer, avant d'appeler une méthode, que ses conditions préalables sont remplies. Donc, généralement, si une classe est responsable d’un état et trouve un état invalide, elle doit s’affirmer. Si la classe trouve une condition à violer qui ne relève pas de sa responsabilité, elle devrait la jeter.

C'est une question fine et très pertinente. IMHO, toute architecture d'application doit fournir une stratégie pour signaler les invariants cassés. On peut décider d'utiliser des exceptions, d'utiliser un objet 'Registre d'erreur' ou de vérifier explicitement le résultat de toute action. Peut-être y a-t-il même d'autres stratégies, ce n'est pas la question.

Dépendre d’un crash fort est une mauvaise idée: vous ne pouvez pas garantir le blocage de l’application si vous ne connaissez pas la cause de la violation invariante. Dans le cas contraire, vous avez toujours des données corrompues.

La solution Interface non virtuelle de litb est une façon élégante de vérifier les invariants.

Question difficile celle-ci:)

Personnellement, j’ai tendance à ne faire qu’une exception, car j’habite généralement trop dans la mise en œuvre de certaines choses afin de prendre en compte ce qui devrait être pris en charge par votre conception. Habituellement, cela revient et me mord plus tard ...

Mon expérience personnelle avec la stratégie "Faites un peu de journalisation-puis-ne-faites-plus-rien-de-plus" est que cela revient aussi à vous mordre - surtout si elle est appliquée comme dans votre cas (pas de stratégie globale, chaque classe pourrait potentiellement le faire de différentes manières).

Ce que je ferais dès que je découvrirais un problème de ce type, ce serait de parler au reste de mon équipe et de leur dire que nous avons besoin d’une sorte de traitement global des erreurs. La manipulation dépend de votre produit (vous ne voulez rien faire et enregistrer quelque chose dans un fichier subtil destiné aux développeurs dans un système de contrôleur de trafic aérien, mais cela fonctionnerait bien si vous fabriquiez un pilote pour, disons, une imprimante :)).

Je suppose que ce que je veux dire, c’est que, à mon humble avis, cette question devrait être résolue au niveau de la conception de votre application plutôt qu’au niveau de la mise en œuvre. - Et malheureusement, il n'y a pas de solution magique: (

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top