Clean Code e Hybrid Objects e Feature Invidia

9

Quindi di recente ho apportato alcuni importanti refactoring al mio codice. Una delle cose principali che ho provato a fare era dividere le mie classi in oggetti dati e oggetti di lavoro. Questo è stato ispirato, tra le altre cose, da questa sezione di Pulisci codice :

Hybrids

This confusion sometimes leads to unfortunate hybrid data structures that are half object and half data structure. They have functions that do significant things, and they also have either public variables or public accessors and mutators that, for all intents and purposes, make the private variables public, tempting other external functions to use those variables the way a procedural program would use a data structure.

Such hybrids make it hard to add new functions but also make it hard to add new data structures. They are the worst of both worlds. Avoid creating them. They are indicative of a muddled design whose authors are unsure of - or worse, ignorant of - whether they need protection from functions or types.

Recentemente stavo guardando il codice a uno dei miei oggetti di lavoro (che accade implementando il Pattern visitatori ) e 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());
}

Ho subito detto a me stesso "invidia per feature! questa logica dovrebbe essere nella classe Data - in particolare nel metodo handleTrade . handleTrade e updateRun dovrebbe sempre avvenire insieme" . Ma poi ho pensato "la classe dati è solo una struttura dati public , se comincio a farlo, allora verrà un oggetto ibrido!"

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

    
posta durron597 10.10.2014 - 18:26
fonte

3 risposte

9

Il testo che hai citato ha un buon consiglio, anche se sostituirò "strutture dati" con "record", assumendo che si intenda qualcosa come le strutture. I record sono solo stupide aggregazioni di dati. Anche se potrebbero essere mutevoli (e quindi di stato in una mentalità di programmazione funzionale), non hanno nessuno stato interno, né invarianti che devono essere protetti. È completamente valido aggiungere operazioni a un record che ne faciliti l'utilizzo.

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

Questa linea è attraversata quando l'interfaccia pubblica di un oggetto ci consente di interrompere l'incapsulamento: ci sono alcuni interni ai quali possiamo accedere direttamente, portando così l'oggetto in uno stato non valido. Mi sembra che Data abbia lo 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 durata della corsa potrebbe mantenere il suo vecchio valore anche quando l'aggressore è stato aggiornato.
  • ecc.

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

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

Recentemente ho scritto una risposta sull'incapsulamento , che approfondisce l'incapsulamento e come puoi assicurarlo .

    
risposta data 13.10.2014 - 18:02
fonte
4

Il fatto che handleTrade() e updateRun() avvengano sempre insieme (e il secondo metodo è in realtà sul visitatore e chiama diversi altri metodi sull'oggetto dei dati) odori di accoppiamento temporale . Ciò significa che i devono chiamare i metodi in un ordine specifico, e suppongo che i metodi di chiamata non funzionanti spezzino qualcosa nel peggiore dei casi, o non riescano a fornire un risultato significativo al meglio. Non va bene.

In genere, il metodo corretto per il refactoring di dipendenze è che ogni metodo restituisca un risultato che può essere inserito nel metodo successivo o eseguito direttamente.

Codice vecchio:

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 le 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 che vuoi 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. Catena tutto insieme, e la tua W può ora essere usata per ottenere una Z.
  • Divisione di oggetti come questa è più probabile che li rendano immutabili, il che presenta numerosi vantaggi che vanno oltre lo scopo di questa domanda. Il rapido takeaway è che gli oggetti immutabili tendono a condurre a un codice più sicuro.
risposta data 13.10.2014 - 16:43
fonte
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 rendi pubbliche le variabili membro membro (oi loro getter / setter) pubblici che non dovrebbero essere pubblici.

Quindi non vedo la necessità di avere classi separate per gli oggetti dati di dati e gli oggetti di lavoro.

Dovresti essere in grado di mantenere le variabili membro membro non pubbliche (il tuo livello database dovrebbe essere in grado di gestire variabili membro non pubbliche)

L'invidia per le caratteristiche è una classe che usa eccessivamente metodi di un'altra classe. Vedi Code_smell . Avere una classe con metodi e stato eliminerebbe questo.

    
risposta data 13.10.2014 - 16:16
fonte