Esiste un nome per il pattern (anti-) dei parametri di passaggio che verranno utilizzati solo a diversi livelli nella catena di chiamate?

205

Stavo cercando di trovare alternative all'uso della variabile globale in qualche codice legacy. Ma questa domanda non riguarda le alternative tecniche, sono principalmente preoccupato della terminologia .

La soluzione ovvia è passare un parametro nella funzione anziché utilizzare un globale. In questo legacy codebase ciò significherebbe che devo cambiare tutte le funzioni nella lunga catena di chiamate tra il punto in cui il valore sarà eventualmente usato e la funzione che riceve il parametro per primo.

higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)

dove newParam era precedentemente una variabile globale nel mio esempio, ma poteva invece essere un valore precedentemente codificato. Il punto è che ora il valore di newParam è ottenuto a higherlevel() e deve "viaggiare" fino a level3() .

Mi chiedevo se ci fosse un nome (i) per questo tipo di situazione / modello in cui è necessario aggiungere un parametro a molte funzioni che "passano" il valore non modificato.

Speriamo che l'uso della terminologia corretta mi consenta di trovare più risorse sulle soluzioni da riprogettare e di descrivere questa situazione ai colleghi.

    
posta ecerulm 31.10.2016 - 15:55
fonte

10 risposte

198

I dati stessi sono chiamati "dati di trasporto" . È un "odore di codice", che indica che un pezzo di codice comunica con un altro pezzo di codice a distanza, attraverso intermediari.

  • Aumenta la rigidità del codice, specialmente nella catena di chiamate. Sei molto più limitato nel modo di refactoring di qualsiasi metodo nella catena di chiamate.
  • Distribuisce la conoscenza di dati / metodi / architettura in luoghi che non se ne preoccupano minimamente. Se hai bisogno di dichiarare i dati che stanno appena attraversando e la dichiarazione richiede una nuova importazione, hai inquinato lo spazio dei nomi.

Il refactoring per rimuovere le variabili globali è difficile e i dati tramp sono un metodo per farlo, e spesso il modo più economico. Ha i suoi costi.

    
risposta data 31.10.2016 - 17:26
fonte
98

Non penso che questo, in sé, sia un anti-modello. Penso che il problema sia che stai pensando alle funzioni come a una catena quando in realtà dovresti pensare a ognuna come una scatola nera indipendente ( NOTA : i metodi ricorsivi sono un'eccezione notevole a questo consiglio.)

Ad esempio, supponiamo di dover calcolare il numero di giorni tra due date del calendario, quindi creo una funzione:

int daysBetween(Day a, Day b)

Per fare ciò, creo una nuova funzione:

int daysSinceEpoch(Day day)

Quindi la mia prima funzione diventa semplicemente:

int daysBetween(Day a, Day b)
{
    return daysSinceEpoch(b) - daysSinceEpoch(a);
}

Non c'è nulla di anti-modello su questo. I parametri del metodo daysBetween vengono passati a un altro metodo e non vengono mai referenziati diversamente nel metodo, ma sono comunque necessari per quel metodo per fare ciò che deve fare.

Quello che consiglierei è osservare ogni funzione e iniziare con un paio di domande:

  • Questa funzione ha un obiettivo chiaro e mirato o è un metodo "fai alcune cose"? Solitamente il nome della funzione aiuta qui e se ci sono cose che non sono descritte dal nome, questa è una bandiera rossa.
  • Ci sono troppi parametri? A volte un metodo può legittimamente richiedere molto input ma avere così tanti parametri lo rende gravoso da usare o capire.

Se stai cercando un guazzabuglio di codice senza un singolo scopo inserito in un metodo, dovresti iniziare a svelarlo. Questo può essere noioso. Inizia con le cose più facili da estrarre e spostati in un metodo separato e ripeti finché non hai qualcosa di coerente.

Se hai solo troppi parametri, considera Metodo di refactoring degli oggetti .

    
risposta data 31.10.2016 - 16:21
fonte
59

BobDalgleish ha già notato che questo pattern (anti-) è chiamato " dati di prova ".

Secondo la mia esperienza, la causa più comune di eccessivi dati di vagabondo è costituita da una serie di variabili di stato collegate che dovrebbero essere realmente incapsulate in un oggetto o in una struttura di dati. A volte, può anche essere necessario annidare un gruppo di oggetti per organizzare correttamente i dati.

Per un semplice esempio, considera un gioco con un personaggio giocatore personalizzabile, con proprietà come playerName , playerEyeColor e così via. Ovviamente, il giocatore ha anche una posizione fisica sulla mappa di gioco e varie altre proprietà come, ad esempio, il livello di salute attuale e massimo e così via.

In una prima iterazione di un gioco del genere, potrebbe essere una scelta perfettamente ragionevole per trasformare tutte queste proprietà in variabili globali - dopo tutto, c'è solo un giocatore, e quasi tutto nel gioco coinvolge in qualche modo il giocatore. Quindi il tuo stato globale potrebbe contenere variabili come:

playerName = "Bob"
playerEyeColor = GREEN
playerXPosition = -8
playerYPosition = 136
playerHealth = 100
playerMaxHealth = 100

Ma a un certo punto potresti scoprire che è necessario modificare questo disegno, forse perché vuoi aggiungere una modalità multiplayer al gioco. Come primo tentativo, potresti provare a rendere tutte quelle variabili locali e passarle a funzioni che ne hanno bisogno. Tuttavia, potresti scoprire che un'azione particolare nel tuo gioco potrebbe coinvolgere una catena di chiamate di funzioni come, ad esempio:

mainGameLoop()
 -> processInputEvent()
     -> doPlayerAction()
         -> movePlayer()
             -> checkCollision()
                 -> interactWithNPC()
                     -> interactWithShopkeeper()

... e la funzione interactWithShopkeeper() ha il negoziante indirizza il giocatore per nome, quindi ora devi improvvisamente passare playerName come dati tramp attraverso tutte quelle funzioni. E, naturalmente, se il negoziante pensa che i giocatori con gli occhi azzurri siano ingenui e addebiterà loro prezzi più alti, allora dovrai passare playerEyeColor attraverso l'intera catena di funzioni, e così via.

La soluzione corretta , in questo caso, è ovviamente quella di definire un oggetto giocatore che incapsula il nome, il colore degli occhi, la posizione, la salute e qualsiasi altra proprietà del personaggio del giocatore. In questo modo, devi solo passare quell'oggetto singolo a tutte le funzioni che in qualche modo coinvolgono il giocatore.

Inoltre, molte delle funzioni di cui sopra potrebbero essere naturalmente trasformate in metodi di quell'oggetto giocatore, che darebbe loro automaticamente l'accesso alle proprietà del giocatore. In un certo senso, questo è solo zucchero sintattico, dal momento che chiamare un metodo su un oggetto passa in modo efficace l'istanza dell'oggetto come parametro nascosto al metodo in ogni caso, ma rende il codice più chiaro e più naturale se usato correttamente.

Ovviamente, un gioco tipico avrebbe molto più stato "globale" del solo giocatore; ad esempio, quasi sicuramente avrai una sorta di mappa su cui si svolge il gioco e un elenco di personaggi non giocanti che si spostano sulla mappa, e magari oggetti posizionati su di esso, e così via. Potresti passare tutti quelli intorno come oggetti tramp, ma ciò riempirebbe di nuovo gli argomenti del tuo metodo.

Invece, la soluzione è di fare in modo che gli oggetti memorizzino riferimenti a qualsiasi altro oggetto con cui hanno relazioni permanenti o temporanee. Quindi, ad esempio, l'oggetto giocatore (e probabilmente anche qualsiasi oggetto NPC) probabilmente dovrebbe memorizzare un riferimento all'oggetto "mondo di gioco", che avrebbe un riferimento al livello / mappa corrente, in modo che un metodo come player.moveTo(x, y) lo faccia non è necessario che venga specificata esplicitamente la mappa come parametro.

Allo stesso modo, se il nostro personaggio avesse, per esempio, un cane che li seguiva, raggrupperemmo tutte le variabili di stato che descrivono il cane in un singolo oggetto e daremo al giocatore oggetto di riferimento al cane (in modo che il giocatore può, ad esempio, chiamare il cane per nome) e viceversa (in modo che il cane sappia dove si trova il giocatore). E, naturalmente, probabilmente vorremmo rendere il giocatore e il cane oggetti entrambi sottoclassi di un oggetto "attore" più generico, così da poter riutilizzare lo stesso codice per, diciamo, spostarci entrambi sulla mappa.

Ps. Anche se ho usato un gioco come esempio, ci sono altri tipi di programmi in cui emergono anche questi problemi. Nella mia esperienza, però, il problema sottostante tende ad essere sempre lo stesso: si hanno un mucchio di variabili separate (sia locali che globali) che vogliono davvero essere raggruppate in uno o più oggetti interconnessi. Se i "dati tramp" che invadono le tue funzioni sono costituiti da impostazioni di opzioni "globali" o query di database o vettori di stato nella cache in una simulazione numerica, la soluzione è invariabilmente per identificare il contesto naturale a cui appartengono i dati e trasformalo in un oggetto (o qualunque sia l'equivalente più vicino nella lingua scelta).

    
risposta data 31.10.2016 - 23:39
fonte
33

Non sono a conoscenza di un nome specifico per questo, ma suppongo che valga la pena menzionare che il problema che descrivi è solo il problema di trovare il miglior compromesso per l'ambito di tale parametro:

  • come variabile globale, l'ambito è troppo grande quando il programma raggiunge una certa dimensione

  • come parametro puramente locale, l'ambito potrebbe essere troppo piccolo, quando porta a molti elenchi di parametri ripetitivi nelle catene di chiamata

  • così come un trade-off, puoi spesso impostare un parametro di questo tipo come variabile membro in una o più classi, e questo è ciò che chiamerei design della classe appropriato .

risposta data 31.10.2016 - 16:38
fonte
21

Credo che lo schema che stai descrivendo sia esattamente iniezione di dipendenza . Diversi commentatori hanno sostenuto che questo è un modello , non un anti-pattern , e tenderei a essere d'accordo.

Concordo anche con la risposta di @ JimmyJames, dove afferma che è buona pratica di programmazione trattare ogni funzione come una scatola nera che prende tutti i suoi input come parametri espliciti. Cioè, se stai scrivendo una funzione che crea un sandwich di burro di arachidi e gelatina, potresti scriverlo come

Sandwich make_sandwich() {
    PeanutButter pb = get_peanut_butter();
    Jelly j = get_jelly();
    return pb + j;
}
extern PhysicalRefrigerator g_refrigerator;
PeanutButter get_peanut_butter() {
    return g_refrigerator.get("peanut butter");
}
Jelly get_jelly() {
    return g_refrigerator.get("jelly");
}

ma sarebbe migliore pratica per applicare l'iniezione di dipendenza e scriverlo in questo modo:

Sandwich make_sandwich(Refrigerator& r) {
    PeanutButter pb = get_peanut_butter(r);
    Jelly j = get_jelly(r);
    return pb + j;
}
PeanutButter get_peanut_butter(Refrigerator& r) {
    return r.get("peanut butter");
}
Jelly get_jelly(Refrigerator& r) {
    return r.get("jelly");
}

Ora hai una funzione che documenta chiaramente tutte le sue dipendenze nella sua firma di funzione, il che è ottimo per la leggibilità. Dopo tutto, è vero che per make_sandwich devi accedere a Refrigerator ; quindi la firma della vecchia funzione era fondamentalmente disonesta non prendendo il frigorifero come parte dei suoi ingressi.

Come bonus, se fai la tua gerarchia di classi a destra, evita di affettare e così via, puoi anche testare unitamente la funzione make_sandwich passando a MockRefrigerator ! (Potrebbe essere necessario testare l'unità in questo modo perché l'ambiente di test delle unità potrebbe non avere accesso a qualsiasi PhysicalRefrigerator s.)

Capisco che non tutti gli usi dell'iniezione di dipendenza richiedono plumbing un parametro con lo stesso nome di molti livelli nello stack delle chiamate, quindi non sto rispondendo esattamente alla domanda chiesto ... ma se stai cercando ulteriori letture su questo argomento, "l'iniezione di dipendenza" è sicuramente una parola chiave rilevante per te.

    
risposta data 31.10.2016 - 22:27
fonte
15

Questa è praticamente la definizione da manuale di accoppiamento , un modulo che ha una dipendenza che influenza profondamente un altro e che crea un effetto a catena quando viene modificato. Gli altri commenti e le risposte sono corrette sul fatto che questo è un miglioramento rispetto al globale, perché l'accoppiamento è ora più esplicito e più facile da vedere per il programmatore, invece di sovversivo. Ciò non significa che non dovrebbe essere risolto. Dovresti essere in grado di refactoring per rimuovere o ridurre l'accoppiamento, anche se se è stato lì un po 'può essere doloroso.

    
risposta data 31.10.2016 - 17:16
fonte
3

Sebbene questa risposta non direttamente risponda alla tua domanda, sento che sarei negligente nel lasciarlo passare senza menzionare come migliorare su di esso (poiché come dici tu, potrebbe essere un anti- modello). Spero che tu e altri lettori possiate ricavare il valore da questo commento aggiuntivo su come evitare "i dati del vagabondo" (come Bob Dalgleish ha così utile chiamarlo per noi).

Sono d'accordo con le risposte che suggeriscono di fare qualcosa di più OO per evitare questo problema. Tuttavia, un altro modo per aiutare a ridurre questo passaggio di argomenti in profondità senza semplicemente saltare a " basta passare una classe dove si passavano molti argomenti! " è quello di refactoring in modo che alcuni passaggi del processo avvengano in il livello più alto invece di quello inferiore. Ad esempio, ecco alcuni prima codice:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   FilterAndReportStuff(stuffs, desiredName);
}

public void FilterAndReportStuff(IEnumerable<Stuff> stuffs, string desiredName) {
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   ReportStuff(stuffs.Filter(filter));
}

public void ReportStuff(IEnumerable<Stuff> stuffs) {
   stuffs.Report();
}

Nota che ciò diventa ancora peggio quanto più cose devono essere fatte in ReportStuff . Potrebbe essere necessario passare l'istanza del Reporter che si desidera utilizzare. E tutti i tipi di dipendenze che devono essere passati insieme, funzionano con funzioni annidate.

Il mio suggerimento è di portare tutto a un livello superiore, dove la conoscenza dei passaggi richiede la vita in un singolo metodo invece di essere diffusa attraverso una catena di chiamate di metodo. Ovviamente sarebbe più complicato nel codice reale, ma questo ti dà un'idea:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   var filteredStuffs = stuffs.Filter(filter)
   filteredStuffs.Report();
}

Si noti che la grande differenza qui è che non è necessario passare le dipendenze attraverso una lunga catena. Anche se appiattisci non solo un livello, ma alcuni livelli in profondità, se anche questi livelli raggiungono un certo "appiattimento" in modo che il processo sia visto come una serie di passaggi a quel livello, avrai fatto un miglioramento.

Anche se questo è ancora procedurale e nulla è stato trasformato in un oggetto ancora, è un buon passo verso la decisione di quale tipo di incapsulamento si può ottenere trasformando qualcosa in una classe. Il metodo incatenato chiama nello scenario prima nasconde i dettagli di ciò che sta realmente accadendo e può rendere il codice molto difficile da capire. Mentre puoi esagerare con questo e finire con il fare conoscere al codice di livello superiore cose che non dovrebbero, o fare un metodo che fa troppe cose violando così il principio di responsabilità singola, in generale ho trovato che appiattire le cose aiuta un po ' in chiarezza e nel fare cambiamenti incrementali verso un codice migliore.

Nota che mentre stai facendo tutto questo, dovresti considerare la testabilità. Le chiamate al metodo concatenato rendono effettivamente il test dell'unità più difficile perché non si dispone di un buon punto di ingresso e di un punto di uscita nell'assieme per la sezione che si desidera testare. Nota che con questo appiattimento, dato che i tuoi metodi non impiegano più dipendenze, sono più facili da testare, non richiedono altrettanti mock!

Recentemente ho provato ad aggiungere unit test a una classe (che non ho scritto) che ha preso qualcosa come 17 dipendenze, tutte cose che hanno dovuto essere derise! Non ho ancora capito tutto, ma ho diviso la classe in tre classi, ognuna delle quali si occupava di uno dei nomi separati di cui si occupava, e ho ottenuto l'elenco delle dipendenze fino a 12 per il peggiore e circa 8 per il il migliore.

La testabilità ti costringerà a scrivere un codice migliore. Dovresti scrivere dei test unitari perché scoprirai che ti fa pensare al tuo codice in modo diverso e scriverai un codice migliore fin dall'inizio, a prescindere dal numero di bug che potresti aver avuto prima di scrivere i test unitari.

    
risposta data 02.11.2016 - 04:03
fonte
1

Non stai letteralmente violando la legge di Demeter, ma il tuo problema è simile a quello in qualche modo. Poiché il punto della tua domanda è trovare risorse, ti suggerisco di leggere su Law of Demeter e vedere quanta parte di questo consiglio si applica alla tua situazione.

    
risposta data 31.10.2016 - 20:59
fonte
1

Ci sono casi in cui il meglio (in termini di efficienza, manutenibilità e facilità di implementazione) di avere certe variabili come globali piuttosto che il sovraccarico di passare sempre tutto intorno (diciamo che hai 15 o più variabili che devono persistere). Quindi ha senso trovare un linguaggio di programmazione che supporti meglio lo scope (come variabili statiche private del C ++) per alleviare il potenziale pasticcio (dello spazio dei nomi e delle manomissioni). Ovviamente questa è solo una conoscenza comune.

Tuttavia, l'approccio indicato dall'OP è molto utile se si sta facendo una programmazione funzionale.

    
risposta data 01.11.2016 - 05:18
fonte
0

Non c'è nessun anti-modello qui, perché il chiamante non conosce tutti questi livelli sotto e non gli importa.

Qualcuno sta chiamando higherLevel (params) e si aspetta che higherLevel faccia il suo lavoro. Ciò che higherLevel fa con params non è il business dei chiamanti. higherLevel gestisce il problema nel modo migliore possibile, in questo caso passando i parametri al livello1 (parametri). Questo è assolutamente Ok.

Viene visualizzata una catena di chiamate, ma non esiste una catena di chiamate. C'è una funzione in cima a fare il suo lavoro nel miglior modo possibile. E ci sono altre funzioni. Ogni funzione può essere sostituita in qualsiasi momento.

    
risposta data 12.07.2017 - 10:28
fonte