Perché la codifica dei nomi degli argomenti nei nomi delle funzioni non è più comune? [chiuso]


47

In Clean Code l'autore fornisce un esempio di

assertExpectedEqualsActual(expected, actual)

vs

assertEquals(expected, actual)

con il primo dichiarato più chiaro perché elimina la necessità di ricordare dove vanno gli argomenti e il potenziale abuso che ne deriva. Tuttavia, non ho mai visto un esempio del precedente schema di denominazione in nessun codice e vedo quest'ultimo per tutto il tempo. Perché i programmatori non adottano il primo se è, come afferma l'autore, più chiaro del secondo?


9
Penso che questa sia un'ottima domanda per una discussione. Ma non qualcosa a cui si possa rispondere con una risposta obiettiva. Quindi questa domanda potrebbe essere chiusa come basata sull'opinione.
Euforico

54
Molte persone discuterebbero contro il primo schema di denominazione perché è eccessivamente prolisso , ben oltre il punto in cui favorirebbe la chiarezza. Soprattutto assertEquals(), quel metodo viene usato centinaia di volte in una base di codice, quindi ci si può aspettare che i lettori familiarizzino con la convenzione una volta. Strutture diverse hanno convenzioni diverse (ad es. (actual, expected) or an agnostic (Sinistra, destra) `), ma nella mia esperienza questa è al massimo una fonte minore di confusione.
amon,

5
Perché il guadagno è così piccolo, rispetto ai suoi benefici, che qualsiasi persona sana di mente probabilmente si allontanerebbe. Se desideri un approccio più fluido , dovresti provare assert(a).toEqual(b)(anche se l'IMO è ancora inutilmente prolisso) in cui puoi concatenare poche asserzioni correlate.
Adriano Repetti,

18
Come facciamo a sapere che i valori attuali e previsti sono valori? Sicuramente dovrebbe essere assertExpectedValueEqualsActualValue? Ma aspetta, come ci ricordiamo se utilizza ==o .equalso Object.equals? Dovrebbe essere assertExpectedValueEqualsMethodReturnsTrueWithActualValueParameter?
user253751

6
Dato che, per questo particolare metodo, l'ordine dei due argomenti non ha importanza, sembra un cattivo esempio scegliere di sposare i vantaggi di questo schema di denominazione.
Steven Rands,

Risposte:


66

Perché è più da scrivere e più da leggere

Il motivo più semplice è che alle persone piace digitare di meno e codificare tali informazioni significa scrivere di più. Quando lo leggo, ogni volta che devo leggere tutto, anche se ho familiarità con quale dovrebbe essere l'ordine degli argomenti. Anche se non ha familiarità con l'ordine degli argomenti ...

Molti sviluppatori usano gli IDE

Gli IDE forniscono spesso un meccanismo per visualizzare la documentazione per un determinato metodo passando con il mouse o tramite una scorciatoia da tastiera. Per questo motivo, i nomi dei parametri sono sempre a portata di mano.

La codifica degli argomenti introduce duplicazione e accoppiamento

I nomi dei parametri dovrebbero già documentare ciò che sono. Scrivendo i nomi nel nome del metodo, stiamo duplicando anche quelle informazioni nella firma del metodo. Creiamo anche un accoppiamento tra il nome del metodo e i parametri. Dì expectede actualconfondi i nostri utenti. Passare da assertEquals(expected, actual)a assertEquals(planned, real)non richiede la modifica del codice client utilizzando la funzione. Passare da assertExpectedEqualsActual(expected, actual)a assertPlannedEqualsReal(planned, real)significa cambiare in modo sostanziale l'API. Oppure non cambiamo il nome del metodo, che diventa rapidamente confuso.

Usa tipi invece di argomenti ambigui

Il vero problema è che abbiamo argomenti ambigui che possono essere facilmente scambiati perché sono dello stesso tipo. Possiamo invece utilizzare il nostro sistema di tipi e il nostro compilatore per applicare l'ordine corretto:

class Expected<T> {
    private T value;
    Expected(T value) { this.value = value; }
    static Expected<T> is(T value) { return new Expected<T>(value); }
}

class Actual<T> {
    private T value;
    Actual(T value) { this.value = value; }
    static Actual<T> is(T value) { return new Actual<T>(value); }
}

static assertEquals(Expected<T> expected, Actual<T> actual) { /* ... */ }

// How it is used
assertEquals(Expected.is(10), Actual.is(x));

Questo può quindi essere applicato a livello di compilatore e garantisce che non è possibile ripristinarli. Avvicinandosi da una prospettiva diversa, questo è essenzialmente ciò che la libreria Hamcrest fa per i test.


5
Bene, se usi un IDE hai i nomi dei parametri nella guida del fumetto; se non ne usi uno, ricordare il nome della funzione equivale a ricordare gli argomenti, quindi non si ottiene nulla in entrambi i modi.
Peter - Ripristina Monica il

29
Se ti opponi a assertExpectedEqualsActual"perché è più da scrivere e più da leggere", come puoi difendere assertEquals(Expected.is(10), Actual.is(x))?
Ruakh

9
@ruakh non è paragonabile. assertExpectedEqualsActualrichiede ancora al programmatore di occuparsi di specificare gli argomenti nel giusto ordine. La assertEquals(Expected<T> expected, Actual<T> actual)firma utilizza il compilatore per imporre l'utilizzo corretto, che è un approccio completamente diverso. Puoi ottimizzare questo approccio per brevità, ad esempio expect(10).equalsActual(x), ma non era questa la domanda ...
Holger,

6
Inoltre, in questo caso particolare (==), l'ordine degli argomenti è in realtà irrilevante per il valore finale. L'ordine conta solo per un effetto collaterale (segnalazione del fallimento). Quando si ordinano le cose, può avere (marginalmente) più senso. Ad esempio strcpy (dest, src).
Kristian H,

1
Non posso essere più d'accordo, specialmente con la parte relativa alla duplicazione e all'accoppiamento ... Se ogni volta che un parametro di funzione cambia il suo nome, anche il nome della funzione dovrebbe cambiare, dovresti tracciare tutti gli usi di quella funzione e cambiarli pure ... Ciò farebbe una marea di cambiamenti inaspettati per me, la mia squadra e tutti gli altri usando il nostro codice come dipendenza ...
mrsmn

20

Chiedete un lungo dibattito nella programmazione. Quanta verbosità è buona? Come risposta generale, gli sviluppatori hanno scoperto che non è valsa la pena la prolissità aggiuntiva che nomina gli argomenti.

La verbosità non significa sempre maggiore chiarezza. Tener conto di

copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)

contro

copy(output, source)

Entrambi contengono lo stesso bug, ma abbiamo effettivamente reso più facile trovare quel bug? Come regola generale, la cosa più semplice da eseguire il debug è quando tutto è al massimo conciso, tranne le poche cose che hanno il bug, e quelle sono abbastanza dettagliate per dirti cosa è andato storto.

C'è una lunga storia di aggiunta di verbosità. Ad esempio, c'è la " notazione ungherese " generalmente impopolare che ci ha dato nomi meravigliosi come lpszName. Ciò è generalmente caduto a margine della popolazione del programmatore generale. Tuttavia, l'aggiunta di caratteri ai nomi delle variabili dei membri (come mNameo m_Nameo name_) continua ad avere popolarità in alcune cerchie. Altri lo hanno abbandonato del tutto. Mi capita di lavorare su una base di codice di simulazione fisica i cui documenti di stile di codifica richiedono che qualsiasi funzione che restituisce un vettore deve specificare il frame del vettore nella funzione call ( getPositionECEF).

Potresti essere interessato ad alcune delle lingue rese popolari da Apple. Objective-C include i nomi degli argomenti come parte della firma della funzione (La funzione [atm withdrawFundsFrom: account usingPin: userProvidedPin]è scritta nella documentazione come withdrawFundsFrom:usingPin:. Questo è il nome della funzione). Swift ha preso una serie di decisioni simili, richiedendo di inserire i nomi degli argomenti nelle chiamate di funzione ( greet(person: "Bob", day: "Tuesday")).


13
A parte tutti gli altri punti, sarebbe molto più facile da leggere se copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)fossero scritti copy_from_source_stream_to_destination_stream_without_blocking(file_stream_from_choose_preferred_output_dialog, heuristically_decided_source_file_handle). Vedi quanto è stato più facile ?! Questo perché è troppo facile perdere piccoli cambiamenti a metà di quella humungousunbrokenwordsalad, e ci vuole più tempo per capire dove si trovano i confini delle parole. Smashing confonde.
tchrist,

1
La sintassi obj-C withdrawFundsFrom: account usingPin: userProvidedPinè in realtà presa in prestito da SmallTalk.
joH1

14
@tchrist stai attento ad essere certo di avere ragione su argomenti che riguardano le guerre sante. L'altro lato non è sempre sbagliato.
Cort Ammon,

3
@tchrist Addingunderscoresnakesthingseasiertoreadnotharderasyouseesta manipolando l'argomento. La risposta qui ha usato le maiuscole, che stai omettendo. AddingCapitalizationMakesThingsEasyEnoughToReadAsYouCanSeeHere. In secondo luogo, 9 volte su 10, un nome non dovrebbe mai crescere oltre [verb][adjective][noun](dove ogni blocco è opzionale), un formato che è ben leggibile usando la semplice maiuscola:ReadSimpleName
Flater

5
@tchrist - la scienza del tuo studio ( link full text gratuito ) mostra semplicemente che i programmatori addestrati ad usare lo stile di sottolineatura sono più veloci nella lettura dello stile di sottolineatura rispetto al caso del cammello. I dati mostrano anche che la differenza è minore per le materie più esperte (e la maggior parte delle materie essendo studenti suggerisce che anche quelle probabilmente non erano particolarmente esperte). Questo non significa che anche i programmatori che hanno trascorso più tempo ad usare il caso dei cammelli daranno lo stesso risultato.
Jules,

8

L'autore di "Clean Code" sottolinea un problema legittimo, ma la sua soluzione suggerita è piuttosto inelegante. Di solito ci sono modi migliori per migliorare i nomi dei metodi poco chiari.

Ha ragione nel dire che assertEquals(dalle librerie di unit test di stile xUnit) non si chiarisce quale argomento sia l'atteso e quale sia l'effettivo. Anche questo mi ha morso! Molte librerie di unit test hanno notato il problema e hanno introdotto sintassi alternative, come:

actual.Should().Be(expected);

O simili. Che è sicuramente molto più chiaro di assertEqualsma anche molto meglio di assertExpectedEqualsActual. Ed è anche molto più compostabile.


1
Sono anale e seguo l'ordine raccomandato, ma mi sembra che se mi aspetto che il risultato fun(x)sia 5, cosa potrebbe andare storto se invertire l'ordine - assert(fun(x), 5)? Come ti ha morso?
emory

3
@emory So che jUnit (almeno) crea un messaggio di errore attraverso i valori di expectede actual, quindi invertirli potrebbe risultare in un messaggio non accurato. Ma sono d'accordo che suona più naturale però :)
joH1

@ joH1 mi sembra debole. il codice non riuscito fallirà e il codice di passaggio passerà se lo fai assert(expected, observed)o assert(observed, expected). Un esempio migliore potrebbe essere qualcosa del genere locateLatitudeLongitude: se inverti le coordinate, si incasinerà seriamente.
emory

1
@emory Le persone che non si preoccupano dei messaggi di errore sensibili nei test unitari sono il motivo per cui ho a che fare con "Assert.IsTrue fallito" in alcune vecchie basi di codice. È estremamente divertente eseguire il debug. Ma sì, in questo caso il problema potrebbe non essere così essenziale (tranne se facciamo confronti sfocati dove l'ordine degli argomenti in genere è importante). Affermazioni fluide sono davvero un ottimo modo per evitare questo problema e anche rendere il codice più espressivo (e fornire un messaggio di errore molto migliore per l'avvio).
Voo

@emory: l'inversione dell'argomento renderà i messaggi di errore fuorvianti e ti invierà nel percorso sbagliato durante il debug.
JacquesB,

5

Stai cercando di orientare il tuo percorso tra Scilla e Cariddi verso la chiarezza, cercando di evitare inutili verbosità (noto anche come vagabondaggio senza scopo) così come eccessiva brevità (nota anche come terseness criptico).

Quindi, dobbiamo guardare l'interfaccia che si desidera valutare, un modo per fare asserzioni di debug secondo cui due oggetti sono uguali.

  1. C'è qualche altra funzione che potrebbe prendere in considerazione arità e nome?
    No, quindi il nome stesso è abbastanza chiaro.
  2. I tipi hanno qualche significato?
    No, quindi ignoriamoli. L'hai già fatto? Buono.
  3. È simmetrico nei suoi argomenti?
    Quasi, in caso di errore, il messaggio mette ogni rappresentazione degli argomenti nella propria posizione.

Quindi, vediamo se questa piccola differenza ha qualche significato e non è coperta dalle convenzioni forti esistenti.

Il pubblico previsto è disturbato se gli argomenti vengono scambiati involontariamente?
No, anche gli sviluppatori ottengono una traccia dello stack e devono comunque esaminare attentamente il codice sorgente per correggere il bug.
Anche senza una traccia dello stack completa, la posizione delle asserzioni risolve tale domanda. E se anche quello manca e non è ovvio dal messaggio che è quale, al massimo raddoppia le possibilità.

L'ordine degli argomenti segue la convenzione?
Sembra essere il caso. Anche se nella migliore delle ipotesi sembra una convenzione debole.

Pertanto, la differenza sembra piuttosto insignificante, e l'ordine degli argomenti è coperto da una convenzione sufficientemente forte che qualsiasi sforzo per metterlo nel nome-funzione ha utilità negativa.


bene l'ordine potrebbe importare con jUnit, che crea un messaggio di errore specifico dai valori di expectede actual(almeno con Strings)
joH1

Penso di aver coperto quella parte ...
Deduplicatore

l'hai menzionato ma considera: assertEquals("foo", "doo")dà il messaggio di errore è ComparisonFailure: expected:<[f]oo> but was:<[d]oo>... Scambiare i valori invertirebbe il significato del messaggio, che suona più anti simmetrico per me. Comunque, come hai detto, uno sviluppatore ha altri indicatori per risolvere l'errore, ma può essere fuorviante IMHO e richiedere un po 'più di tempo di debug.
joH1

L'idea che esista una "convenzione" per gli ordini degli argomenti è divertente, considerando che entrambi i campi (dest, src vs. src, dest) ne hanno discusso almeno finché esiste la sintassi AT&T vs. Intel. E i messaggi di errore inutili nei test unitari sono una piaga che dovrebbe essere sradicata e non forzata. È quasi come "Assert.IsTrue failed" ("hey devi comunque eseguire il test unitario per eseguirne il debug, quindi eseguilo di nuovo e mettici un punto di interruzione", "hey devi comunque guardare il codice, quindi controlla se l'ordine è corretto ").
Voo

@Voo: Il punto è che il "danno" per sbagliarlo è minuscolo (la logica non dipende da esso, e l'utilità dei messaggi non è compromessa in modo significativo), e quando si scrive l'IDE ti mostrerà il nome dei parametri e digita comunque.
Deduplicatore

3

Spesso non aggiunge alcuna chiarezza logica.

Confronta "Aggiungi" con "AggiungiFirstArgumentToSecondArgument".

Se hai bisogno di un sovraccarico che, diciamo, aggiunge tre valori. Cosa avrebbe più senso?

Un altro "Aggiungi" con tre argomenti?

o

"AddFirstAndSecondAndThirdArgument"?

Il nome del metodo dovrebbe trasmettere il suo significato logico. Dovrebbe dire cosa fa. Raccontare, a livello micro, quali passi prende non semplifica il lettore. I nomi degli argomenti forniranno ulteriori dettagli se necessario. Se hai ancora bisogno di maggiori dettagli, il codice sarà lì per te.


4
Addsuggerisce un'operazione commutativa. Il PO si occupa di situazioni in cui l'ordine conta.
Rosie F,

In Swift, ad esempio, si chiamerebbe add (5, a: x) o si aggiungerà (5, più: 7, a: x) o si aggiungerà (5, più: 7, dando: x) se si definisce la funzione add () di conseguenza.
gnasher729,

Il terzo sovraccarico dovrebbe essere chiamato "Sum"
StingyJack

@StringyJack Hmm .. Sum non è un'istruzione, è un sostantivo che lo rende meno adatto per un nome di metodo. Ma se la pensi così e se vuoi essere un purista al riguardo, anche la versione a due argomenti dovrebbe essere chiamata Sum. Se dovessi avere un metodo Add, dovrebbe avere un argomento che viene aggiunto all'istanza dell'oggetto stesso (che dovrebbe essere un tipo numerico o vettoriale). Le 2 o più varietà di argomenti (come le chiameresti) sarebbero statiche. Quindi le 3 o più versioni degli argomenti sarebbero ridondanti e avremmo implementato un operatore positivo: - |
Martin Maat,

1
@Martin Aspetta cosa? sumè un verbo perfettamente cromulento . È particolarmente comune nella frase "riassumere".
Voo

2

Vorrei aggiungere qualcos'altro che è accennato da altre risposte, ma non credo sia stato menzionato esplicitamente:

@puck dice "Non c'è ancora alcuna garanzia che il primo argomento menzionato nel nome della funzione sia davvero il primo parametro".

@cbojar dice "Usa tipi invece di argomenti ambigui"

Il problema è che i linguaggi di programmazione non capiscono i nomi: sono solo trattati come simboli atomici opachi. Quindi, come con i commenti sul codice, non esiste necessariamente alcuna correlazione tra il nome di una funzione e il modo in cui funziona effettivamente.

Confronta assertExpectedEqualsActual(foo, bar)con alcune alternative (da questa pagina e altrove), come:

# Putting the arguments in a labelled structure
assertEquals({expected: foo, actual: bar})

# Using a keyword arguments language feature
assertEquals(expected=foo, actual=bar)

# Giving the arguments different types, forcing us to wrap them
assertEquals(Expected(foo), Actual(bar))

# Breaking the symmetry and attaching the code to one of the arguments
bar.Should().Be(foo)

Tutti questi hanno più struttura del nome dettagliato, che dà al linguaggio qualcosa di non opaco da guardare. La definizione e l'utilizzo della funzione dipendono anche da questa struttura, quindi non può essere fuori sincrono con ciò che l'implementazione sta facendo (come può fare un nome o un commento).

Quando incontro o prevedo un problema come questo, prima di gridare frustrato al mio computer, per prima cosa mi chiedo se sia "giusto" dare la colpa alla macchina. In altre parole, alla macchina sono state fornite informazioni sufficienti per distinguere ciò che volevo da ciò che chiedevo?

Una chiamata come assertEqual(expected, actual)ha più senso assertEqual(actual, expected), quindi è facile per noi confonderli e per la macchina andare avanti e fare la cosa sbagliata. Se assertExpectedEqualsActualinvece lo usassimo, potrebbe renderci meno propensi a fare un errore, ma non fornisce più informazioni alla macchina (non capisce l'inglese e la scelta del nome non dovrebbe influire sulla semantica).

Ciò che rende più preferibili gli approcci "strutturati", come argomenti di parole chiave, campi etichettati, tipi distinti, ecc. È che le informazioni extra sono anche leggibili automaticamente , in modo che possiamo avere la macchina individuare usi errati e aiutarci a fare le cose nel modo giusto. Il assertEqualcaso non è poi così grave, poiché l'unico problema sarebbero i messaggi imprecisi. Un esempio più sinistro potrebbe essere String replace(String old, String new, String content), che è facile confondere con il String replace(String content, String old, String new)quale ha un significato molto diverso. Un semplice rimedio sarebbe quello di prenderne una coppia [old, new], il che renderebbe immediatamente gli errori un errore (anche senza tipi).

Nota che anche con i tipi, potremmo trovarci a non "dire alla macchina ciò che vogliamo". Ad esempio l'anti-pattern chiamato "programmazione tipicamente stringa" tratta tutti i dati come stringhe, il che rende facile confondere gli argomenti (come in questo caso), dimenticare di fare qualche passo (es. Fuga), per rompere accidentalmente invarianti (es. rendendo JSON non analizzabile), ecc.

Ciò è anche correlato alla "cecità booleana", in cui calcoliamo un gruppo di booleani (o numeri, ecc.) In una parte del codice, ma quando si cerca di usarli in un'altra non è chiaro cosa rappresentino effettivamente, se li abbiamo mescolati, ecc. Confronta questo con ad esempio enumerazioni distinte che hanno nomi descrittivi (ad esempio LOGGING_DISABLEDpiuttosto che false) e che causano un messaggio di errore se le mescoliamo .


1

perché rimuove la necessità di ricordare dove vanno gli argomenti

Davvero? Non c'è ancora alcuna garanzia che il primo argomento menzionato nel nome della funzione sia davvero il primo parametro. Quindi meglio cercare (o lasciare che il tuo IDE lo faccia) e rimanere con nomi ragionevoli che fare affidamento ciecamente su un nome abbastanza sciocco.

Se leggi il codice dovresti vedere facilmente cosa succede quando i parametri sono nominati come dovrebbero essere. copy(source, destination)è molto più facile da capire di quanto non piaccia copyFromTheFirstLocationToTheSecondLocation(placeA, placeB).

Perché i programmatori non adottano il primo se è, come afferma l'autore, più chiaro del secondo?

Perché ci sono diversi punti di vista su stili diversi e puoi trovare x autori di altri articoli che affermano il contrario. Saresti impazzito cercando di seguire tutto ciò che qualcuno scrive da qualche parte ;-)


0

Concordo sul fatto che la codifica dei nomi dei parametri in nomi di funzioni rende la scrittura e l'utilizzo di funzioni più intuitive.

copyFromSourceToDestination( // "...ahh yes, the source directory goes first"

È facile dimenticare l'ordinamento degli argomenti nelle funzioni e nei comandi della shell e per questo motivo molti programmatori si affidano alle caratteristiche IDE o ai riferimenti alle funzioni. Avere gli argomenti descritti nel nome sarebbe una soluzione eloquente a questa dipendenza.

Tuttavia, una volta scritta, la descrizione degli argomenti diventa ridondante al prossimo programmatore che deve leggere l'istruzione, poiché nella maggior parte dei casi verranno utilizzate le variabili denominate.

copy(sourceDir, destinationDir); // "...makes sense"

La complessità di questo conquisterà la maggior parte dei programmatori e personalmente trovo che sia più facile da leggere.

EDIT: Come sottolineato da @Blrfl, la codifica dei parametri non è poi così "intuitiva" poiché è necessario innanzitutto ricordare il nome della funzione. Ciò richiede la ricerca di riferimenti alle funzioni o il supporto di un IDE che probabilmente fornirà comunque informazioni sull'ordinamento dei parametri.


9
Quindi, se posso interpretare l'avvocato del diavolo per un minuto: è intuitivo solo quando si conosce il nome completo della funzione. Se sai che esiste una funzione di copia e non ricordi se è copyFromSourceToDestinationo copyToDestinationFromSource, le tue scelte la stanno trovando per tentativi ed errori o leggendo il materiale di riferimento. Gli IDE che possono completare i nomi parziali sono solo una versione automatizzata di quest'ultimo.
Blrfl,

@Blrfl Il punto di chiamarlo copyFromSourceToDestinationè che se pensi che sia copyToDestinationFromSource, il compilatore troverà il tuo bug, ma se è stato chiamato copy, non lo farà. Ottenere i parametri di una routine di copia nel modo sbagliato è facile, poiché strcpy, strcat ecc. Creano un precedente. E il terse è più facile da leggere? MergeLists (listA, listB, listC) crea listA da listB & listC, oppure legge listA & listB e scrive listC?
Rosie F,

4
@RosieF Se non fossi sicuro del significato degli argomenti, leggerei la documentazione prima di scrivere il codice. Inoltre, anche con i nomi delle funzioni più dettagliati, c'è ancora spazio per l'interpretazione di ciò che è effettivamente l'ordine. Qualcuno che dà un'occhiata fredda al codice non sarà in grado di intuire che hai stabilito la convenzione che ciò che è nel nome della funzione riflette l'ordine degli argomenti. Dovrebbero comunque saperlo in anticipo o leggere i documenti.
Blrfl

OTOH, destinationDir.copy (sourceDir); // "... ha più senso"
Kristian H,

1
@KristianH In che direzione dir1.copy(dir2)funziona? Nessuna idea. Che dire dir1.copyTo(dir2)?
maaartinus,
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.