Quelle est la meilleure façon d'appeler une méthode qui n'est disponible que pour une classe qui implémente une interface mais pas l'autre?

11

Fondamentalement, je dois exécuter différentes actions dans une certaine condition. Le code existant est écrit de cette façon

Interface de base

// DoSomething.java
interface DoSomething {

   void letDoIt(String info);
}

Mise en place de la première classe ouvrière

class DoItThisWay implements DoSomething {
  ...
}

Mise en place de la deuxième classe ouvrière

class DoItThatWay implements DoSomething {
   ...
}

La classe principale

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          worker = new DoItThisWay();
        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }

Ce code fonctionne bien et est facile à comprendre.

Maintenant, en raison d'une nouvelle exigence, je dois transmettre une nouvelle information qui n'a de sens que pour DoItThisWay.

Ma question est la suivante: le style de codage suivant convient-il à cette exigence?

Utiliser une nouvelle variable et méthode de classe

// Use new class variable and method

class DoItThisWay implements DoSomething {
  private int quality;
  DoSomething() {
    quality = 0;
  }

  public void setQuality(int quality) {
    this.quality = quality;
  };

 public void letDoIt(String info) {
   if (quality > 50) { // make use of the new information
     ...
   } else {
     ...
   }
 } ;

}

Si je le fais de cette façon, je dois apporter la modification correspondante à l'appelant:

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          int quality = obtainQualityInfo();
          DoItThisWay tmp = new DoItThisWay();
          tmp.setQuality(quality)
          worker = tmp;

        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }

Est-ce un bon style de codage? Ou puis-je simplement le lancer

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          int quality = obtainQualityInfo();
          worker = new DoItThisWay();
          ((DoItThisWay) worker).setQuality(quality)
        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }
Anthony Kong
la source
19
Pourquoi ne pas simplement passer qualityau constructeur pour DoItThisWay?
David Arno
J'ai probablement besoin de réviser ma question ... car pour des raisons de performances, la construction de DoItThisWayet DoItThatWayse fait une fois dans le constructeur de Main. Mainest une classe à longue durée de vie et doingItest appelée plusieurs fois.
Anthony Kong
Oui, la mise à jour de votre question serait une bonne idée dans ce cas.
David Arno
Voulez-vous dire que la setQualityméthode sera appelée plusieurs fois pendant la durée de vie de l' DoItThisWayobjet?
jpmc26
1
Il n'y a pas de différence pertinente entre les deux versions que vous avez présentées. D'un point de vue logique, ils font la même chose.
Sebastian Redl

Réponses:

6

Je suppose que cela qualitydoit être défini à côté de chaque letDoIt()appel sur un DoItThisWay().

Le problème que je vois surgir ici est le suivant: vous introduisez un couplage temporel (c'est-à-dire ce qui se passe si vous oubliez d'appeler setQuality()avant d'appeler letDoIt()un DoItThisWay?). Et les implémentations pour DoItThisWayet DoItThatWaysont divergentes (l'une doit avoir setQuality()appelé, l'autre pas).

Bien que cela ne puisse pas causer de problèmes pour le moment, cela pourrait éventuellement vous hanter. Il pourrait être utile de réexaminer la question letDoIt()et de déterminer si les informations de qualité doivent faire partie de celles que infovous passez par elle; mais cela dépend des détails.

CharonX
la source
15

Est-ce un bon style de codage?

De mon point de vue, aucune de vos versions ne l'est. Le premier appel à faire setQualityavant letDoItpeut être appelé est un couplage temporel . Vous êtes bloqué en DoItThisWaytant que dérivé de DoSomething, mais ce n'est pas (du moins pas sur le plan fonctionnel), c'est plutôt quelque chose comme un

interface DoSomethingWithQuality {
   void letDoIt(String info, int quality);
}

Cela ferait Mainplutôt quelque chose comme

class Main {
    // omitting the creation
    private DoSomething doSomething;
    private DoSomethingWithQuality doSomethingWithQuality;

    public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
            int quality = obtainQualityInfo();
            doSomethingWithQuality.letDoIt(info, quality);
        } else {
            doSomething.letDoIt(info);
        }
    }
}

Vous pouvez en revanche passer directement les paramètres aux classes (en supposant que cela soit possible) et déléguer la décision à utiliser à une usine (cela rendrait les instances interchangeables à nouveau et les deux peuvent être dérivées DoSomething). Cela ferait Mainressembler à ceci

class Main {
    private DoSomethingFactory doSomethingFactory;

     public doingIt(String info) {
         int quality = obtainQualityInfo();
         DoSomething doSomethingWorker = doSomethingFactory.Create(info, quality);
         doSomethingWorker.letDoIt();
     }
}

Je sais que vous avez écrit cela

car pour des raisons de performances, la construction de DoItThisWay et DoItThatWay est effectuée une fois dans le constructeur de Main

Mais vous pouvez mettre en cache les pièces coûteuses à créer en usine et les transmettre au constructeur également.

Paul Kertscher
la source
Argh, m'espionnez-vous pendant que je tape des réponses? Nous faisons des
remarques
1
@DocBrown Oui, c'est discutable, mais puisque le DoItThisWaynécessite que la qualité soit définie avant de l'appeler, letDoItil se comporte différemment. Je m'attends à ce que a DoSomethingfonctionne de la même manière (sans définir la qualité auparavant) pour tous les dérivés. Est-ce que ça a du sens? Bien sûr, c'est un peu flou, car si nous appelions la setQualityméthode depuis une usine, le client serait agnostique quant à savoir si la qualité doit être définie ou non.
Paul Kertscher
2
@DocBrown Je suppose que le contrat pour letDoIt()(sans informations supplémentaires) "Je peux correctement exécuter (faire ce que je fais) si vous me le fournissez String info". Le setQuality()fait d'exiger un appel préalable renforce la condition préalable à l'appel letDoIt()et constituerait donc une violation de LSP.
CharonX
2
@CharonX: si, par exemple, il y aurait un contrat impliquant que " letDoIt" ne lèverait aucune exception, et oublier d'appeler "setQuality" ferait letDoItalors lever une exception, alors je suis d'accord, une telle implémentation violerait le LSP. Mais cela me semble être des hypothèses très artificielles.
Doc Brown
1
C'est une bonne réponse. La seule chose que j'ajouterais est un commentaire sur la gravité désastreuse du couplage temporel pour une base de code. C'est un contrat caché entre des composants apparemment découplés. Avec un couplage normal, au moins c'est explicite et facilement identifiable. Faire cela consiste essentiellement à creuser une fosse et à la couvrir de feuilles.
JimmyJames
10

Supposons qualityqu'il ne puisse pas être transmis au constructeur et que l'appel à setQualityest requis.

Actuellement, un extrait de code comme

    int quality = obtainQualityInfo();
    worker = new DoItThisWay();
    ((DoItThisWay) worker).setQuality(quality);

est beaucoup trop petit pour y investir trop de pensées. À mon humble avis, il semble un peu moche, mais pas vraiment difficile à comprendre.

Des problèmes surviennent lorsqu'un tel extrait de code se développe et en raison de la refactorisation, vous vous retrouvez avec quelque chose comme

    int quality = obtainQualityInfo();
    worker = CreateAWorkerForThisInfo();
    ((DoItThisWay) worker).setQuality(quality);

Maintenant, vous ne voyez pas immédiatement si ce code est toujours correct et le compilateur ne vous le dira pas. Donc, introduire une variable temporaire du type correct et éviter la conversion est un peu plus sûr pour le type, sans aucun effort supplémentaire réel.

Cependant, je donnerais en fait tmpun meilleur nom:

      int quality = obtainQualityInfo();
      DoItThisWay workerThisWay = new DoItThisWay();
      workerThisWay.setQuality(quality)
      worker = workerThisWay;

Une telle dénomination aide à donner un mauvais code.

Doc Brown
la source
tmp pourrait être encore mieux nommé quelque chose comme "workerThatUsesQuality"
user949300
1
@ user949300: À mon humble avis, ce n'est pas une bonne idée: supposez DoItThisWayque vous obtenez des paramètres plus spécifiques - selon ce que vous suggérez, le nom doit être changé à chaque fois. Il est préférable d'utiliser des noms pour des variables qui rendent le type d'objet clair, et non les noms de méthodes disponibles.
Doc Brown
Pour clarifier, ce serait le nom de la variable, pas le nom de la classe. Je suis d'accord que mettre toutes les capacités dans le nom de la classe est une mauvaise idée. Je suggère donc que cela workerThisWaydevienne quelque chose usingQuality. Dans ce petit extrait de code, il est plus sûr d'être plus précis. (Et, s'il y a une meilleure expression dans leur domaine que "usingQuality", utilisez-la.
user949300
2

Une interface commune où l'initialisation varie en fonction des données d'exécution se prête généralement au modèle d'usine.

DoThingsFactory factory = new DoThingsFactory(thingThatProvidesQuality);
DoSomething doSomething = factory.getDoer(info);

doSomething.letDoIt();

DoThingsFactory peut alors se soucier d'obtenir et de définir les informations de qualité à l'intérieur de la getDoer(info)méthode avant de renvoyer l'objet DoThingsWithQuality concret casté à l'interface DoSomething.

Greg Burghardt
la source