Razionale per preferire le variabili locali alle variabili di istanza?


109

La base di codice su cui sto lavorando spesso utilizza variabili di istanza per condividere dati tra vari metodi banali. Lo sviluppatore originale è fermamente convinto che ciò aderisca alle migliori pratiche dichiarate nel libro di Clean Code di zio Bob / Robert Martin: "La prima regola delle funzioni è che dovrebbero essere piccole". e "Il numero ideale di argomenti per una funzione è zero (niladic). (...) Gli argomenti sono difficili. Prendono molto potere concettuale."

Un esempio:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  private byte[] encodedData;
  private EncryptionInfo encryptionInfo;
  private EncryptedObject payloadOfResponse;
  private URI destinationURI;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    getEncodedData(encryptedRequest);
    getEncryptionInfo();
    getDestinationURI();
    passRequestToServiceClient();

    return cryptoService.encryptResponse(payloadOfResponse);
  }

  private void getEncodedData(EncryptedRequest encryptedRequest) {
    encodedData = cryptoService.decryptRequest(encryptedRequest, byte[].class);
  }

  private void getEncryptionInfo() {
    encryptionInfo = cryptoService.getEncryptionInfoForDefaultClient();
  }

  private void getDestinationURI() {
    destinationURI = router.getDestination().getUri();
  }

  private void passRequestToServiceClient() {
    payloadOfResponse = serviceClient.handle(destinationURI, encodedData, encryptionInfo);
  }
}

Rifletterei questo nel seguente usando variabili locali:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    byte[] encodedData = cryptoService.decryptRequest(encryptedRequest, byte[].class);
    EncryptionInfo encryptionInfo = cryptoService.getEncryptionInfoForDefaultClient();
    URI destinationURI = router.getDestination().getUri();
    EncryptedObject payloadOfResponse = serviceClient.handle(destinationURI, encodedData,
      encryptionInfo);

    return cryptoService.encryptResponse(payloadOfResponse);
  }
}

Questo è più breve, elimina l'accoppiamento di dati implicito tra i vari metodi banali e limita gli ambiti variabili al minimo richiesto. Eppure, nonostante questi vantaggi, non riesco ancora a convincere lo sviluppatore originale che questo refactoring è giustificato, poiché sembra contraddire le pratiche di zio Bob di cui sopra.

Da qui le mie domande: qual è l'obiettivo logico scientifico per favorire le variabili locali rispetto alle variabili di istanza? Non riesco proprio a metterci il dito sopra. La mia intuizione mi dice che gli accoppiamenti nascosti sono cattivi e che uno scopo ristretto è meglio di uno ampio. Ma qual è la scienza per sostenere questo?

E viceversa, ci sono degli svantaggi di questo refactoring che forse ho trascurato?


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
maple_shaft

Risposte:


170

Qual è l'obiettivo logico e scientifico per favorire le variabili locali rispetto alle variabili di istanza?

L'ambito non è uno stato binario, è un gradiente. Puoi classificarli dal più grande al più piccolo:

Global > Class > Local (method) > Local (code block, e.g. if, for, ...)

Modifica: ciò che chiamo "ambito di classe" è ciò che intendi per "variabile di istanza". Per quanto ne so, quelli sono sinonimi, ma io sono un sviluppatore C #, non un sviluppatore Java. Per brevità, ho inserito tutta la statica nella categoria globale poiché la statica non è l'argomento della domanda.

Più piccolo è il campo di applicazione, meglio è. La logica è che le variabili dovrebbero vivere nel più piccolo ambito possibile . Ci sono molti vantaggi in questo:

  • Ti costringe a pensare alla responsabilità della classe attuale e ti aiuta a rispettare SRP.
  • Essa permette di non dover per evitare conflitti di nomi a livello mondiale, ad esempio se due o più classi hanno una Nameproprietà, non si è costretti a loro come prefisso FooName, BarName... Così mantenendo i nomi delle variabili più pulito e concisa possibile.
  • Declama il codice limitando le variabili disponibili (ad esempio per Intellisense) a quelle che sono contestualmente rilevanti.
  • Consente una qualche forma di controllo dell'accesso in modo che i tuoi dati non possano essere manipolati da un attore che non conosci (ad esempio una classe diversa sviluppata da un collega).
  • Rende il codice più leggibile quando si garantisce che la dichiarazione di queste variabili cerchi di rimanere il più vicino possibile all'utilizzo effettivo di queste variabili.
  • Dichiarare sfrenatamente le variabili in un ambito troppo ampio è spesso indicativo di uno sviluppatore che non capisce abbastanza OOP o come implementarlo. Vedere le variabili con un ambito eccessivamente ampio funge da bandiera rossa che probabilmente c'è qualcosa che non va nell'approccio OOP (o con lo sviluppatore in generale o la base di codice in particolare).
  • (Commento di Kevin) L'uso della gente del posto ti costringe a fare le cose nel giusto ordine. Nel codice originale (variabile di classe), potresti erroneamente spostarti passRequestToServiceClient()all'inizio del metodo, e sarebbe comunque compilato. Con la gente del posto, potresti commettere quell'errore solo se hai passato una variabile non inizializzata, il che è abbastanza ovvio che non lo fai davvero.

Eppure, nonostante questi vantaggi, non riesco ancora a convincere lo sviluppatore originale che questo refactoring è giustificato, poiché sembra contraddire le pratiche di zio Bob di cui sopra.

E viceversa, ci sono degli svantaggi di questo refactoring che forse ho trascurato?

Il problema qui è che il tuo argomento per le variabili locali è valido, ma hai anche apportato ulteriori modifiche che non sono corrette e fanno sì che la correzione suggerita non superi il test dell'olfatto.

Anche se capisco il tuo suggerimento "nessuna variabile di classe" e ne ha merito, in realtà hai rimosso anche i metodi stessi, e questo è un gioco completamente diverso. I metodi avrebbero dovuto rimanere, e invece avresti dovuto modificarli per restituire il loro valore piuttosto che memorizzarlo in una variabile di classe:

private byte[] getEncodedData() {
    return cryptoService.decryptRequest(encryptedRequest, byte[].class);
}

private EncryptionInfo getEncryptionInfo() {
    return cryptoService.getEncryptionInfoForDefaultClient();
}

// and so on...

Sono d'accordo con quello che hai fatto nel processmetodo, ma avresti dovuto chiamare i sottometodi privati ​​anziché eseguire direttamente i loro corpi.

public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    byte[] encodedData = getEncodedData();
    EncryptionInfo encryptionInfo = getEncryptionInfo();

    //and so on...

    return cryptoService.encryptResponse(payloadOfResponse);
}

Vorresti quel livello extra di astrazione, specialmente quando incontri metodi che devono essere riutilizzati più volte. Anche se al momento non riutilizzi i tuoi metodi , è comunque una buona prassi creare già dei sottometodi laddove pertinenti, anche se solo per facilitare la leggibilità del codice.

Indipendentemente dall'argomento della variabile locale, ho immediatamente notato che la correzione suggerita è notevolmente meno leggibile rispetto all'originale. Ammetto che l'uso sfrenato delle variabili di classe toglie anche la leggibilità del codice, ma non a prima vista rispetto a te che hai impilato tutta la logica in un unico metodo (ora prolisso).


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
maple_shaft

79

Il codice originale utilizza variabili membro come argomenti. Quando dice di minimizzare il numero di argomenti, ciò che realmente intende è minimizzare la quantità di dati che i metodi richiedono per funzionare. Inserire tali dati nelle variabili membro non migliora nulla.


20
Completamente d'accordo! Queste variabili membro sono semplicemente argomenti di funzione impliciti. In effetti è peggio da ora non esiste alcun legame esplicito tra il valore di quella variabile e l'uso della funzione (da un POV esterno)
Rémi

1
Direi che non è questo il significato del libro. Quante funzioni richiedono esattamente zero dati di input per l'esecuzione? Questo è uno dei pezzi che penso che il libro abbia sbagliato.
Qwertie

1
@Qwertie Se si dispone di un oggetto, i dati su cui funziona potrebbero essere completamente incapsulati al suo interno. Funzioni come process.Start();o myString.ToLowerCase()non dovrebbero sembrare troppo strane (e in effetti sono le più facili da capire).
R. Schmitz,

5
Entrambi hanno un argomento: l'implicito this. Si potrebbe persino sostenere che questo argomento sia esplicitamente indicato prima del punto.
BlackJack

47

Altre risposte hanno già spiegato perfettamente i vantaggi delle variabili locali, quindi tutto ciò che rimane è questa parte della tua domanda:

Eppure, nonostante questi vantaggi, non riesco ancora a convincere lo sviluppatore originale che questo refactoring è giustificato, poiché sembra contraddire le pratiche di zio Bob di cui sopra.

Dovrebbe essere facile. Basta puntarlo alla seguente citazione nel codice pulito di zio Bob:

Non hanno effetti collaterali

Gli effetti collaterali sono bugie. La tua funzione promette di fare una cosa, ma fa anche altre cose nascoste. A volte apporta modifiche inattese alle variabili della propria classe. A volte li renderà ai parametri passati nella funzione o ai globuli di sistema. In entrambi i casi si tratta di mistruth subdole e dannose che spesso provocano strani accoppiamenti temporali e dipendenze dell'ordine.

(esempio omesso)

Questo effetto collaterale crea un accoppiamento temporale. Cioè, checkPassword può essere chiamato solo in determinati momenti (in altre parole, quando è sicuro inizializzare la sessione). Se viene chiamato fuori servizio, i dati della sessione potrebbero andare persi inavvertitamente. Gli accoppiamenti temporali sono confusi, specialmente se nascosti come effetto collaterale. Se è necessario disporre di un accoppiamento temporale, è necessario chiarirlo nel nome della funzione. In questo caso potremmo rinominare la funzione checkPasswordAndInitializeSession, anche se questo sicuramente viola "Fai una cosa".

Cioè, lo zio Bob non solo dice che una funzione dovrebbe prendere pochi argomenti, ma dice anche che le funzioni dovrebbero evitare di interagire con lo stato non locale ogni volta che è possibile.


3
In un "mondo perfetto", questa sarebbe la seconda risposta elencata. La prima risposta alla situazione ideale in cui il collega ascolta la ragione, ma se il collega è un fanatico, questa risposta qui affronterebbe la situazione senza troppa confusione.
R. Schmitz,

2
Per una variazione più pragmatica di questa idea, è semplicemente molto più facile ragionare sullo stato locale rispetto all'istanza o allo stato globale. La mutabilità ben definita e strettamente contenuta e gli effetti collaterali raramente portano a problemi. Ad esempio, molte funzioni di ordinamento operano sul posto tramite effetti collaterali, ma questo è facile da ragionare in un ambito locale.
Beefster

1
Ah, il buon vecchio "in un assiomatico contraddittorio, è possibile provare qualsiasi cosa". Dato che non esistono verità concrete e bugie IRL, tutti i dogmi dovranno includere dichiarazioni che affermano cose opposte, cioè che siano contraddittorie.
ivan_pozdeev l'

26

"In contraddizione con ciò che pensa lo zio di qualcuno" non è MAI un buon argomento. MAI. Non prendere la saggezza dagli zii, pensa a te stesso.

Detto questo, le variabili di istanza dovrebbero essere utilizzate per memorizzare informazioni che sono effettivamente necessarie per essere archiviate in modo permanente o semi-permanente. Le informazioni qui non lo sono. È molto semplice vivere senza le variabili di istanza, quindi possono andare.

Test: scrivere un commento di documentazione per ciascuna variabile di istanza. Puoi scrivere qualcosa che non sia completamente inutile? E scrivi un commento di documentazione ai quattro accessori. Sono ugualmente inutili.

La cosa peggiore è assumere il modo di decrittografare le modifiche, perché si utilizza un cryptoService diverso. Invece di dover modificare quattro righe di codice, è necessario sostituire quattro variabili di istanza con variabili diverse, quattro getter con variabili diverse e modificare quattro righe di codice.

Ma ovviamente la prima versione è preferibile se si è pagati dalla riga di codice. 31 righe anziché 11 righe. Tre volte più righe da scrivere e da mantenere per sempre, da leggere quando si esegue il debug di qualcosa, da adattare quando sono necessarie modifiche, da duplicare se si supporta un secondo cryptoService.

(Ha perso il punto importante che l'uso delle variabili locali ti costringe a effettuare le chiamate nel giusto ordine).


16
Pensare per te è ovviamente buono. Ma il tuo paragrafo di apertura include in modo efficace discenti o junior che respingono il contributo di insegnanti o anziani; che va troppo lontano.
Flater

9
@Flater Dopo aver pensato al contributo degli insegnanti o degli anziani e aver visto che hanno torto, ignorare il loro contributo è l'unica cosa giusta. Alla fine, non si tratta di licenziare, ma di metterlo in discussione e respingerlo solo se si rivela definitivamente sbagliato.
glglgl

10
@ChristianHackl: Sono pienamente d'accordo con il non seguire ciecamente il tradizionalismo, ma non lo accetterei neanche alla cieca. La risposta sembra suggerire di evitare le conoscenze acquisite a favore della propria visione, e questo non è un approccio salutare da adottare. Seguendo ciecamente le opinioni degli altri, no. Mettere in discussione qualcosa quando non sei d'accordo, ovviamente sì. Respingerlo completamente perché non sei d'accordo, no. Mentre la leggo, il primo paragrafo della risposta sembra suggerire almeno quest'ultimo. Dipende molto da cosa significa gnasher con "pensa a te stesso", che necessita di elaborazione.
Flater

9
Su una piattaforma di saggezza condivisa, questo sembra un po 'fuori posto ...
drjpizzle

4
MAI è anche MAI un buon argomento ... Sono pienamente d'accordo sul fatto che esistono regole per guidare, non per prescrivere. Tuttavia, le regole sulle regole non sono che una sottoclasse di regole, quindi hai pronunciato il tuo verdetto ...
cmaster

14

Qual è l'obiettivo logico e scientifico per favorire le variabili locali rispetto alle variabili di istanza? Non riesco proprio a metterci il dito sopra. La mia intuizione mi dice che gli accoppiamenti nascosti sono cattivi e che uno scopo ristretto è meglio di uno ampio. Ma qual è la scienza per sostenere questo?

Le variabili di istanza servono per rappresentare le proprietà del loro oggetto host, non per rappresentare le proprietà specifiche dei thread di calcolo con scopi più ristretti rispetto all'oggetto stesso. Alcuni dei motivi per trarre una tale distinzione che sembrano non essere già stati coperti ruotano intorno alla concorrenza e al rientro. Se i metodi scambiano dati impostando i valori delle variabili di istanza, due thread simultanei possono facilmente intasare i valori reciproci per quelle variabili di istanze, producendo bug intermittenti e difficili da trovare.

Anche un solo thread può riscontrare problemi in tal senso, poiché esiste un rischio elevato che un modello di scambio di dati basato su variabili di istanza renda i metodi non rientranti. Allo stesso modo, se le stesse variabili vengono utilizzate per trasmettere dati tra coppie diverse di metodi, esiste il rischio che un singolo thread che esegue anche una catena non ricorsiva di invocazioni di metodi si imbatterà in bug che ruotano attorno a modifiche impreviste delle variabili di istanza coinvolte.

Per ottenere risultati corretti in modo affidabile in uno scenario di questo tipo, è necessario utilizzare variabili separate per comunicare tra ciascuna coppia di metodi in cui uno invoca l'altro, oppure per fare in modo che ogni implementazione del metodo tenga conto di tutti i dettagli di implementazione di tutti gli altri metodi che invoca, sia direttamente che indirettamente. Questo è fragile e si ridimensiona male.


7
Finora, questa è l'unica risposta che menziona sicurezza dei thread e concorrenza. È un po 'sorprendente dato l'esempio di codice specifico nella domanda: un'istanza di SomeBusinessProcess non può elaborare in modo sicuro più richieste crittografate contemporaneamente. Il metodo public EncryptedResponse process(EncryptedRequest encryptedRequest)non è sincronizzato e le chiamate simultanee probabilmente ostruiscono i valori delle variabili di istanza. Questo è un buon punto da sollevare.
Joshua Taylor,

9

Discutendo solo process(...), l'esempio dei tuoi colleghi è molto più leggibile nel senso della logica aziendale. Al contrario, il tuo contro esempio richiede più di una rapida occhiata per estrarre qualsiasi significato.

Detto questo, il codice pulito è sia leggibile che di buona qualità - spingere lo stato locale in uno spazio più globale è solo un assemblaggio di alto livello, quindi uno zero per la qualità.

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest request) {
    checkNotNull(encryptedRequest);

    return encryptResponse
      (routeTo
         ( destination()
         , requestData(request)
         , destinationEncryption()
         )
      );
  }

  private byte[] requestData(EncryptedRequest encryptedRequest) {
    return cryptoService.decryptRequest(encryptedRequest, byte[].class);
  }

  private EncryptionInfo destinationEncryption() {
    return cryptoService.getEncryptionInfoForDefaultClient();
  }

  private URI destination() {
    return router.getDestination().getUri();
  }

  private EncryptedObject routeTo(URI destinationURI, byte[] encodedData, EncryptionInfo encryptionInfo) {
    return serviceClient.handle(destinationURI, encodedData, encryptionInfo);
  }

  private void encryptResponse(EncryptedObject payloadOfResponse) {
    return cryptoService.encryptResponse(payloadOfResponse);
  }
}

Questa è una rappresentazione che elimina la necessità di variabili in qualsiasi ambito. Sì, il compilatore li genererà, ma la parte importante è che controlla che il codice sia efficiente. Pur essendo relativamente leggibile.

Solo un punto sulla denominazione. Vuoi il nome più breve che sia significativo e si espanda sulle informazioni già disponibili. vale a dire. destinationURI, l '"URI" è già noto con la firma del tipo.


4
L'eliminazione di tutte le variabili non semplifica necessariamente la lettura del codice.
Pharap

Elimina completamente tutte le variabili con lo stile senza punti en.wikipedia.org/wiki/Tacit_programming
Marcin

@Pharap Vero, la mancanza di variabili non garantisce la leggibilità. In alcuni casi rende anche più difficile il debug. Il punto è che nomi ben scelti, un chiaro uso dell'espressione, possono comunicare un'idea molto chiaramente, pur essendo efficienti.
Kain0_0

7

Vorrei solo rimuovere queste variabili e metodi privati ​​del tutto. Ecco il mio refactor:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    return cryptoService.encryptResponse(
        serviceClient.handle(router.getDestination().getUri(),
        cryptoService.decryptRequest(encryptedRequest, byte[].class),
        cryptoService.getEncryptionInfoForDefaultClient()));
  }
}

Per il metodo privato, ad esempio router.getDestination().getUri()è più chiaro e più leggibile di getDestinationURI(). Lo ripeto anche se uso due volte la stessa riga nella stessa classe. Per vederlo in un altro modo, se c'è bisogno di un getDestinationURI(), allora probabilmente appartiene a qualche altra classe, non alla SomeBusinessProcessclasse.

Per variabili e proprietà, la necessità comune per loro è quella di contenere valori da utilizzare in seguito. Se la classe non ha un'interfaccia pubblica per le proprietà, probabilmente non dovrebbero essere proprietà. Il peggior tipo di proprietà della classe utilizzata è probabilmente per passare valori tra metodi privati ​​mediante effetti collaterali.

Comunque, la classe deve solo fare process()e quindi l'oggetto verrà gettato via, non è necessario mantenere alcuno stato in memoria. Un ulteriore potenziale di refattore sarebbe quello di eliminare il CryptoService da quella classe.

Sulla base dei commenti, voglio aggiungere che questa risposta si basa sulla pratica del mondo reale. In effetti, nella revisione del codice, la prima cosa che sceglierei è il refactoring della classe e spostare il lavoro di crittografia / decrittografia. Una volta fatto, chiedo se sono necessari i metodi e le variabili, sono stati nominati correttamente e così via. Il codice finale sarà probabilmente più vicino a questo:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;

  public Response process(Request request) {
    return serviceClient.handle(router.getDestination().getUri());
  }
}

Con il codice sopra, non penso che abbia bisogno di ulteriori refactoring. Come per le regole, penso che ci voglia esperienza per sapere quando e quando non applicarle. Le regole non sono teorie che hanno dimostrato di funzionare in tutte le situazioni.

La revisione del codice d'altra parte ha un reale impatto su quanto tempo può passare un pezzo di codice. Il mio trucco è avere meno codice e renderlo facile da capire. Un nome di variabile può essere un punto di discussione, se posso rimuoverlo i revisori non dovrebbero nemmeno pensarci.


Il mio voto, anche se molti esiteranno qui. Naturalmente alcune astrazioni, hanno un senso. (A proposito un metodo chiamato "processo" è terribile.) Ma qui la logica è minima. La domanda del PO è tuttavia su un intero stile di codice, e in questo caso il caso potrebbe essere più complesso.
Joop Eggen,

1
Un chiaro problema con il concatenamento di tutto in un'unica chiamata di metodo è la pura leggibilità. Inoltre, non funziona se è necessaria più di un'operazione su un determinato oggetto. Inoltre, è quasi impossibile eseguire il debug perché non è possibile eseguire le operazioni e ispezionare gli oggetti. Mentre questo funziona a livello tecnico, non lo sosterrei perché trascura in modo massiccio aspetti non runtime dello sviluppo del software.
Flater

@Flater Sono d'accordo con i tuoi commenti, non vogliamo applicarlo ovunque. Ho modificato la mia risposta per chiarire la mia posizione pratica. Quello che voglio mostrare è che in pratica applichiamo le regole solo quando è appropriato. In questo caso, la chiamata del metodo concatenamento va bene e, se devo eseguire il debug, invoco i test per i metodi concatenati.
imel96

@JoopEggen Sì, le astrazioni hanno un senso. Nell'esempio, i metodi privati ​​non danno comunque alcuna astrazione, gli utenti della classe non ne sanno nemmeno nulla
imel96

1
@ imel96 è divertente che tu sia probabilmente una delle poche persone qui che ha notato che l'accoppiamento tra ServiceClient e CryptoService rende utile concentrarsi sull'iniezione di CS in SC anziché in SBP, risolvendo il problema di fondo a un livello architettonico superiore ... questo è IMVHO il punto principale di questa storia; è troppo facile tenere traccia del quadro generale mentre ci si concentra sui dettagli.
vaxquis l'

4

La risposta di Flater copre abbastanza bene i problemi di scoping, ma penso che ci sia anche un altro problema qui.

Si noti che esiste una differenza tra una funzione che elabora i dati e una funzione che accede semplicemente ai dati .

Il primo esegue la vera logica aziendale, mentre il secondo salva la digitazione e forse aggiunge sicurezza aggiungendo un'interfaccia più semplice e riutilizzabile.

In questo caso sembra che le funzioni di accesso ai dati non salvino la digitazione e non vengano riutilizzate da nessuna parte (o ci sarebbero altri problemi nel rimuoverle). Quindi queste funzioni semplicemente non dovrebbero esistere.

Mantenendo solo la logica aziendale nelle funzioni denominate, otteniamo il meglio da entrambi i mondi (a metà tra la risposta di Flater e la risposta di imel96 ):

public EncryptedResponse process(EncryptedRequest encryptedRequest) {

    byte[] requestData = decryptRequest(encryptedRequest);
    EncryptedObject responseData = handleRequest(router.getDestination().getUri(), requestData, cryptoService.getEncryptionInfoForDefaultClient());
    EncryptedResponse response = encryptResponse(responseData);

    return response;
}

// define: decryptRequest(), handleRequest(), encryptResponse()

3

La prima e più importante cosa: lo zio Bob sembra essere un predicatore a volte, ma afferma che ci sono eccezioni alle sue regole.

L'intera idea di Clean Code è migliorare la leggibilità ed evitare errori. Esistono diverse regole che si stanno violando a vicenda.

Il suo argomento sulle funzioni è che le funzioni niladiche sono le migliori, tuttavia sono accettabili fino a tre parametri. Personalmente penso che anche 4 siano ok.

Quando vengono utilizzate le variabili di istanza, devono creare una classe coerente. Ciò significa che le variabili dovrebbero essere utilizzate in molti, se non in tutti i metodi non statici.

Le variabili che non vengono utilizzate in molti punti della classe devono essere spostate.

Non considero ottimale né la versione originale né quella refactored, e @Flater ha già affermato molto bene cosa si può fare con i valori di ritorno. Migliora la leggibilità e riduce gli errori nell'uso dei valori restituiti.


1

Le variabili locali riducono l'ambito quindi limita i modi in cui le variabili possono essere utilizzate e quindi aiuta a prevenire determinate classi di errore e migliora la leggibilità.

La variabile di istanza riduce i modi in cui è possibile chiamare la funzione, il che aiuta anche a ridurre alcune classi di errore e migliora la leggibilità.

Dire che uno ha ragione e l'altro ha torto può essere una conclusione valida in un caso particolare, ma come consiglio generale ...

TL; DR: Penso che il motivo per cui hai un odore troppo zelo sia, c'è troppo zelo.


0

Nonostante il fatto che i metodi che iniziano con get ... non debbano restituire nulla, la prima soluzione è data dalla separazione dei livelli di astrazione all'interno dei metodi. Sebbene la seconda soluzione sia più mirata, è ancora più difficile ragionare su ciò che sta accadendo nel metodo. Le assegnazioni di variabili locali non sono necessarie qui. Vorrei mantenere i nomi dei metodi e refactificare il codice in qualcosa del genere:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    return getEncryptedResponse(
            passRequestToServiceClient(getDestinationURI(), getEncodedData(encryptedRequest) getEncryptionInfo())
        );
  }

  private EncryptedResponse getEncryptedResponse(EncryptedObject encryptedObject) {
    return cryptoService.encryptResponse(encryptedObject);
  }

  private byte[] getEncodedData(EncryptedRequest encryptedRequest) {
    return cryptoService.decryptRequest(encryptedRequest, byte[].class);
  }

  private EncryptionInfo getEncryptionInfo() {
    return cryptoService.getEncryptionInfoForDefaultClient();
  }

  private URI getDestinationURI() {
    return router.getDestination().getUri();
  }

  private EncryptedObject passRequestToServiceClient(URI destinationURI, byte[] encodedData, EncryptionInfo encryptionInfo) {
    return serviceClient.handle(destinationURI, encodedData, encryptionInfo);
  }
}

0

Entrambe le cose fanno lo stesso e le differenze di prestazioni sono impercettibili, quindi non credo che ci sia un argomento scientifico . Si tratta quindi di preferenze soggettive.

E anch'io tendo ad apprezzare la tua strada meglio di quella del tuo collega. Perché? Perché penso che sia più facile da leggere e capire, nonostante ciò che dice un autore di libri.

Entrambe le vie realizzano la stessa cosa, ma la sua strada è più diffusa. Per leggere quel codice è necessario spostarsi avanti e indietro tra diverse funzioni e variabili membro. Non è tutto condensato in un unico posto, devi ricordare tutto ciò che hai in testa per capirlo. È un carico cognitivo molto maggiore.

Al contrario, il tuo approccio racchiude tutto molto più densamente, ma non per renderlo impenetrabile. Lo leggi solo riga per riga e non hai bisogno di memorizzare così tanto per capirlo.

Tuttavia, se è abituato a programmare in modo tale, posso immaginare che per lui potrebbe essere il contrario.


Questo ha davvero dimostrato di essere un grande ostacolo per me capire il flusso. Questo esempio è piuttosto piccolo, ma un altro ha usato 35 (!) Variabili di istanza per i suoi risultati intermedi, senza contare le dozzine di variabili di istanza contenenti le dipendenze. È stato piuttosto difficile da seguire, poiché è necessario tenere traccia di ciò che è già stato impostato in precedenza. Alcuni sono stati anche riutilizzati in seguito, rendendolo ancora più difficile. Fortunatamente gli argomenti presentati qui hanno finalmente convinto il mio collega ad accordarsi con il refactoring.
Alexander
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.