Meglio avere 2 metodi con un significato chiaro o solo 1 metodo a doppio uso?


30

Per semplificare l'interfaccia, è meglio non avere il getBalance()metodo? Passare 0al charge(float c);darà lo stesso risultato:

public class Client {
    private float bal;
    float getBalance() { return bal; }
    float charge(float c) {
        bal -= c;
        return bal;
    }
}

Forse prendi nota javadoc? Oppure lascialo all'utente della classe per capire come ottenere l'equilibrio?


26
Sto generosamente supponendo che questo sia un esempio di codice illustrativo e che non stai usando la matematica in virgola mobile per memorizzare valori monetari. Altrimenti dovrei avere tutto predicato. :-)
corsiKa

1
@corsiKa nah. Questo è solo un esempio. Ma sì, avrei usato il float per rappresentare il denaro in una vera classe ... Grazie per avermi ricordato i pericoli dei numeri in virgola mobile.
David,

10
Alcune lingue distinguono tra mutatori e accessori con buoni risultati. Sarebbe terribile fastidioso non riuscire a ottenere l'equilibrio di un'istanza accessibile solo come costante!
JDługosz,

Risposte:


90

Sembra che tu suggerisca che la complessità di un'interfaccia sia misurata dal numero di elementi che possiede (metodi, in questo caso). Molti sosterrebbero che dover ricordare che il chargemetodo può essere usato per restituire l'equilibrio di un Clientaggiunge molta più complessità che avere l'elemento extra del getBalancemetodo. Rendere le cose più esplicite è molto più semplice, specialmente al punto in cui non lascia alcuna ambiguità, indipendentemente dal numero più elevato di elementi nell'interfaccia.

Inoltre, chiamare charge(0)viola il principio del minimo stupore , noto anche come metrica WTF al minuto (da Clean Code, immagine sotto), rendendo difficile per i nuovi membri del team (o quelli attuali, dopo un po 'di distanza dal codice) fino a capiscono che la chiamata viene effettivamente utilizzata per ottenere il saldo. Pensa a come reagirebbero gli altri lettori:

inserisci qui la descrizione dell'immagine

Inoltre, la firma del chargemetodo è in contrasto con le linee guida per la separazione di una sola cosa e comando-query , perché fa sì che l'oggetto cambi il suo stato restituendo al contempo un nuovo valore.

Tutto sommato, credo che l'interfaccia più semplice in questo caso sarebbe:

public class Client {
  private float bal;
  float getBalance() { return bal; }
  void charge(float c) { bal -= c; }
}

25
Il punto sulla separazione comando-query è estremamente importante, IMO. Immagina di essere un nuovo sviluppatore in questo progetto. Esaminando il codice, è immediatamente chiaro che getBalance()restituisce un valore e non modifica nulla. D'altra parte, charge(0)sembra che probabilmente modifica qualcosa ... forse invia un evento PropertyChanged? E cosa sta restituendo, il valore precedente o il nuovo valore? Ora devi cercarlo nei documenti e sprecare il potere del cervello, che avrebbe potuto essere evitato usando un metodo più chiaro.
Mago Xy,

19
Inoltre, usare charge(0)come modo per ottenere l'equilibrio perché sembra che non abbia alcun effetto ora ti lega a questi dettagli di implementazione; Posso facilmente immaginare un requisito futuro in cui il rilevamento del numero di addebiti diventa rilevante, a quel punto avere la tua base di codice piena di addebiti che non sono realmente addebiti diventa un punto dolente. Dovresti fornire elementi di interfaccia che significano le cose che i tuoi clienti devono fare, non quelli che semplicemente fanno ciò che i tuoi clienti devono fare.
Ben

12
L'ammonizione CQS non è necessariamente appropriata. Per un charge()metodo, se tale metodo è atomico in un sistema concorrente, potrebbe essere appropriato restituire il valore nuovo o precedente.
Dietrich Epp,

3
Ho trovato le tue due citazioni per CQRS estremamente poco convincenti. Ad esempio, in genere mi piacciono le idee di Meyer, ma guardare il tipo di ritorno di una funzione per capire se muta qualcosa è arbitrario e poco chiaro. Molte strutture di dati simultanee o immutabili richiedono mutazioni + ritorni. Come aggiungeresti a una stringa senza violare CQRS? Hai citazioni migliori?
user949300,

6
Quasi nessun codice è conforme alla separazione delle query dei comandi. Non è idiomatico in nessun linguaggio popolare orientato agli oggetti, ed è violato dalle API integrate di ogni linguaggio che abbia mai usato. Descriverlo come una "best practice" sembra quindi bizzarro; puoi mai nominare un solo progetto software che segua effettivamente questo schema?
Mark Amery,

28

IMO, la sostituzione getBalance()con charge(0)tutta l'applicazione non è una semplificazione. Sì, ha meno righe, ma offusca il significato del charge()metodo, che potrebbe potenzialmente causare mal di testa lungo la linea quando tu o qualcun altro ha bisogno di rivisitare questo codice.

Sebbene possano dare lo stesso risultato, ottenere il saldo di un conto non equivale a un addebito pari a zero, quindi sarebbe probabilmente meglio separare le tue preoccupazioni. Ad esempio, se hai mai avuto bisogno di modificare charge()per accedere ogni volta che si verifica una transazione del conto, ora hai un problema e dovresti comunque separare la funzionalità.


13

È importante ricordare che il codice dovrebbe essere auto-documentato. Quando chiamo charge(x), mi aspetto xdi essere addebitato. Le informazioni sull'equilibrio sono secondarie. Inoltre, potrei non sapere come charge()viene implementato quando lo chiamo e sicuramente non saprò come verrà implementato domani. Ad esempio, considera questo potenziale aggiornamento futuro per charge():

float charge(float c) {
    lockDownUserAccountUntilChargeClears();
    bal -= c;
    Unlock();
    return bal;
}

Improvvisamente usare charge()per ottenere l'equilibrio non sembra così buono.


Sì! Un buon punto che chargeè molto più pesante.
Deduplicatore

2

Usare charge(0);per ottenere il saldo è una cattiva idea: un giorno qualcuno potrebbe aggiungere un po 'di codice lì per registrare gli addebiti effettuati senza rendersi conto dell'altro uso della funzione, e quindi ogni volta che qualcuno ottiene il saldo verrà registrato come addebito. (Ci sono modi per aggirare questo come un'istruzione condizionale che dice qualcosa del tipo:

if (c > 0) {
    // make and log charge
}
return bal;

ma questi si basano sul programmatore che sa implementarli, cosa che non farà se non è immediatamente ovvio che sono necessari.

In breve: non fare affidamento né sui tuoi utenti né sui successori dei programmatori che charge(0);si rendono conto che è il modo corretto di ottenere l'equilibrio, perché a meno che non ci sia documentazione sulla loro garanzia di non perdere, allora francamente sembra il modo più spaventoso di ottenere l'equilibrio possibile.


0

So che ci sono molte risposte, ma un altro motivo charge(0)è che un semplice errore charge(9)causerà la diminuzione del saldo del cliente ogni volta che si desidera ottenere il proprio saldo. Se hai un buon test unitario potresti essere in grado di mitigare quel rischio, ma se non riesci ad essere diligente in ogni chiamata, chargepotresti avere questo contrattempo.


-2

Vorrei menzionare un caso particolare in cui avrebbe senso avere meno metodi, più multiuso, se c'è molto polimorfismo, cioè molte implementazioni di questa interfaccia ; specialmente se quelle implementazioni sono in codice sviluppato separatamente che non può essere aggiornato in sincronia (l'interfaccia è definita da una libreria).

In tal caso, semplificare il lavoro di scrittura di ogni implementazione è molto più prezioso della chiarezza dell'uso di essa, poiché il primo evita bug di violazione del contratto (i due metodi sono incoerenti tra loro), mentre il secondo danneggia solo la leggibilità, che può essere recuperato da una funzione di supporto o da un metodo superclasse che definisce getBalancein termini di charge.

(Questo è un modello di progettazione, per il quale non ricordo un nome specifico: definire una complessa interfaccia intuitiva per il chiamante in termini di una minima intuitiva per gli implementatori. In Mac OS classico l'interfaccia minima per le operazioni di disegno era chiamata "collo di bottiglia" ma questo non sembra essere un termine popolare.)

Se questo non è il caso (ci sono poche implementazioni o esattamente una), allora ha senso separare i metodi per chiarezza e consentire la semplice aggiunta di comportamenti rilevanti per addebiti diversi da zero charge().


1
In effetti, ho avuto casi in cui è stata scelta l'interfaccia più minimale per facilitare i test di copertura completi. Ma questo non è necessariamente esposto all'utente della classe. Ad esempio , inserire , aggiungere ed eliminare possono essere tutti semplici wrapper di un sostituto generale che ha tutti i percorsi di controllo ben studiati e testati.
JDługosz,

Ho visto un'API del genere. L'allocatore Lua 5.1+ lo utilizza. Fornisci una funzione e, in base ai parametri che passa, decidi se sta tentando di allocare nuova memoria, deallocare memoria o riallocare memoria dalla vecchia memoria. È doloroso scrivere tali funzioni. Preferirei piuttosto che Lua ti avesse appena assegnato due funzioni, una per allocazione e una per deallocazione.
Nicol Bolas,

@NicolBolas: E nessuno per la riallocazione? Ad ogni modo, quello ha l'ulteriore "vantaggio" di essere quasi uguale a realloc, a volte, su alcuni sistemi.
Deduplicatore

@Deduplicator: allocazione vs. riallocazione è ... OK. Non è bello metterli nella stessa funzione, ma non è così male come mettere la deallocazione nella stessa. È anche una facile distinzione da fare.
Nicol Bolas,

Direi che confondere la deallocazione con la (ri) allocazione è un esempio particolarmente negativo di questo tipo di interfaccia. (Deallocation ha un contratto molto diverso: non si traduce in un puntatore valido.)
Kevin Reid
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.