Comment maintenir un nombre d'arguments bas et toujours séparer les dépendances de tiers?

13

J'utilise une bibliothèque tierce. Ils me passent un POJO qui, à nos fins et à des fins, est probablement implémenté comme ceci:

public class OurData {
  private String foo;
  private String bar;
  private String baz;
  private String quux;
  // A lot more than this

  // IMPORTANT: NOTE THAT THIS IS A PACKAGE PRIVATE CONSTRUCTOR
  OurData(/* I don't know what they do */) {
    // some stuff
  }

  public String getFoo() {
    return foo;
  }

  // etc.
}

Pour de nombreuses raisons, notamment, mais sans s'y limiter, l'encapsulation de leur API et la facilitation des tests unitaires, je souhaite encapsuler leurs données. Mais je ne veux pas que mes classes de base dépendent de leurs données (encore une fois, pour des raisons de test)! Alors maintenant, j'ai quelque chose comme ça:

public class DataTypeOne implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
  }
}

public class DataTypeTwo implements DataInterface {
  private String foo;
  private int bar;
  private double baz;

  public DataTypeOne(String foo, int bar, double baz, String quux) {
    this.foo = foo;
    this.bar = bar;
    this.baz = baz;
    this.quux = quux;
  }
}

Et puis ceci:

public class ThirdPartyAdapter {
  public static makeMyData(OurData data) {
    if(data.getQuux() == null) {
      return new DataTypeOne(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
      );
    } else {
      return new DataTypeTwo(
        data.getFoo(),
        Integer.parseInt(data.getBar()),
        Double.parseDouble(data.getBaz()),
        data.getQuux();
      );
  }
}

Cette classe d'adaptateur est couplée aux quelques autres classes qui DOIVENT connaître l'API tierce, ce qui limite son omniprésence dans le reste de mon système. Cependant ... cette solution est BRUTE! Dans Clean Code, page 40:

Plus de trois arguments (polyadiques) nécessitent une justification très spéciale - et ne devraient donc pas être utilisés de toute façon.

Choses que j'ai envisagées:

  • Créer un objet d'usine plutôt qu'une méthode d'assistance statique
    • Ne résout pas le problème d'avoir un bajillion d'arguments
  • Création d'une sous-classe de DataTypeOne et DataTypeTwo qui a un constructeur dépendant
    • A toujours un constructeur protégé polyadique
  • Créez des implémentations entièrement distinctes conformes à la même interface
  • Plusieurs des idées ci-dessus simultanément

Comment gérer cette situation?


Notez que ce n'est pas une situation de couche anti-corruption . Il n'y a rien de mal avec leur API. Les problèmes sont:

  • Je ne veux pas que MES structures de données aient import com.third.party.library.SomeDataStructure;
  • Je ne peux pas construire leurs structures de données dans mes cas de test
  • Ma solution actuelle se traduit par un nombre d'arguments très très élevé. Je veux garder le nombre d'arguments bas, SANS passer dans leurs structures de données.
  • Cette question est " qu'est - ce qu'une couche anti-corruption?". Ma question est " comment puis-je utiliser un modèle, n'importe quel modèle, pour résoudre ce scénario?"

Je ne demande pas non plus de code (sinon cette question serait sur SO), je demande juste assez de réponse pour me permettre d'écrire le code efficacement (ce que cette question ne fournit pas).

durron597
la source
S'il existe plusieurs POJO tiers, il peut être utile d'écrire du code de test personnalisé qui utilise une carte avec certaines conventions (par exemple, nommez les touches int_bar) comme entrée de test. Ou utilisez JSON ou XML avec du code intermédiaire personnalisé. En fait, une sorte de DSL pour tester com.thirdparty.
user949300
La citation complète de Clean Code:The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.
Lilienthal
11
L'adhésion aveugle à un modèle ou à une directive de programmation est son propre anti-modèle .
Lilienthal
2
"encapsuler leur API et faciliter les tests unitaires" Cela pourrait être un cas de sur-test et / ou de dommage de conception induit par le test (ou indiquant que vous pourriez concevoir cela différemment pour commencer). Demandez-vous ceci: cela rend-il vraiment votre code plus facile à comprendre, à modifier et à réutiliser? Je mettrais mon argent sur "non". Dans quelle mesure est-il réaliste que vous échangiez cette bibliothèque? Probablement pas très. Si vous l'échangez, est-ce vraiment plus facile d'en déposer un complètement différent en place? Encore une fois, je parierais sur "non".
jpmc26
1
@JamesAnderson Je viens de reproduire la citation complète parce que je l'ai trouvée intéressante, mais je ne savais pas clairement si l'extrait faisait référence aux fonctions en général ou aux constructeurs en particulier. Je ne voulais pas approuver la revendication et, comme l'a dit jpmc26, mon prochain commentaire devrait vous donner une indication que je ne le faisais pas. Je ne sais pas pourquoi vous ressentez le besoin d'attaquer les universitaires, mais l'utilisation de polysyllabes ne fait pas de quelqu'un un élitiste universitaire perché sur sa tour d'ivoire au-dessus des nuages.
Lilienthal

Réponses:

10

La stratégie que j'ai utilisée quand il y a plusieurs paramètres d'initialisation est de créer un type qui ne contient que les paramètres d'initialisation

public class DataTypeTwoParameters {
    public String foo;  // use getters/setters instead if it's appropriate
    public int bar;
    public double baz;
    public String quuz;
}

Ensuite, le constructeur de DataTypeTwo prend un objet DataTypeTwoParameters, et DataTypeTwo est construit via:

DataTypeTwoParameters p = new DataTypeTwoParameters();
p.foo = "Hello";
p.bar = 4;
p.baz = 3;
p.quuz = "World";

DataTypeTwo dtt = new DataTypeTwo(p);

Cela donne beaucoup d'occasions de préciser quels sont tous les paramètres entrant dans DataTypeTwo et ce qu'ils signifient. Vous pouvez également fournir des valeurs par défaut raisonnables dans le constructeur DataTypeTwoParameters afin que seules les valeurs qui doivent être définies puissent être effectuées dans l'ordre que le consommateur de l'API aime.

Erik
la source
Approche intéressante. Où mettriez-vous un sujet pertinent Integer.parseInt? Dans un setter, ou en dehors de la classe des paramètres?
durron597
5
En dehors de la classe des paramètres. La classe de paramètres doit être un objet "stupide" et ne doit pas essayer de faire autre chose que d'exprimer les entrées requises et leurs types. Parsing doit se faire ailleurs, comme: p.bar = Integer.parseInt("4").
Erik
7
cela ressemble à un motif d' objet de paramètre
moucher
9
... ou anti-motif.
Telastyn
1
... ou vous pouvez simplement renommer DataTypeTwoParametersà DataTypeTwo.
user253751
14

Vous avez vraiment deux préoccupations distinctes ici: encapsuler une API et maintenir le nombre d'arguments bas.

Lors de l'encapsulation d'une API, l'idée est de concevoir l'interface comme à partir de zéro, ne connaissant rien d'autre que les exigences. Vous dites qu'il n'y a rien de mal avec leur API, puis dans la même liste de souffle un certain nombre de choses qui ne vont pas avec leur API: testabilité, constructibilité, trop de paramètres dans un objet, etc. Écrivez l'API que vous souhaitez avoir. Si cela nécessite plusieurs objets au lieu d'un, faites-le. Si cela nécessite un habillage d'un niveau supérieur, vers les objets qui créent le POJO, faites-le.

Ensuite, une fois que vous avez votre API souhaitée, le nombre de paramètres peut ne plus être un problème. Si tel est le cas, il existe un certain nombre de modèles communs à considérer:

  • Un objet paramètre, comme dans la réponse d'Erik .
  • Le modèle de générateur , dans lequel vous créez un objet générateur distinct, puis appelez un certain nombre de paramètres pour définir les paramètres individuellement, puis créez votre objet final.
  • Le modèle prototype , où vous clonez des sous-classes de votre objet souhaité avec les champs déjà définis en interne.
  • Une usine que vous connaissez déjà.
  • Une combinaison de ce qui précède.

Notez que ces modèles de création finissent souvent par appeler un constructeur polyadique, ce que vous devriez considérer correct lorsqu'il est encapsulé. Le problème avec les constructeurs polyadiques n'est pas de les appeler une fois, c'est quand vous êtes obligé de les appeler chaque fois que vous avez besoin de construire un objet.

Notez qu'il est généralement beaucoup plus facile et plus facile de passer à l'API sous-jacente en stockant une référence à l' OurDataobjet et en transférant les appels de méthode, plutôt que d'essayer de réimplémenter ses internes. Par exemple:

public class DataTypeTwo implements DataInterface {
  private OurData data;

  public DataTypeOne(OurData data) {
    this.data = data;
  }

   public String getFoo() {
    return data.getFoo();
  }

  public int getBar() {
    return Integer.parseInt(data.getBar());
  }
  ...
}
Karl Bielefeldt
la source
Première moitié de cette réponse: super, très utile, +1. Deuxième moitié de cette réponse: «passez à l'API sous-jacente en stockant une référence à l' OurDataobjet» - c'est ce que j'essaie d'éviter, au moins dans la classe de base, pour s'assurer qu'il n'y a pas de dépendance.
durron597
1
C'est pourquoi vous ne le faites que dans l'une de vos implémentations de DataInterface. Vous créez une autre implémentation pour vos objets fictifs.
Karl Bielefeldt
@ durron597: oui, mais vous savez déjà comment résoudre ce problème s'il vous dérange vraiment.
Doc Brown
1

Je pense que vous interprétez peut-être trop strictement la recommandation de l'oncle Bob. Pour les classes normales, avec la logique et les méthodes et constructeurs et autres, un constructeur polyadique ressemble en effet beaucoup à l'odeur du code. Mais pour quelque chose qui est strictement un conteneur de données qui expose des champs, et qui est déjà généré par ce qui est essentiellement un objet Factory, je ne pense pas que ce soit trop mauvais.

Vous pouvez utiliser le modèle d'objet de paramètre, comme suggéré dans un commentaire, peut envelopper ces paramètres de constructeur pour vous, ce qu'est votre encapsuleur de type de données local est déjà , essentiellement, un objet de paramètre. Tout ce que votre objet Parameter fera, c'est emballer les paramètres (comment allez-vous les créer? Avec un constructeur polyadique?), Puis les décompresser une seconde plus tard dans un objet qui est presque identique.

Si vous ne voulez pas exposer les setters pour vos champs et les appeler, je pense que s'en tenir à un constructeur polyadique à l'intérieur d'une usine bien définie et encapsulée est très bien.

Avner Shahar-Kashtan
la source
Le problème est que le nombre de champs dans ma structure de données a changé plusieurs fois et changera probablement à nouveau. Ce qui signifie que je dois refactoriser le constructeur dans tous mes cas de test. Le modèle de paramètre avec des valeurs par défaut sensibles semble être une meilleure façon de procéder; avoir une version mutable qui est enregistrée sous la forme immuable pourrait me faciliter la vie de plusieurs façons.
durron597