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


96

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 fooManagerclasse, 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 ThisHappenedaction.

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 Initiliazeappel. 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!


13
S'assurer que les appels aux méthodes d'une classe se produisent dans un ordre spécifique n'est généralement pas possible . C’est l’un des nombreux problèmes qui sont équivalents au problème d’arrêt. Bien qu'il soit possible de faire de la non-conformité une erreur de compilation lors de la compilation dans certains cas, il n'y a pas de solution générale. C’est probablement pour cette raison que peu de langages prennent en charge des constructions qui vous permettraient de diagnostiquer au moins les cas les plus évidents, même si l’analyse des flux de données est devenue assez sophistiquée.
Kilian Foth


5
La réponse à l'autre question est sans importance, la question n'est pas la même, donc ce sont des questions différentes. Si quelqu'un cherche cette discussion, il ne tapera pas le titre de l'autre question.
Gil Sand

6
Qu'est-ce qui empêche de fusionner le contenu d'initialize dans le ctor? L'appel doit-il 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?
Repéré

7
Ce problème s'appelle le couplage temporel . Si vous le pouvez, essayez d'éviter de placer l'objet dans un état non valide en lançant des exceptions dans le constructeur lorsque vous rencontrez des entrées non valides. Ainsi, vous ne pourrez jamais dépasser l'appel newsi 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.
Séraliser

Réponses:


161

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 FooManagerd'être utilisé sans être initialisé? En empêchant un FooManagerd'ê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 FooManageravant de pouvoir l'initialiser, par exemple parce que le FooManagerest transmis comme dépendance.

Ne créez pas de FooManagersi 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ète FooManager, 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:

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

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 MaybeInitializedFooManagerqui a une get()méthode qui retourne le FooManager, mais jette si le FooManagern'est pas complètement initialisé. Cela ne fonctionne que si l'initialisation est effectuée via l'encapsuleur ou s'il existe une FooManager#isInitialized()méthode.

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 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.

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() { ... }
}

Ceci extrait le comportement principal de la classe dans le fichier MainImpl.


9
J'aime les deux premières solutions (+1), mais je ne pense pas que votre dernier extrait avec sa répétition des méthodes in FooManagerand in UninitialisedImplsoit mieux que de répéter if (!initialized) throw;.
Bergi

3
@ Bergi Certaines personnes aiment trop les cours. Bien que si FooManagera un grand nombre de méthodes, il pourrait être plus facile que potentiellement oublier certains des if (!initialized)contrôles. Mais dans ce cas, vous devriez probablement préférer diviser la classe.
user253751

1
MaybeInitializedFoo ne semble pas meilleur qu'un Foo initialisé, mais +1 pour donner quelques options / idées.
Matthew James Briggs

7
+1 L'usine est la bonne option. Vous ne pouvez pas améliorer la conception d'une image sans la modifier, aussi, à mon humble avis, la dernière partie de la réponse à propos de la moitié du temps, c'est que cela ne va pas aider le PO.
Nathan Cooper

6
@ Bergi J'ai trouvé la technique présentée dans la dernière section extrêmement utile (voir aussi la technique de refactoring «Remplacer les conditions par le polymorphisme» et le modèle d'état). Il est facile d'oublier la vérification d'une méthode si nous utilisons un if / else, il est beaucoup plus difficile d'oublier l'implémentation d'une méthode requise par l'interface. Plus important encore, MainImpl correspond exactement à un objet où la méthode initialize est fusionnée avec le constructeur. Cela signifie que l'implémentation de MainImpl peut bénéficier des garanties les plus fortes que cela offre, ce qui simplifie le code principal. ...
amon

79

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 Initializeavec 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 UnitializedFooManagerinstance, qui a une Initialize(...)méthode qui retourne InitializedFooManager. Les méthodes qui ne peuvent être appelées que dans l'état initialisé existent uniquement InitializedFooManager. 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 appeler Openavant que la ReadLineméthode puisse être appelée et doit se rappeler de toujours appeler Close(même si une exception se produit), après quoi la ReadLinemé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 une Readmé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:

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 consisterait à définir en Filetant 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.

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 des méthodes dans un certain ordre. Il est tout simplement impossible d'appeler des méthodes dans le mauvais ordre.


2
De plus, je me souviens de cas où il n'était tout simplement pas possible de passer du constructeur, mais je n'ai pas d'exemple à montrer pour l'instant
Gil Sand

2
L'exemple décrit maintenant un modèle d'usine - mais la première phrase décrit en fait le paradigme RAII . Alors, quelle réponse cette réponse est-elle censée recommander?
Ext3h le

11
@ Ext3h Je ne pense pas que le modèle d'usine et RAII s'excluent mutuellement.
Pieter B

@PieterB Non, ils ne le sont pas, une usine peut assurer la RAII. Mais seulement avec l’implication supplémentaire que le modèle / constructeur args (appelé UnitializedFooManager) ne doit pas partager un état avec les instances ( InitializedFooManager), comme cela Initialize(...)a été fait Instantiate(...)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 la movesémantique afin de consommer le modèle de manière fiable.
Ext3h

2
@ Ext3h: Je recommande la première solution car c'est la plus simple. Mais si le client doit pouvoir accéder à l'objet avant d'appeler Initialize, vous ne pouvez pas l'utiliser, mais vous pourrez peut-être utiliser la deuxième solution.
JacquesB

39

Normalement, je vérifierais simplement l’initialisation et jetterais (par exemple) un IllegalStateExceptionsi 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

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

et ainsi vous contrôlez la création d'objets et de cycle de vie, et vos clients obtenir le Componentsuite de votre initialisation. C'est effectivement une instanciation paresseuse.


1
Bonne réponse - 2 bonnes options dans une belle réponse succincte. D'autres réponses ci-dessus présentent la gymnastique des systèmes de types qui sont (théoriquement) valables; mais dans la majorité des cas, ces 2 options plus simples sont les choix pratiques idéaux.
Thomas W

1
Remarque: Dans .NET, InvalidOperationException est promu par Microsoft pour correspondre à ce que vous avez appelé IllegalStateException.
miroxlav

1
Je n'ai pas voté contre cette réponse, mais ce n'est pas vraiment une bonne réponse. En jetant IllegalStateExceptionou InvalidOperationExceptionà 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.
displayName

Vous remarquerez que le lancement d'exception est une option possible et je vais développer mon option préférée: sécuriser cette compilation. J'ai modifié ma réponse pour souligner cela
Brian Agnew

14

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 FooManagerfichier dans Haskell, il serait criminel de permettre à vos utilisateurs d'en créer un qui ne soit pas capable de gérer les Foos, 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 FooManageret struct UninitializedFooManagertypes 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.


"Si vous écrivez en Python, il n'y a pas de mécanisme dans le langage pour vous permettre de le faire." Python a des constructeurs, et c'est assez simple de créer des méthodes d'usine. Alors peut-être que je ne comprends pas ce que vous entendez par "ceci"?
AL Flanagan

3
Par "ceci", j'entendais une erreur lors de la compilation, par opposition à une erreur d'exécution.
Patrick Collins

En passant à mentionner Python 3.5 qui est la version actuelle a des types via un système de type enfichable. Il est certainement possible d’obtenir une erreur de Python avant d’exécuter le code uniquement parce qu’il a été importé. Jusqu'à 3,5, c'était tout à fait correct cependant.
Benjamin Gruenbaum

Ah d'accord. En retard +1. Même confus, c'était très perspicace.
AL Flanagan

J'aime ton style Patrick.
Gil Sand

4

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:

/** 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 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.


3

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()et b(). L'exigence est que a()s'appelle toujours avant b(). Vous pouvez créer une vérification au moment de la compilation en retournant un nouvel objet a()et en le 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 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).


2

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);et abstract 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.


2

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.


1
En ce qui concerne le nettoyage forcé, un modèle peut être utilisé lorsqu'une ressource ne peut être allouée qu'en appelant une méthode particulière (par exemple, dans un langage OO typique, il pourrait avoir un constructeur privé). Cette méthode alloue la ressource, appelle une fonction (ou une méthode d'interface) qui lui est transmise avec une référence à la ressource, puis détruit la ressource une fois que cette fonction est revenue. Outre la fin abrupte du thread, ce modèle garantit que la ressource est toujours détruite. C'est une approche commune dans les langages fonctionnels, mais je l'ai également vue utilisée à bon escient dans les systèmes OO.
Jules


2

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!


0

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û.

  • Soit explicitement demander cette information requise dans le constructeur;
  • Ou laissez les constructeurs intacts et modifiez les méthodes de sorte que pour pouvoir les appeler, vous devez fournir les données manquantes.

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.


0

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:

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 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.


-1

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:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

Étant donné qu'un constructeur est implémenté, il est impossible pour quiconque de créer une instance de FooManagersans passer par MakeFooManager().


1
cela semble simplement répéter le point soulevé et expliqué dans une réponse antérieure qui avait été affichée plusieurs heures auparavant
GBNat

1
est-ce que cela a un avantage sur la simple vérification à zéro dans le ctor? il semble que vous veniez de créer une méthode qui n'englobe qu'une autre méthode. Pourquoi?
Sara

Aussi, pourquoi les variables membres foo et bar?
Patrick M

-1

Il y a plusieurs approches:

  1. 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 .

  2. 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.

  3. Utilisez des assertions dans vos versions de débogage.

  4. Lance des exceptions si l'utilisation de la classe n'est pas valide.

* Voici le genre de commentaires que vous ne devriez pas écrire:

// gets Foo
GetFoo();
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.