Come si può mantenere basso il numero degli argomenti e mantenere separate le dipendenze di terze parti?


13

Uso una libreria di terze parti. Mi passano un POJO che, per i nostri intenti e scopi, è probabilmente implementato in questo modo:

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

Per molte ragioni, incluso ma non limitato all'incapsulamento della loro API e alla facilitazione del test unitario, voglio racchiudere i loro dati. Ma non voglio che le mie classi principali dipendano dai loro dati (di nuovo, per motivi di test)! Quindi in questo momento ho qualcosa del genere:

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;
  }
}

E poi questo:

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();
      );
  }
}

Questa classe dell'adattatore è accoppiata con le altre poche classi che DEVONO conoscere l'API di terze parti, limitando la sua pervasività attraverso il resto del mio sistema. Tuttavia ... questa soluzione è LORDA! In Codice pulito, pagina 40:

Più di tre argomenti (poliadici) richiedono una giustificazione molto speciale - e quindi non dovrebbero essere usati comunque.

Cose che ho considerato:

  • Creazione di un oggetto factory anziché un metodo di supporto statico
    • Non risolve il problema di avere un bajillion di argomenti
  • Creazione di una sottoclasse di DataTypeOne e DataTypeTwo che ha un costruttore dipendente
    • Ha ancora un costruttore poliadico protetto
  • Crea implementazioni completamente separate conformi alla stessa interfaccia
  • Molteplici delle idee precedenti contemporaneamente

Come dovrebbe essere gestita questa situazione?


Nota che questa non è una situazione di livello anticorruzione . Non c'è niente di sbagliato con la loro API. I problemi sono:

  • Non voglio che le MIE strutture dati abbiano import com.third.party.library.SomeDataStructure;
  • Non riesco a costruire le loro strutture di dati nei miei casi di test
  • La mia soluzione attuale ha come risultato conteggi di argomenti molto elevati. Voglio mantenere bassi i conteggi degli argomenti, SENZA passare nelle loro strutture di dati.
  • Quella domanda è " cos'è uno strato anticorruzione?". La mia domanda è " come posso usare un modello, qualsiasi modello, per risolvere questo scenario?"

Nemmeno io sto chiedendo il codice (altrimenti questa domanda sarebbe su SO), sto solo chiedendo abbastanza di una risposta per permettermi di scrivere il codice in modo efficace (che quella domanda non fornisce).


Se esistono diversi POJO di terze parti, potrebbe valere la pena scrivere codice di test personalizzato che utilizza una mappa con alcune convenzioni (ad esempio, denominare i tasti int_bar) come input del test. Oppure usa JSON o XML con un codice intermedio personalizzato. In effetti, una specie di DSL per testare com.thirdparty.
user949300

La citazione completa da 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'adesione cieca a un modello o una linea guida di programmazione è il suo stesso anti-modello .
Lilienthal,

2
"incapsulando la loro API e facilitando i test unitari" Sembra che questo potrebbe essere un caso di over-test e / o danni alla progettazione indotti dal test (o indicativo che potresti progettare questo in modo diverso per cominciare). Chiediti questo: questo semplifica davvero la comprensione, la modifica e il riutilizzo del codice? Metterei i miei soldi su "no". Quanto è realisticamente probabile che tu abbia mai scambiato questa libreria? Probabilmente non molto. Se lo sostituisci, ciò rende davvero più semplice eliminarne uno completamente diverso? Ancora una volta, scommetterei su "no".
jpmc26,

1
@JamesAnderson Ho appena riprodotto la citazione completa perché l'ho trovata interessante ma non mi era chiaro dallo snippet se si riferisse a funzioni in generale o ai costruttori in particolare. Non intendevo approvare l'affermazione e, come ha detto jpmc26, il mio prossimo commento dovrebbe darti qualche indicazione che non lo stavo facendo. Non sono sicuro del motivo per cui senti il ​​bisogno di attaccare gli accademici, ma l'uso di polisillabi non rende qualcuno un elitario accademico appollaiato sulla sua torre d'avorio sopra le nuvole.
Lilienthal,

Risposte:


10

La strategia che ho usato quando ci sono diversi parametri di inizializzazione è quella di creare un tipo che contenga solo i parametri per l'inizializzazione

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

Quindi il costruttore per DataTypeTwo accetta un oggetto DataTypeTwoParameters e DataTypeTwo viene creato tramite:

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

DataTypeTwo dtt = new DataTypeTwo(p);

Ciò offre molte opportunità per chiarire quali sono tutti i parametri in DataTypeTwo e cosa significano. Puoi anche fornire valori predefiniti ragionevoli nel costruttore DataTypeTwoParameters in modo che solo i valori che devono essere impostati possano essere eseguiti in qualsiasi ordine che piaccia al consumatore dell'API.


Approccio interessante Dove vorresti mettere un rilevante Integer.parseInt? In un setter o al di fuori della classe dei parametri?
durron597,

5
Fuori dalla classe dei parametri. La classe dei parametri dovrebbe essere un oggetto "stupido" e non dovrebbe tentare di fare altro che esprimere quali sono gli input richiesti e i loro tipi. Di analisi dovrebbe essere fatto altrove, come: p.bar = Integer.parseInt("4").
Erik,

7
questo suona come un modello di oggetto parametro
moscerino

9
... o anti-pattern.
Telastyn,

1
... o si può solo cambiare titolo DataTypeTwoParametersal DataTypeTwo.
user253751

14

Hai davvero due preoccupazioni separate qui: racchiudere un'API e mantenere basso il conteggio degli argomenti.

Quando si racchiude un'API, l'idea è di progettare l'interfaccia come da zero, non conoscendo altro che i requisiti. Dici che non c'è niente di sbagliato nella loro API, quindi nella stessa lista di respiro una serie di cose che non vanno nella loro API: testabilità, costruibilità, troppi parametri in un oggetto, ecc. Scrivi l'API che desideri avere. Se ciò richiede più oggetti invece di quello, fallo. Se richiede il wrapping di un livello superiore, agli oggetti che creano il POJO, fallo.

Quindi, una volta ottenuta l'API desiderata, il conteggio dei parametri potrebbe non essere più un problema. Se lo è, ci sono una serie di schemi comuni da considerare:

  • Un oggetto parametro, come nella risposta di Erik .
  • Il modello builder , in cui si crea un oggetto builder separato, quindi si chiama un numero di setter per impostare i parametri singolarmente, quindi creare l'oggetto finale.
  • Il modello prototipo , in cui clonare sottoclassi dell'oggetto desiderato con i campi già impostati internamente.
  • Una fabbrica, che conosci già.
  • Una combinazione di quanto sopra.

Nota che questi schemi creazionali finiscono spesso per chiamare un costruttore poliadico, che dovresti considerare a posto quando è incapsulato. Il problema con i costruttori poliadici non li chiama una volta, è quando sei costretto a chiamarli ogni volta che devi costruire un oggetto.

Si noti che in genere è molto più semplice e più gestibile passare all'API sottostante memorizzando un riferimento OurDataall'oggetto e inoltrando le chiamate del metodo, piuttosto che provare a reimplementare i suoi interni. Per esempio:

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

Prima metà di questa risposta: eccezionale, molto utile, +1. Seconda metà di questa risposta: "passare all'API sottostante memorizzando un riferimento OurDataall'oggetto": questo è ciò che sto cercando di evitare, almeno nella classe base, per garantire che non vi siano dipendenze.
durron597,

1
Ecco perché lo fai solo in una delle tue implementazioni di DataInterface. Crei un'altra implementazione per i tuoi oggetti finti.
Karl Bielefeldt,

@ durron597: sì, ma sai già come risolvere quel problema se ti disturba davvero.
Doc Brown,

1

Penso che potresti interpretare la raccomandazione di zio Bob troppo rigorosamente. Per le classi normali, con logica e metodi e costruttori e simili, un costruttore poliadico sembra davvero molto simile all'odore del codice. Ma per qualcosa che è rigorosamente un contenitore di dati che espone i campi ed è generato da quello che è essenzialmente un oggetto Factory già, non penso che sia troppo male.

È possibile utilizzare il modello Oggetto parametro, come suggerito in un commento, può racchiudere questi parametri del costruttore per te, quale sia il wrapper del tipo di dati locale è già , essenzialmente, un oggetto Parametro. Tutto ciò che farà l'oggetto Parameter è impacchettare i parametri (Come lo creerai? Con un costruttore poliadico?) E poi scompattarli un secondo dopo in un oggetto che è quasi identico.

Se non vuoi esporre setter per i tuoi campi e chiamarli, penso che attenersi a un costruttore poliadico all'interno di una fabbrica ben definita e incapsulata vada bene.


Il problema è che il numero di campi nella mia struttura dati è cambiato più volte e probabilmente cambierà di nuovo. Ciò significa che devo refactificare il costruttore in tutti i miei casi di test. Il modello di parametri con valori predefiniti sensibili suona come un modo migliore per andare; avere una versione mutabile che viene salvata nella forma immutabile potrebbe semplificarmi la vita in molti modi.
durron597,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.