Clean Code e oggetti ibridi e invidia delle caratteristiche


10

Di recente ho apportato alcune importanti rifattorizzazioni al mio codice. Una delle cose principali che ho cercato di fare è stata quella di dividere le mie classi in oggetti dati e oggetti worker. Questo è stato ispirato, tra le altre cose, da questa sezione di Clean Code :

ibridi

Questa confusione a volte porta a sfortunate strutture dati ibride che sono metà oggetto e metà struttura dati. Hanno funzioni che fanno cose significative e hanno anche variabili pubbliche o accessori e mutatori pubblici che, a tutti gli effetti, rendono pubbliche le variabili private, tentando altre funzioni esterne di usare quelle variabili come un programma procedurale userebbe struttura dati.

Tali ibridi rendono difficile l'aggiunta di nuove funzioni ma anche l'aggiunta di nuove strutture dati. Sono i peggiori di entrambi i mondi. Evita di crearli. Sono indicativi di un disegno confuso i cui autori non sono sicuri - o peggio, ignoranti - se hanno bisogno di protezione da funzioni o tipi.

Recentemente stavo guardando il codice di uno dei miei oggetti worker (che succede per implementare il modello visitatore ) e ho visto questo:

@Override
public void visit(MarketTrade trade) {
    this.data.handleTrade(trade);
    updateRun(trade);
}

private void updateRun(MarketTrade newTrade) {
    if(this.data.getLastAggressor() != newTrade.getAggressor()) {
        this.data.setRunLength(0);
        this.data.setLastAggressor(newTrade.getAggressor());
    }
    this.data.setRunLength(this.data.getRunLength() + newTrade.getLots());
}

Mi sono subito detto "invidia delle funzionalità! Questa logica dovrebbe essere nella Dataclasse - in particolare nel handleTrademetodo. handleTradeE updateRundovrebbe sempre accadere insieme". Ma poi ho pensato "la classe di dati è solo una publicstruttura di dati, se comincio a farlo, allora arriverà un oggetto ibrido!"

Cosa c'è di meglio e perché? Come decidi quale fare?


2
Perché i "dati" devono essere una struttura di dati. Ha un comportamento chiaro. Quindi basta disattivare tutti i getter e setter, in modo che nessun oggetto possa manipolare lo stato interno.
Cormac Mulhall,

Risposte:


9

Il testo che hai citato ha un buon consiglio, anche se sostituirò "strutture di dati" con "record", supponendo che si intende qualcosa di simile a strutture. I record sono solo stupide aggregazioni di dati. Sebbene possano essere mutabili (e quindi dichiarati in una mentalità di programmazione funzionale), non hanno uno stato interno, nessun invariante che deve essere protetto. È completamente valido aggiungere operazioni a un record che ne semplifichi l'utilizzo.

Ad esempio, possiamo sostenere che un vettore 3D è un record stupido. Tuttavia, ciò non dovrebbe impedirci di aggiungere un metodo simile add, il che rende più semplice l'aggiunta di vettori. L'aggiunta del comportamento non trasforma un record (non così stupido) in un ibrido.

Questa linea viene attraversata quando l'interfaccia pubblica di un oggetto ci consente di interrompere l'incapsulamento: ci sono alcuni interni a cui possiamo accedere direttamente, portando così l'oggetto in uno stato non valido. Mi sembra che Dataabbia uno stato e che possa essere portato in uno stato non valido:

  • Dopo aver gestito uno scambio, l'ultimo aggressore potrebbe non essere aggiornato.
  • L'ultimo aggressore può essere aggiornato anche quando non si sono verificati nuovi scambi.
  • La lunghezza della corsa potrebbe conservare il suo vecchio valore anche quando l'aggressore è stato aggiornato.
  • eccetera.

Se uno stato è valido per i tuoi dati, allora tutto va bene con il tuo codice e puoi continuare. Altrimenti: la Dataclasse è responsabile della propria coerenza dei dati. Se la gestione di uno scambio implica sempre l'aggiornamento dell'aggressore, questo comportamento deve far parte della Dataclasse. Se la modifica dell'aggressore comporta l'impostazione della lunghezza della corsa su zero, questo comportamento deve far parte della Dataclasse. Datanon è mai stato un disco stupido. L'hai già reso un ibrido aggiungendo setter pubblici.

C'è uno scenario in cui puoi considerare di rilassare queste rigide responsabilità: se Dataè privato per il tuo progetto e quindi non fa parte di alcuna interfaccia pubblica, puoi comunque garantire un corretto utilizzo della classe. Tuttavia, ciò pone la responsabilità di mantenere la Datacoerenza in tutto il codice, piuttosto che raccoglierli in una posizione centrale.

Di recente ho scritto una risposta sull'incapsulamento , che approfondisce cos'è l'incapsulamento e come è possibile garantirlo.


5

Il fatto che handleTrade()e updateRun()avvenga sempre insieme (e il secondo metodo sia effettivamente sul visitatore e chiama diversi altri metodi sull'oggetto dati) profuma di accoppiamento temporale . Ciò significa che è necessario chiamare i metodi in un ordine specifico e immagino che chiamare i metodi fuori servizio romperà qualcosa nel peggiore dei casi o non riuscirà a fornire un risultato significativo nella migliore delle ipotesi. Non bene.

Tipicamente, il modo corretto di escludere tale dipendenza è che ogni metodo restituisca un risultato che può essere immesso nel metodo successivo o applicato direttamente.

Vecchio codice:

MyObject x = ...;
x.actionOne();
x.actionTwo();
String result = x.actionThree();

Nuovo codice:

MyObject x = ...;
OneResult r1 = x.actionOne();
TwoResult r2 = r1.actionTwo();
String result = r2.actionThree();

Questo ha diversi vantaggi:

  • Sposta preoccupazioni separate in oggetti separati ( SRP ).
  • Rimuove l'accoppiamento temporale: è impossibile chiamare i metodi fuori servizio e le firme dei metodi forniscono una documentazione implicita su come chiamarli. Hai mai guardato la documentazione, visto l'oggetto desiderato e lavorato all'indietro? Voglio l'oggetto Z. Ma ho bisogno di una Y per ottenere una Z. Per ottenere una Y, ho bisogno di una X. Aha! Ho una W, che è necessaria per ottenere una X. Incatenala tutta insieme, e la tua W ora può essere utilizzata per ottenere una Z.
  • Suddividere gli oggetti in questo modo ha maggiori probabilità di renderli immutabili, il che comporta numerosi vantaggi al di là dell'ambito di questa domanda. La soluzione rapida è che gli oggetti immutabili tendono a portare a un codice più sicuro.

Non esiste alcun accoppiamento temporale tra queste due chiamate di metodo. Scambia il loro ordine e il comportamento non cambia.
durron597,

1
Inizialmente ho anche pensato all'accoppiamento sequenziale / temporale durante la lettura della domanda, ma poi ho notato che il updateRunmetodo era privato . Evitare l'accoppiamento sequenziale è un buon consiglio, ma si applica solo alla progettazione API / alle interfacce pubbliche e non ai dettagli di implementazione. La vera domanda sembra essere se updateRundovrebbe essere nel visitatore o nella classe di dati, e non vedo come questa risposta affronti quel problema.
amon,

La visibilità di updateRunè irrilevante, l'importante è l'implementazione di this.datacui non è presente nella domanda ed è l'oggetto manipolato dall'oggetto visitatore.

Semmai, il fatto che questo visitatore stia semplicemente chiamando un gruppo di setter e non stia effettivamente elaborando nulla è una ragione per cui l'accoppiamento temporale non è presente. Probabilmente non importa quale ordine vengano chiamati i setter.

0

Dal mio punto di vista una classe dovrebbe contenere "valori per stato (variabili membro) e implementazioni di comportamento (funzioni membro, metodi)".

Le "sfortunate strutture dati ibride" emergono se si rendono pubbliche le variabili membro dello stato di classe (o i loro getter / setter) che non dovrebbero essere pubbliche.

Quindi vedo che non è necessario disporre di classi separate per oggetti dati dati e oggetti worker.

Dovresti essere in grado di mantenere non pubbliche le variabili dei membri dello stato (il tuo livello di database dovrebbe essere in grado di gestire variabili dei membri non pubbliche)

Un'invidia caratteristica è una classe che utilizza eccessivamente metodi di un'altra classe. Vedi Code_smell . Avere una classe con metodi e stato eliminerebbe questo.

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.