web-dev-qa-db-fra.com

Comment avertir les autres programmeurs de l'implémentation de classe

J'écris des classes qui "doivent être utilisées d'une manière spécifique" (je suppose que toutes les classes doivent ...).

Par exemple, je crée la classe fooManager, qui nécessite un appel à, disons, Initialize(string,string). Et, pour pousser l'exemple un peu plus loin, la classe serait inutile si nous n'écoutons pas son action ThisHappened.

Mon point est que la classe que j'écris nécessite des appels de méthode. Mais il se compilera très bien si vous n'appelez pas ces méthodes et se retrouvera avec un nouveau FooManager vide. À un moment donné, cela ne fonctionnera pas, ou peut se bloquer, selon la classe et ce qu'il fait. Le programmeur qui implémente ma classe regarderait évidemment à l'intérieur et réaliserait "Oh, je n'ai pas appelé Initialize!", Et ça irait.

Mais je n'aime pas ça. Ce que je souhaiterais idéalement, c'est que le code ne compile PAS si la méthode n'était pas appelée; Je suppose que ce n'est tout simplement pas possible. Ou quelque chose qui serait immédiatement visible et clair.

Je suis 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 partout où cela est nécessaire si la classe est initialisée; sinon, je lèverai une exception disant "La classe n'a pas été initialisée, êtes-vous sûr que vous appelez .Initialize(string,string)?".

Je suis plutôt d'accord avec cette approche, mais cela conduit à beaucoup de code qui est compilé et, en fin de compte, n'est pas nécessaire pour l'utilisateur final.

De plus, c'est parfois encore plus de code quand il y a plus de méthodes qu'un simple Initiliaze à appeler. J'essaie de garder mes classes avec pas trop de méthodes/actions publiques, mais cela ne résout pas le problème, il reste juste raisonnable.

Ce que je recherche ici, c'est:

  • Mon approche est-elle correcte?
  • Y en a t-il un meilleur?
  • Que faites-vous/conseillez-vous?
  • Suis-je en train de résoudre un non-problème? Des collègues m'ont dit que c'était au programmeur de vérifier la classe avant d'essayer de l'utiliser. Je ne suis respectueusement pas d'accord, mais je crois que c'est une autre question.

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 de nombreuses questions ici:

  • Je ne parle certainement pas seulement de la partie Initialisation d'une classe, mais plutôt de toute la vie. Empêchez vos collègues d'appeler une méthode deux fois, en vous assurant d'appeler X avant Y, etc. Tout ce qui finirait par être obligatoire et dans la documentation, mais que j'aimerais dans le code et aussi simple et petit que possible. J'ai vraiment aimé l'idée d'Asserts, bien que je sois certain que je devrai mélanger d'autres idées car les Asserts ne seront pas toujours possibles.

  • J'utilise le langage C #! Comment n'ai-je pas mentionné cela?! Je suis dans un environnement Xamarin et je crée des applications mobiles utilisant généralement environ 6 à 9 projets dans une solution, y compris PCL, iOS, Android et projets Windows. Je suis développeur depuis environ an et demi (école et travail combinés), d'où mes déclarations/questions parfois ridicules. Tout cela est probablement hors de propos ici, mais trop d'informations ne sont 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 l'injection de dépendance, avoir des paramètres autres que les interfaces est hors de propos. 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 où 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, qui pourrait aller dans un paramètre évidemment. Mais une fois la vue disparue, StopFetching () doit être appelé. Sinon, la classe continuerait à chercher des annonces pour une vue qui n'est même pas là, et c'est mauvais.

De plus, 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. L'annonce fonctionne toujours, donc l'utilisateur et le développeur ne verront pas de différence, et les analyses auront simplement des données erronées. Cela doit être évité, mais je ne sais pas comment le développeur peut savoir qu'il doit s'inscrire à l'événement tao. C'est un exemple simplifié cependant, mais l'idée est là, "assurez-vous qu'il utilise l'Action publique qui est disponible" et au bon moment bien sûr!

96
Gil Sand

Dans de tels cas, il est préférable d'utiliser le système de type de votre langue pour vous aider à effectuer une initialisation correcte. Comment empêcher qu'un FooManager soit tilisé sans être initialisé? En empêchant un FooManager d'être créé sans les informations nécessaires pour l'initialiser correctement. En particulier, toute initialisation est de la responsabilité du constructeur. Vous ne devez jamais laisser votre constructeur créer un objet dans un état illégal.

Mais les appelants doivent construire un FooManager avant de pouvoir l'initialiser, par exemple parce que FooManager est transmis comme une 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 récupérer un FooManager entièrement construit, mais uniquement avec les informations d'initialisation. (Dans la programmation fonctionnelle, je suggère que vous utilisiez une application partielle pour le constructeur.) Par exemple:

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

Le problème est que vous devez fournir les informations d'initialisation à chaque fois que vous accédez à FooManager.

Si cela est nécessaire dans votre langue, vous pouvez encapsuler l'opération getFooManager() dans une classe de type usine ou de type constructeur.

Je veux vraiment vérifier à l'exécution que la méthode initialize() a été appelée, plutôt que d'utiliser une solution de niveau système.

Il est possible de trouver un compromis. Nous créons une classe wrapper MaybeInitializedFooManager qui a une méthode get() qui retourne le FooManager, mais lève si le FooManager n'a pas été complètement initialisé. Cela ne fonctionne que si l'initialisation est effectuée via le wrapper, ou s'il existe une méthode FooManager#isInitialized().

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

Je ne veux pas changer l'API de ma classe.

Dans ce cas, vous souhaiterez éviter les conditions if (!initialized) throw; dans chaque méthode. Heureusement, il existe un modèle simple pour résoudre ce problème.

L'objet que vous fournissez aux utilisateurs n'est qu'un shell vide qui délègue tous les appels à un objet d'implémentation. Par défaut, l'objet d'implémentation renvoie une erreur pour chaque méthode qui n'a pas été initialisée. Cependant, la méthode initialize() remplace l'objet d'implémentation par un objet entièrement construit.

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

Cela extrait le comportement principal de la classe dans le MainImpl.

160
amon

Le moyen le plus efficace et le plus utile d'empêcher les clients de "mal utiliser" un objet est de le rendre impossible.

La solution la plus simple est de fusionner Initialize avec le constructeur. De cette façon, l'objet ne sera jamais disponible pour le client dans l'état non initialisé, donc 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 d'usine. 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 de méthode constructeur ou usine.

Si vous devez être en mesure d'accéder à l'objet non initialisé avant l'initialisation, vous pouvez alors implémenter les deux états en tant que classes distinctes, donc vous commencez avec une instance UnitializedFooManager, qui a une fonction Initialize(...) méthode qui renvoie InitializedFooManager. Les méthodes qui ne peuvent être appelées qu'à l'état initialisé n'existent que sur InitializedFooManager. Cette approche peut être étendue à plusieurs états si nécessaire.

Par rapport aux exceptions d'exécution, il est plus difficile de représenter les états en tant que classes distinctes, mais cela vous donne également des garanties au moment de la compilation que vous n'appelez pas des méthodes qui ne sont pas valides pour l'état de l'objet, et il documente les transitions d'état plus clairement dans le code.

Mais plus généralement, la solution idéale est de concevoir vos classes et méthodes sans contraintes telles que vous obliger à appeler des méthodes à certains moments et dans un certain ordre. Ce n'est pas toujours possible, mais cela peut être évité dans de nombreux cas en utilisant un modèle approprié.

Si vous avez un couplage temporel complexe (un certain nombre de méthodes doivent être appelées dans un certain ordre), une solution pourrait être d'utiliser inversion de contrôle, donc vous créez 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 les opérations dans le bon ordre est transférée du client à la classe elle-même.

Un exemple simple: vous avez un objet File- qui vous permet de lire à partir d'un fichier. Cependant, le client doit appeler Open avant que la méthode ReadLine puisse être appelée, et doit toujours se rappeler d'appeler Close (même si une exception se produit) après la méthode ReadLine ne doit plus être appelée. C'est le couplage temporel. Il peut être évité en ayant une seule méthode qui prend un rappel ou un délégué comme 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 une méthode Read au rappel. De cette façon, il n'est pas possible pour le client d'oublier d'appeler des méthodes dans le bon ordre.

Interface avec couplage temporel:

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

Sans couplage temporel:

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

Une solution plus lourde serait de définir File comme une classe abstraite qui vous oblige à implémenter une méthode (protégée) qui sera exécutée sur le fichier ouvert. C'est ce qu'on appelle un modèle méthode du modèle.

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

L'avantage est le même: le client n'a pas à se rappeler d'appeler les méthodes dans un certain ordre. Il n'est tout simplement pas possible d'appeler des méthodes dans le mauvais ordre.

79
JacquesB

Je vérifierais normalement l'intialisation et lancerais (disons) un IllegalStateException si vous essayez de l'utiliser sans être initialisé.

Cependant, si vous voulez être sûr à la compilation (et c'est louable et préférable), pourquoi ne pas traiter l'initialisation comme une méthode d'usine renvoyant un objet construit et initialisé, par ex.

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

et ainsi vous contrôlez la création et le cycle de vie des objets, et vos clients obtiennent le Component à la suite de votre initialisation. C'est effectivement une instanciation paresseuse.

39
Brian Agnew

Je vais rompre un peu avec les autres réponses et être en désaccord: il est impossible de répondre à cette question sans savoir dans quelle langue vous travaillez. Que ce soit un plan valable ou non et le bon "avertissement" à donner vos utilisateurs dépendent entièrement des mécanismes fournis par votre langue et des conventions que les autres développeurs de cette langue ont tendance à suivre.

Si vous avez un FooManager dans Haskell, ce serait criminel pour permettre à vos utilisateurs d'en créer un qui n'est pas capable de gérer Foos, parce que le système de type le rend si facile et ce sont les conventions que les développeurs Haskell attendent.

D'un autre côté, si vous écrivez C, vos collègues auraient entièrement le droit de vous retirer et de vous tirer dessus pour définir des struct FooManager et struct UninitializedFooManager types qui prennent en charge différentes opérations, car cela conduirait à un code inutilement complexe pour très peu d'avantages.

Si vous écrivez 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 illustratifs en termes de combien/peu de travail le système de type est attendu et capable de faire.

Suivez les attentes raisonnables d'un développeur dans votre langue, et résistez à l'envie de sur-concevoir une solution qui n'a pas d'implémentation naturelle et idiomatique (à moins que l'erreur ne soit facile à fabriquer et si difficile à attraper qu'il vaut la peine d'aller à des longueurs extrêmes pour le rendre impossible). Si vous n'avez pas suffisamment 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.

14
Patrick Collins

Comme vous semblez ne pas vouloir expédier la vérification du code au client (mais semblez le faire pour le programmeur), vous pouvez utiliser les fonctions assert , si elles sont disponibles dans votre langage de programmation.

De cette façon, vous avez les vérifications dans l'environnement de développement (et tous les tests qu'un autre développeur appellerait Échoueraient de manière prévisible), mais vous n'enverrez pas le code au client car les affirmations (au moins en Java) ne sont compilées que de manière sélective.

Ainsi, une classe Java utilisant ceci ressemblerait à ceci:

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

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 quiconque fasse de mal dans un environnement réel.

De plus, ils sont assez minces et ne prennent pas d'énormes portions de la syntaxe if () throw ..., ils n'ont pas besoin d'être interceptés, etc.

4
Angelo Fuchs

En examinant ce problème plus généralement que les réponses actuelles, qui se sont principalement concentrées sur l'initialisation. Considérons un objet qui aura deux méthodes, a() et b(). La condition est que a() soit toujours appelée avant b(). Vous pouvez créer une vérification au moment de la compilation que cela se produit en renvoyant un nouvel objet de a() et en déplaçant b() vers le nouvel objet plutôt que vers l'original. Exemple d'implémentation:

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

Maintenant, il est impossible d'appeler b() sans d'abord appeler a (), car il est implémenté dans une interface cachée jusqu'à a() is Certes, c'est beaucoup d'efforts, donc je n'utiliserais pas habituellement cette approche, mais il y a des situations où cela peut être bénéfique (surtout si votre classe va être réutilisée dans un beaucoup de situations par des programmeurs qui pourraient ne pas être familiers avec son implémentation, ou avec du 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 le suggèrent de nombreuses réponses existantes. Il fonctionne à peu près la de la même manière, la seule vraie différence est où les données sont stockées (dans l'objet d'origine plutôt que celui retourné) et quand elles sont destinées à être utilisées (à tout moment par rapport uniquement à l'initialisation).

3
Jules

La réponse est: oui, non et parfois. :-)

Certains des problèmes que vous décrivez sont faciles à résoudre, 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 à aucune vérification du tout.

Temps d'exécution RE:

Il est au moins conceptuellement simple de forcer les fonctions à être appelées dans un certain ordre, etc., avec des vérifications au moment de l'exécution. Mettez simplement des drapeaux qui indiquent quelle fonction a été exécutée, et laissez chaque fonction commencer par quelque chose comme "sinon pré-condition-1 ou pas pré-condition-2, puis lever l'exception". Si les vérifications deviennent complexes, vous pouvez les pousser 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 à false ou une référence d'objet clé à null. Ensuite, lorsqu'une fonction est appelée qui a besoin des données pertinentes, elle vérifie l'indicateur. S'il est faux, il remplit les données et définit le drapeau sur true. Si c'est vrai, cela continue en supposant 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. Ensuite, lorsque vous atteignez un point où une fonction de précondition aurait dû être appelée, si elle n'a pas encore été appelée, appelez-la. Bien sûr, cela ne fonctionne que si vous avez les données pour l'appeler à ce stade, mais dans de nombreux cas, vous pouvez inclure toutes les données requises dans les paramètres de fonction de l'appel "externe".

Temps de compilation RE:

Comme d'autres l'ont dit, si vous devez initialiser avant qu'un objet puisse être utilisé, cela place l'initialisation dans le constructeur. C'est un conseil assez typique pour OOP. Si possible, vous devez faire en sorte que le constructeur crée une instance valide et utilisable de l'objet.

Je vois une discussion sur Que faire si vous avez besoin de faire circuler des références à l'objet avant qu'il ne soit initialisé. Je ne peux pas penser à un véritable exemple de cela du haut de ma tête, mais je suppose que cela pourrait arriver. Des solutions ont été suggérées, mais les solutions que j'ai vues ici et toutes celles auxquelles je peux penser sont compliquées et compliquent le code. À un moment donné, vous devez vous demander: cela vaut-il la peine de créer du code laid et difficile à comprendre afin que nous puissions avoir une vérification au moment de la compilation plutôt qu'une vérification au moment de l'exécution? Ou est-ce que je crée beaucoup de travail pour moi-même et pour les autres pour un objectif agréable mais pas obligatoire?

Si deux fonctions doivent toujours être exécutées ensemble, la solution simple est d'en faire une seule fonction. Comme si chaque fois que vous ajoutez une publicité à une page, vous devez également ajouter un gestionnaire de métriques pour celle-ci, puis mettez le code pour ajouter le gestionnaire de métriques dans la fonction "ajouter une publicité".

Certaines choses seraient presque impossibles à vérifier au moment de la compilation. Comme l'exigence qu'une certaine fonction ne puisse pas être appelée plus d'une fois. (J'ai écrit de nombreuses fonctions comme celle-ci. J'ai récemment écrit une fonction qui recherche des fichiers dans un répertoire magique, traite tous ceux qu'il trouve, puis les supprime. Bien sûr, une fois le fichier supprimé, il n'est pas possible de le réexécuter. Etc.) Je ne connais aucun langage ayant une fonctionnalité qui vous permet d'empêcher qu'une fonction soit appelée deux fois sur la même instance au moment 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 contrôles de temps d'exécution. Cette vérification du temps d'exécution en particulier est évidente, n'est-ce pas? msgstr "si déjà - été - ici = vrai, alors lever l 'exception sinon définir déjà - été - ici = vrai".

RE impossible à appliquer

Il n'est pas rare qu'une classe nécessite 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 de forcer cela à se produire lors de la compilation ou le temps d'exécution. Je pense que la plupart des langages OOP ont une sorte de provision pour une fonction "finalizer" qui est appelée quand une instance est récupérée, mais je pense que la plupart disent aussi qu'ils ne peuvent pas garantir que cela sera jamais exécuté . OOP incluent souvent une fonction "dispose" avec une sorte de disposition pour définir une portée sur l'utilisation d'une instance, une clause "using" ou "with" et quand le programme se termine cette portée, le disposer est exécuté. Mais cela nécessite que le programmeur utilise le spécificateur d'étendue approprié. Il ne force pas le programmeur à le faire correctement, cela lui facilite simplement la tâche.

Dans les cas où il est impossible de forcer le programmeur à utiliser correctement la classe, j'essaie de faire en sorte que le programme explose s'il le fait mal, plutôt que de donner des résultats incorrects. Exemple simple: j'ai vu de nombreux programmes qui initialisent un tas de données en valeurs fictives, de sorte que l'utilisateur n'obtiendra jamais d'exception de pointeur nul 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 bogues plus difficiles à trouver. Si, lorsqu'un programmeur ne parvenait pas à appeler la fonction "charger les données" et tentait allègrement d'utiliser les données, il obtenait une exception de pointeur nul, l'exception lui montrait rapidement où était le problème. Mais si vous les masquez en rendant les données vides ou nulles dans de tels cas, le programme peut s'exécuter complètement mais produire des résultats inexacts. Dans certains cas, le programmeur peut même ne pas réaliser qu'il y a eu un problème. S'il le remarque, il devra peut-être suivre une longue piste pour trouver où cela s'est mal passé. Mieux vaut échouer tôt que d'essayer de continuer courageusement.

En général, bien sûr, c'est toujours bien quand vous pouvez rendre impossible une utilisation incorrecte d'un objet. C'est bien si les fonctions sont structurées de telle manière qu'il n'y a tout simplement aucun moyen pour le programmeur de faire des appels invalides, ou si des appels invalides 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.

2
Jay

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 qu'elle ne devienne utile, j'ai tendance à rendre cette classe de base abstraite, puis à définir plusieurs méthodes abstraites dans cette classe qui sont utilisées dans le flux de la classe de base (comme abstract Collection<Information> get_extra_information(args); et abstract Collection<OtherObjects> get_other_objects(args); qui par contrat d'héritage doivent être implémentées par la classe concrète, forçant l'utilisateur de cette classe de base à fournir tout ce dont la classe de base a besoin.

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 j'ai juste besoin 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éthode abstraite permettent à l'implémentation de traiter les arguments passés aux appels de méthode abstraite. Ou même dans le cas où aucun argument n'est utilisé, cela 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 lors du passage de la variable comme argument à le constructeur. Certes, 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.

2
klaar

Oui, vos instincts sont parfaits. Il y a plusieurs années j'ai écrit des articles dans des magazines (un peu comme cet endroit, mais sur l'arbre mort et éteint une fois par mois) discutant Débogage, Abugging, et Antibugging.

Si vous pouvez obtenir le compilateur pour détecter les abus, c'est la meilleure façon. Les fonctionnalités linguistiques disponibles dépendent de la langue et vous n'avez pas précisé. De toute façon, des techniques spécifiques seraient détaillées pour les 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 genre de test: vous encore devez savoir pour appeler les fonctions appropriées en premier et les utiliser dans le bon façon.

Concevoir le composant pour éviter que même être un problème en premier lieu soit beaucoup plus subtil, et j'aime l'exemple le tampon est comme 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 buffer () ne peut pas être appelé après que vous ayez commencé à lire (). Au contraire, il ne doit être utilisé qu'une seule fois, immédiatement après la construction. Le fait que la classe gère le tampon automatiquement et de manière invisible pour l'appelant éviterait conceptuellement le problème.

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 permettra à votre équipe d'économiser beaucoup de travail de débogage un jour, lorsque le code est conservé et que l'utilisation spécifique est perturbée. Le test peut être configuré comme un ensemble formel de contraintes et invariants qui peut être testé. Cela ne signifie pas que la surcharge d'espace supplémentaire pour suivre l'état et le travail de vérification est toujours laissée à chaque appel. C'est dans la classe donc il pourrait être appelé, et le code documente les contraintes et hypothèses réelles.

Peut-être que la vérification n'est effectuée que dans une version de débogage.

Peut-être que la vérification est complexe (pensez à heapcheck, par exemple), mais c'est présent et peut être fait de temps en temps, ou des appels ajoutés ici et là lorsque le code est en cours d'élaboration et certains problème apparu, ou pour effectuer des tests spécifiques pour vous assurer qu'il n'y a pas un tel problème dans un test unitaire ou programme de test d'intégration qui renvoie à la même classe.

Vous pouvez décider que les frais généraux sont triviaux après tout. Si la classe produit des E/S, et un octet d'état supplémentaire et un test pour cela ne sont rien. Les classes iostream courantes vérifient que le flux est en mauvais état.

Vos instincts sont bons. Continuez!

2
JDługosz

Le principe du masquage d'informations (encapsulation) est que les entités extérieures à la classe ne doivent pas en savoir plus que ce qui est nécessaire pour utiliser correctement cette classe.

Votre cas semble que vous essayez de faire fonctionner correctement un objet d'une classe sans en dire suffisamment sur la classe aux utilisateurs externes. Vous avez caché plus d'informations que vous n'auriez dû.

  • Soit explicitement demander les informations requises dans le constructeur;
  • Ou conservez les constructeurs intacts et modifiez les méthodes de sorte que pour appeler les méthodes, vous devez fournir les données manquantes.

Paroles de sagesse:

* Dans tous les cas, vous devez repenser la classe et/ou le constructeur et/ou les méthodes.

* En ne concevant pas correctement et en privant la classe d'informations correctes, vous risquez que votre classe se casse non pas à un mais potentiellement à plusieurs endroits.

* Si vous avez mal écrit les exceptions et les messages d'erreur, votre classe en dira encore plus sur elle-même.

* Enfin, si l'outsider est supposé savoir qu'il doit initialiser a, b et c puis appeler d (), e (), f(int) alors vous avez une fuite dans votre abstraction.

0
displayName

Puisque vous avez spécifié que vous utilisez C #, une solution viable à votre situation consiste à tirer parti des Roslyn Code Analyzers . Cela vous permettra de détecter immédiatement les violations et même de suggérer des correctifs de code .

Une façon de l'implémenter serait de décorer les méthodes couplées temporellement avec un attribut qui spécifie l'ordre dans lequel les méthodes doivent être appelées1. Lorsque l'analyseur trouve ces attributs dans une classe, il valide que les méthodes sont appelées dans l'ordre. Cela donnerait à votre classe un aspect semblable à celui-ci:

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1: Je n'ai jamais écrit un analyseur de code Roslyn, donc ce n'est peut-être pas la meilleure implémentation. L'idée d'utiliser Roslyn Code Analyzer pour vérifier que votre API est utilisée correctement est cependant 100% saine.

0
Erik