In genere mandi oggetti o le loro variabili membro in funzioni?

22

Che è una pratica generalmente accettata tra questi due casi:

function insertIntoDatabase(Account account, Otherthing thing) {
    database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue());
}

o

function insertIntoDatabase(long accountId, long thingId, double someValue) {
    database.insertMethod(accountId, thingId, someValue);
}

In altre parole, generalmente è meglio passare interi oggetti in giro o solo i campi di cui hai bisogno?

    
posta Aruka J 24.05.2016 - 20:59
fonte

10 risposte

19

Né è generalmente meglio dell'altro. È una sentenza che devi fare caso per caso.

Ma in pratica, quando sei in una posizione in cui puoi effettivamente prendere questa decisione, è perché devi decidere quale livello nell'architettura del programma generale deve suddividere l'oggetto in primitive, quindi dovresti stai pensando all'intero stack di chiamate , non solo a questo metodo in cui ti trovi attualmente. Presumibilmente, la separazione deve essere eseguita da qualche parte, e non avrebbe senso (o sarebbe inutilmente soggetta a errori ) per farlo più di una volta. La domanda è dove dovrebbe essere quel posto.

Il modo più semplice per prendere questa decisione è pensare a quale codice dovrebbe o non dovrebbe essere modificato se l'oggetto viene modificato . Esponiamo un po 'il tuo esempio:

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account, data);
}
function insertIntoDatabase(Account account, Otherthing data) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(account.getId(), data.getId(), data.getSomeValue());
}

vs

function addWidgetButtonClicked(clickEvent) {
    // get form data
    // get user's account
    insertIntoDatabase(account.getId(), data.getId(), data.getSomeValue());
}
function insertIntoDatabase(long accountId, long dataId, double someValue) {
    // open database connection
    // check data doesn't already exist
    database.insertMethod(accountId, dataId, someValue);
}

Nella prima versione, il codice dell'interfaccia utente passa ciecamente l'oggetto data e tocca al codice del database per estrarre i campi utili da esso. Nella seconda versione, il codice UI sta suddividendo l'oggetto data nei suoi campi utili e il codice del database li riceve direttamente senza sapere da dove provengono. L'implicazione chiave è che, se la struttura dell'oggetto data dovesse cambiare in qualche modo, la prima versione richiederebbe solo la modifica del codice del database, mentre la seconda versione richiederebbe solo la modifica del codice dell'interfaccia utente . Quale di questi due è corretta dipende in gran parte dal tipo di dati contenuti nell'oggetto data , ma di solito è molto ovvio. Ad esempio, se data è una stringa fornita dall'utente come "20/05/1999", è necessario che il codice UI sia convertito in un corretto tipo Date prima di passarlo.

    
risposta data 24.05.2016 - 22:12
fonte
5

Quindi quando crei una funzione, dichiari implicitamente un contratto con il codice che lo chiama. "Questa funzione prende queste informazioni e le trasforma in quest'altra cosa (possibilmente con effetti collaterali)".

Quindi, il tuo contratto dovrebbe essere logicamente con gli oggetti (comunque implementati), o con i campi che solo così accadono per far parte di questi altri oggetti. Stai aggiungendo l'accoppiamento in entrambi i modi, ma come programmatore, spetta a te decidere dove appartiene.

In generale , se non è chiaro, quindi privilegia i più piccoli dati necessari affinché la funzione funzioni. Ciò significa spesso passare solo i campi, poiché la funzione non ha bisogno delle altre cose trovate negli oggetti. Ma a volte prendere l'intero oggetto è più corretto in quanto produce meno impatto quando le cose cambiano inevitabilmente in futuro.

    
risposta data 24.05.2016 - 21:32
fonte
5

Questo non è un elenco esaustivo, ma considera alcuni dei seguenti fattori quando decidi se un oggetto deve essere passato a un metodo come argomento:

L'oggetto è immutabile? La funzione è "pura"?

Effetti collaterali sono una considerazione importante per la manutenibilità del tuo codice. Quando vedi il codice con un sacco di oggetti stateful mutabili passati in giro dappertutto, quel codice è spesso meno intuitivo (nello stesso modo in cui le variabili di stato globali possono essere spesso meno intuitive), e il debug spesso diventa più difficile e veloce. consumando.

Come regola generale, mirare a garantire, per quanto ragionevolmente possibile, che qualsiasi oggetto che passi a un metodo sia chiaramente immutabile.

Evitare (di nuovo, per quanto ragionevolmente possibile) qualsiasi progetto in cui si prevede che lo stato di un argomento venga modificato come risultato di una chiamata di funzione - uno degli argomenti più forti per questo approccio è Principio di Least Astonishment ; Ad esempio, qualcuno che legge il tuo codice e vede un argomento passato in una funzione è "meno probabile" aspettarsi che il suo stato cambi dopo che la funzione è tornata.

Quanti argomenti ha già il metodo?

I metodi con elenchi di argomenti eccessivamente lunghi (anche se la maggior parte di questi argomenti ha valori "predefiniti") iniziano ad apparire come un odore di codice. A volte tali funzioni sono necessarie, tuttavia, e potresti prendere in considerazione la creazione di una classe il cui unico scopo è quello di agire come un Oggetto Parametro .

Questo approccio può comportare una piccola quantità di mapping di codici di piastre supplementari dal tuo oggetto "sorgente" al tuo oggetto parametro, ma questo è un costo piuttosto basso in termini di prestazioni e complessità, tuttavia, e ci sono una serie di vantaggi in termini di disaccoppiamento e immutabilità dell'oggetto.

L'oggetto passato appartiene esclusivamente a un "livello" all'interno dell'applicazione (ad esempio, un ViewModel o un'entità ORM?)

Pensa a Separation of Concerns (SoC) . A volte chiedersi se l'oggetto "appartiene" allo stesso livello o modulo in cui esiste il tuo metodo (ad esempio una libreria wrapper API rollata a mano o il tuo core business logic layer ecc.) Può informare se quell'oggetto dovrebbe essere realmente passato a quello metodo.

SoC è una buona base per scrivere codice pulito, liberamente accoppiato, modulare. ad esempio, un oggetto di entità ORM (mappatura tra il codice e lo schema del database) idealmente non dovrebbe essere distribuito nel tuo livello aziendale, o peggio nel tuo livello di presentazione / interfaccia utente.

Nel caso di passaggio di dati tra 'strati', i parametri di dati semplici passati in un metodo sono solitamente preferibili al passaggio in un oggetto dal livello 'errato'. Anche se è probabilmente una buona idea avere modelli separati che esistono al livello 'giusto' su cui invece puoi mappare.

La funzione stessa è troppo grande e / o complessa?

Quando una funzione ha bisogno di molti elementi di dati, potrebbe valere la pena considerare se quella funzione sta assumendo troppe responsabilità; cercare potenziali opportunità di refactoring utilizzando oggetti più piccoli e funzioni più brevi e più semplici.

La funzione dovrebbe essere un oggetto comando / query?

In alcuni casi la relazione tra i dati e la funzione potrebbe essere vicina; in questi casi, considera se un Oggetto Comando o un Oggetto Query sia appropriato.

L'aggiunta di un parametro oggetto a un metodo impone alla classe contenitore di adottare nuove dipendenze?

A volte l'argomento più strong per gli argomenti "Plain old data" è semplicemente che la classe ricevente è già ordinatamente autosufficiente, e l'aggiunta di un parametro oggetto a uno dei suoi metodi inquinerebbe la classe (o se la classe è già inquinata, quindi peggiorerà l'entropia esistente)

Hai veramente bisogno di passare attorno a un oggetto completo o hai solo bisogno di una piccola parte dell'interfaccia di quell'oggetto?

Considera il Principio di segregazione dell'interfaccia in relazione alle tue funzioni, ad esempio quando passi in un oggetto , dovrebbe dipendere solo da parti dell'interfaccia di quell'argomento di cui ha effettivamente bisogno la funzione.

    
risposta data 14.03.2017 - 13:50
fonte
3

Dipende.

Per elaborare, i parametri accettati dal tuo metodo dovrebbero corrispondere semanticamente a ciò che stai cercando di fare. Prendi in considerazione un EmailInviter e queste tre possibili implementazioni di un metodo invite :

void invite(String emailAddressString) {
  invite(EmailAddress.parse(emailAddressString));
}
void invite(EmailAddress emailAddress) {
  ...
}
void invite(User user) {
  invite(user.getEmailAddress());
}

Il passaggio a String in cui dovresti passare in EmailAddress è errato perché non tutte le stringhe sono indirizzi email. La classe EmailAddress migliora semanticamente il comportamento del metodo. Tuttavia il passaggio di un User è anch'esso difettoso, perché mai un EmailInviter dovrebbe essere limitato all'invito degli utenti? E le imprese? Cosa succede se stai leggendo gli indirizzi email da un file o da una riga di comando e non sono associati agli utenti? Mailing list? L'elenco continua.

Ci sono alcuni segnali di pericolo che puoi usare come guida qui. Se stai utilizzando un tipo di valore semplice come String o int ma non tutte le stringhe o gli interi sono validi o c'è qualcosa di "speciale" su di essi, dovresti utilizzare un tipo più significativo. Se stai usando un oggetto e l'unica cosa che fai è chiamare un getter, allora dovresti passare l'oggetto nel getter direttamente. Queste linee guida non sono né dure né veloci, ma poche linee guida sono.

    
risposta data 25.05.2016 - 00:47
fonte
0

Clean Code raccomanda di avere il minor numero possibile di argomenti, il che significa che Object sarebbe di solito l'approccio migliore e penso che abbia un senso. perché

insertIntoDatabase(new Account(id) , new Otherthing(id, "Value"));

è una chiamata più leggibile di

insertIntoDatabase(myAccount.getId(), myOtherthing.getId(), myOtherthing.getValue() );
    
risposta data 24.05.2016 - 21:41
fonte
0

Passa intorno all'oggetto, non al suo stato costituente. Questo supporta i principi orientati agli oggetti dell'incapsulamento e del nascondimento dei dati. Esporre le viscere di un oggetto in varie interfacce di metodo dove non è necessario viola i principi OOP di base.

Che cosa accade se modifichi i campi in Otherthing ? Forse cambi un tipo, aggiungi un campo o rimuovi un campo. Ora tutti i metodi come quello che hai citato nella tua domanda devono essere aggiornati. Se passi intorno all'oggetto, non ci sono modifiche all'interfaccia.

L'unica volta che dovresti scrivere un metodo accettando i campi su un oggetto è quando scrivi un metodo per recuperare l'oggetto:

public User getUser(String primaryKey) {
  return ...;
}

Al momento di fare quella chiamata, il codice chiamante non ha ancora un riferimento all'oggetto perché il punto di chiamare quel metodo è quello di ottenere l'oggetto.

    
risposta data 24.05.2016 - 21:45
fonte
0

Dal punto di vista della manutenibilità, gli argomenti dovrebbero essere chiaramente distinguibili gli uni dagli altri preferibilmente a livello di compilatore.

// this has exactly one way to call it
insertIntoDatabase(Account ..., Otherthing ...)

// the parameter order can be confused in practice
insertIntoDatabase(long ..., long ...)

Il primo design porta al rilevamento precoce dei bug. Il secondo design può portare a problemi di runtime che non vengono visualizzati nei test. Pertanto, è preferibile il primo disegno.

    
risposta data 14.03.2017 - 13:47
fonte
0

Tra i due, la mia preferenza è il primo metodo:

function insertIntoDatabase(Account account, Otherthing thing) { database.insertMethod(account.getId(), thing.getId(), thing.getSomeValue()); }

La ragione è che le modifiche apportate a entrambi gli oggetti lungo la strada, a patto che le modifiche conservino quei getter in modo che la modifica sia trasparente all'esterno dell'oggetto, allora hai meno codice da modificare e test e meno possibilità di interrompere l'app .

Questo è solo il mio processo di pensiero, principalmente basato sul modo in cui mi piace lavorare e strutturare cose di questa natura e che risultano essere abbastanza gestibili e mantenibili a lungo termine.

Non intendo entrare nelle convenzioni sui nomi, ma vorrei sottolineare che sebbene questo metodo contenga la parola "database", il meccanismo di archiviazione può cambiare strada. Dal codice visualizzato, non c'è nulla che colleghi la funzione alla piattaforma di archiviazione del database in uso - o anche se si tratta di un database. Abbiamo appena assunto perché è nel nome. Anche in questo caso, supponendo che i getter siano sempre preservati, cambiare la modalità di memorizzazione di questi oggetti sarà facile.

Vorrei ripensare alla funzione e ai due oggetti anche se si dispone di una funzione che ha una dipendenza da due strutture di oggetti, e in particolare dai getter in uso. Sembra anche che questa funzione leghi questi due oggetti in una cosa cumulativa che viene mantenuta. Il mio istinto mi sta dicendo che un terzo oggetto potrebbe avere un senso. Avrei bisogno di sapere di più su questi oggetti e su come si relazionano in attualità e nella tabella di marcia prevista. Ma il mio istinto si sta inclinando in quella direzione.

Come il codice ora si trova, la domanda chiede "dove dovrebbe o dovrebbe essere questa funzione?" Fa parte dell'account o OtherThing? Dove va?

Suppongo che ci sia già un "database" di oggetti terzi, e mi sto proponendo di inserire questa funzione in quell'oggetto, e poi diventa quel lavoro degli oggetti che è in grado di gestire un Account e un OtherThing, trasformare e quindi anche persistere il risultato.

Se si dovesse arrivare fino a rendere quel terzo oggetto conforme a un modello di mappatura oggetto-relazionale (ORM), tanto meglio. Ciò renderebbe molto ovvio a chiunque lavori con il codice per capire "Ah, questo è il punto in cui Account e OtherThing vengono distrutti insieme e persistono".

Ma potrebbe anche avere senso introdurre un quarto oggetto, che gestisce il lavoro di combinazione e trasformazione di un Account e un Altro, ma non gestisce i meccanismi di persistenza. Lo farei se prevedi molte più interazioni con o tra questi due oggetti, perché crescendo vorrei che i bit di persistenza venissero inclusi in un oggetto che gestisce solo la persistenza.

Sparerei per mantenere il design in modo tale che qualsiasi Account, OtherThing o il terzo oggetto ORM possa essere modificato senza dover cambiare anche gli altri tre. A meno che non ci sia una buona ragione per non farlo, vorrei che Account e OtherThing siano indipendenti e non debbano conoscere i meccanismi interni e le strutture l'uno dell'altro.

Naturalmente, se conoscessi l'intero contesto in cui ciò avverrà, potrei cambiare completamente le mie idee. Ancora una volta, questo è solo il modo in cui penso quando vedo cose come questa, e quanto sia magro.

    
risposta data 14.03.2017 - 17:27
fonte
0

Entrambi gli approcci hanno i loro pro e contro. Ciò che è meglio in uno scenario dipende molto dal caso d'uso a portata di mano.

Parametri multipli Pro, riferimento Oggetto Con:

  • Chiamante non associato a una classe specifica , può passare del tutto valori da diverse origini
  • Lo stato dell'oggetto è sicuro di essere modificato in modo imprevisto all'interno dell'esecuzione del metodo.

Riferimento oggetto Pro:

  • Interfaccia chiara che il metodo è associato a Tipo di riferimento oggetto, che rende difficile passare accidentalmente valori / non validi
  • non collegati
  • Rinominare un campo / getter richiede modifiche a tutte le invocazioni del metodo e non solo nella sua implementazione
  • Se viene aggiunta una nuova proprietà e non è necessario apportare modifiche alla firma del metodo
  • Il metodo può mutare lo stato dell'oggetto
  • Il passaggio di troppe variabili di tipi primitivi simili lo rende confuso per il chiamante rispetto all'ordine (problema con il modello Builder)

Quindi, che cosa deve essere usato e quando dipende molto dai casi d'uso

  1. Passa i singoli parametri: In generale, se il metodo ha nulla a che fare con il tipo di oggetto è meglio passare l'elenco dei parametri in modo che sia applicabile a un pubblico più ampio.
  2. Introduci nuovo oggetto modello: se l'elenco dei parametri diventa grande (più di 3), è meglio introdurre un nuovo oggetto modello appartenente all'API chiamata (modello di build preferito)
  3. Passa riferimento agli oggetti: se il metodo è correlato agli oggetti del dominio, è meglio dal punto di vista della gestibilità e della leggibilità passare i riferimenti all'oggetto.
risposta data 18.03.2017 - 12:45
fonte
0

Da un lato c'è un account e un oggetto Altro. Dall'altra parte, hai la possibilità di inserire un valore in un database, dato l'id di un account e l'id di un Otherthing. Queste sono le due cose date.

Puoi scrivere un metodo prendendo Account e Altro come argomenti. Dal punto di vista professionale, il chiamante non ha bisogno di conoscere alcun dettaglio su Account e Altro. Sul lato negativo, il callee deve conoscere i metodi di Account e Otherthing. E inoltre, non c'è modo di inserire nient'altro in un database che il valore di un oggetto Otherthing e nessun modo di usare questo metodo se si ha l'id di un oggetto account, ma non l'oggetto stesso.

Oppure puoi scrivere un metodo prendendo due ID e un valore come argomenti. Sul lato negativo, il chiamante deve conoscere i dettagli di Account e Altro. E potrebbe esserci una situazione in cui hai effettivamente bisogno di più dettagli di un Account o Altro che solo l'ID da inserire nel database, nel qual caso questa soluzione è totalmente inutile. D'altra parte, si spera che nessuna conoscenza di Account e Otherthing sia necessaria nel callee, e c'è più flessibilità.

Il tuo giudizio: è necessaria più flessibilità? Questo spesso non è una questione di una chiamata, ma sarebbe coerente con tutto il tuo software: o usi gli ID di Account per la maggior parte del tempo, o usi gli oggetti. Mescolarlo ti rende il peggiore di entrambi i mondi.

In C ++, puoi avere un metodo che prende due ID più valore, e un metodo inline che prende Account e Altro, in modo da avere entrambi i modi con zero spese generali.

    
risposta data 19.03.2017 - 22:25
fonte