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


31

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, in genere è meglio passare interi oggetti o solo i campi necessari?


5
Dipenderebbe del tutto dalla funzione della funzione e da come si relaziona (o non si riferisce) all'oggetto in questione.
MetaFight,

Questo è il problema. Non so quando userei l'uno o l'altro. Sento che potrei sempre cambiare il codice per adattarsi a entrambi gli approcci.
AJJ,

1
In termini di API (e non osservando affatto le implementazioni), la prima è astratta e orientata al dominio (il che è buono), mentre la seconda no (il che è negativo).
Erik Eidt,

1
Il primo approccio sarebbe più OO a 3 livelli. Ma dovrebbe esserlo ancora di più eliminando il database di parole dal metodo. Dovrebbe essere "Store" o "Persist" e fare Account o Cosa (non entrambi). Come client di questo livello non dovresti essere a conoscenza del supporto di archiviazione. Quando si recupera un account, è necessario passare l'id o una combinazione di valori di proprietà (non valori di campo) per identificare l'oggetto desiderato. O / e implenente un metodo di enumerazione che passa tutti gli account.
Martin Maat,

1
Tipicamente, entrambi sarebbero sbagliati (o, piuttosto, meno che ottimali). Il modo in cui un oggetto deve essere serializzato nel database dovrebbe essere una proprietà (una funzione membro) dell'oggetto, poiché in genere dipende direttamente dalle variabili membro dell'oggetto. Nel caso in cui cambiate membri dell'oggetto, dovrete anche cambiare il metodo di serializzazione. Funziona meglio se fa parte dell'oggetto
fino al

Risposte:


24

Nessuno dei due è generalmente migliore dell'altro. È un giudizio 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 strato nell'architettura generale del programma dovrebbe suddividere l'oggetto in primitivi, quindi dovresti pensare all'intera chiamata stack , non solo questo metodo in cui ti trovi attualmente. Presumibilmente la rottura deve essere fatta da qualche parte, e non avrebbe senso (o sarebbe inutilmente soggetto a errori) 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 debba essere modificato se l'oggetto viene cambiato . Espandiamo leggermente 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' dataoggetto e spetta al codice del database estrarre i campi utili da esso. Nella seconda versione, il codice dell'interfaccia utente sta suddividendo l' dataoggetto nei suoi campi utili e il codice del database li riceve direttamente senza sapere da dove provengono. L'implicazione chiave è che, se la struttura datadell'oggetto 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 sia corretto dipende in gran parte dal tipo di dati datacontenuti nell'oggetto, ma di solito è molto ovvio. Ad esempio, sedataè una stringa fornita dall'utente come "20/05/1999", dovrebbe essere il codice dell'interfaccia utente per convertirlo in un Datetipo corretto prima di passarlo.


8

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"?

Gli effetti collaterali sono una considerazione importante per la manutenibilità del codice. Quando vedi codice con molti oggetti stateful mutabili che passano dappertutto, quel codice è spesso meno intuitivo (allo stesso modo in cui le variabili di stato globali possono spesso essere meno intuitive) e il debug diventa spesso più difficile e il tempo- consumando.

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

Evita (di nuovo, per quanto ragionevolmente possibile) qualsiasi progetto in base al quale lo stato di un argomento dovrebbe essere modificato a seguito di una chiamata di funzione - uno degli argomenti più forti per questo approccio è il Principio del minimo stupore ; cioè 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 quegli argomenti hanno valori 'predefiniti') iniziano a sembrare 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 è agire come un oggetto parametro .

Questo approccio può comportare una piccola quantità di mappatura aggiuntiva del codice del boilerplate dal tuo oggetto "source" al tuo oggetto parametro, ma è comunque un costo piuttosto basso sia in termini di prestazioni che di complessità, 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 ti chiedi se l'oggetto "appartiene" allo stesso livello o modulo in cui esiste il tuo metodo (ad esempio una libreria wrapper API arrotolata a mano, o il tuo livello di logica aziendale principale, ecc.) Può informare se quell'oggetto deve davvero essere passato a quello metodo.

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

Nel caso del passaggio di dati tra "livelli", in genere è preferibile avere parametri di dati semplici passati in un metodo piuttosto che passare un oggetto dal livello "sbagliato". Anche se è probabilmente una buona idea avere modelli separati che esistono al livello "giusto" su cui è possibile mappare invece.

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 può essere stretta; in quei casi considera se un oggetto comando o un oggetto query sarebbe appropriato.

L'aggiunta di un parametro oggetto a un metodo forza la classe contenitrice ad adottare nuove dipendenze?

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

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

Considera il Principio di segregazione dell'interfaccia rispetto alle tue funzioni - vale a dire quando passa in un oggetto, dovrebbe dipendere solo da parti dell'interfaccia di quell'argomento di cui ha effettivamente bisogno (la funzione).


5

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

Quindi, il tuo contratto dovrebbe logicamente essere con gli oggetti (comunque siano implementati) o con i campi che fanno 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, favorire i dati più piccoli necessari per il funzionamento della funzione. Ciò significa spesso passare solo nei campi, poiché la funzione non ha bisogno delle altre cose trovate negli oggetti. Ma a volte prendere l'intero oggetto è più corretto poiché comporta un impatto minore quando le cose inevitabilmente cambiano in futuro.


Perché non cambi il nome del metodo in insertAccountIntoDatabase o hai intenzione di passare qualsiasi altro tipo ?. Con un certo numero di argomenti per usare obj è facile leggere il codice. Nel tuo caso, preferirei pensare se il nome del metodo cancella ciò che sto per inserire invece di come lo farò.
Laiv

3

Dipende.

Per elaborare, i parametri accettati dal metodo devono corrispondere semanticamente a ciò che stai cercando di fare. Considera una EmailInvitere queste tre possibili implementazioni di un invitemetodo:

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

Il passaggio a un punto in Stringcui dovresti passare EmailAddressè imperfetto perché non tutte le stringhe sono indirizzi e-mail. La EmailAddressclasse corrisponde meglio semanticamente al comportamento del metodo. Tuttavia, anche il passaggio in a Userè imperfetto perché perché mai dovrebbe EmailInviteressere limitato a invitare gli utenti? E le imprese? Cosa succede se stai leggendo indirizzi e-mail 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 avvertimento che puoi usare come guida qui. Se stai usando un tipo di valore semplice come Stringo intma non tutte le stringhe o ints sono valide o c'è qualcosa di "speciale" in esse, dovresti usare un tipo più significativo. Se stai usando un oggetto e l'unica cosa che fai è chiamare un getter, dovresti invece passare direttamente l'oggetto nel getter. Queste linee guida non sono né difficili né veloci, ma poche linee guida lo sono.


0

Clean Code raccomanda di avere il minor numero di argomenti possibile, 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() );

non posso essere d'accordo lì. I due non sono sinonimi. La creazione di 2 nuove istanze di oggetti solo per passarle a un metodo non è buona. Userei insertIntoDatabase (myAccount, myOtherthing) invece di una delle tue opzioni.
jwenting

0

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

Cosa succede se si cambiano i campi in Otherthing? Forse cambi un tipo, aggiungi un campo o rimuovi un campo. Ora tutti i metodi come quello che menzioni nella tua domanda devono essere aggiornati. Se si passa intorno all'oggetto, non ci sono modifiche all'interfaccia.

L'unica volta in cui 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 effettuare quella chiamata, il codice chiamante non ha ancora un riferimento all'oggetto perché il punto di chiamare quel metodo è ottenere l'oggetto.


1
"Cosa succede se si cambiano i campi in Otherthing?" (1) Sarebbe una violazione del principio aperto / chiuso. (2) anche se si passa all'intero oggetto, il codice al suo interno accede quindi ai membri di quell'oggetto (e in caso contrario, perché passare l'oggetto?) Si spezzerebbe comunque ...
David Arno,

@DavidArno il punto della mia risposta non è che nulla si spezzerebbe, ma meno si spezzerebbe. Non dimenticare nemmeno il primo paragrafo: indipendentemente da ciò che si rompe, lo stato interno di un oggetto dovrebbe essere astratto usando l'interfaccia dell'oggetto. Passare attorno al suo stato interno è una violazione dei principi OOP.

0

Dal punto di vista della manutenibilità, gli argomenti dovrebbero essere chiaramente distinguibili l'uno dall'altro 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 progetto porta al rilevamento tempestivo di bug. Il secondo design può portare a sottili problemi di runtime che non vengono visualizzati nei test. Pertanto, il primo disegno deve essere preferito.


0

Delle due, la mia preferenza è il primo metodo:

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

Il motivo è che le modifiche apportate a entrambi gli oggetti lungo la strada, purché le modifiche preservino quei getter in modo che la modifica sia trasparente all'esterno dell'oggetto, quindi hai meno codice da modificare e testare e meno possibilità di interrompere l'app.

Questo è solo il mio processo di pensiero, principalmente basato su come mi piace lavorare e strutturare cose di questa natura e che si dimostrano abbastanza gestibili e mantenibili a lungo termine.

Non entrerò nelle convenzioni di denominazione, ma vorrei sottolineare che sebbene questo metodo contenga la parola "database", quel meccanismo di archiviazione può cambiare lungo la strada. Dal codice mostrato, non c'è nulla che lega la funzione alla piattaforma di archiviazione del database in uso - o anche se si tratta di un database. Abbiamo appena assunto perché è nel nome. Ancora una volta, supponendo che quei getter siano sempre conservati, sarà facile cambiare come / dove sono memorizzati questi oggetti.

Vorrei ripensare la funzione e i due oggetti, perché hai una funzione che ha una dipendenza da due strutture di oggetti, e in particolare dai getter impiegati. Sembra anche che questa funzione stia legando quei 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 saperne di più su questi oggetti e su come si relazionano nella realtà e nella tabella di marcia anticipata. Ma il mio istinto è inclinato in quella direzione.

Allo stato attuale del codice, la domanda pone "Dove sarebbe o dovrebbe essere questa funzione?" Fa parte di Account o Altro? Dove va?

Immagino che ci sia già un terzo "database" di oggetti, e mi sto inclinando a mettere questa funzione in quell'oggetto, e poi diventa quel lavoro di oggetti per essere in grado di gestire un Account e un Altro, trasformare e quindi persistere anche il risultato .

Se dovessi arrivare a far conformare quel terzo oggetto a un modello di mappatura relazionale a oggetti (ORM), tanto meglio. Ciò renderebbe molto ovvio a chiunque lavori con il codice capire "Ah, è qui che Account e OtherThing vengono messi insieme e persistono".

Ma potrebbe anche avere senso introdurre un quarto oggetto, che gestisce il lavoro di combinazione e trasformazione di un Conto e un Altro, ma non di gestire i meccanismi della persistenza. Lo farei se prevedi molte più interazioni con o tra questi due oggetti, perché a quel punto vorrei che i bit di persistenza fossero presi in considerazione in un oggetto che gestisse solo la persistenza.

Vorrei scattare per mantenere il design in modo tale che uno qualsiasi degli 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 fossero indipendenti e non dovessero conoscere i meccanismi interni e le strutture reciproche.

Certo, se conoscessi il contesto completo che questo sarà, potrei cambiare completamente le mie idee. Ancora una volta, questo è il modo in cui penso quando vedo cose del genere, e quanto sia magra.


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.


Pro parametri multipli, riferimento all'oggetto Con:

  • Il chiamante non è associato a una classe specifica , può passare del tutto i valori da fonti diverse
  • Lo stato dell'oggetto non può essere modificato in modo imprevisto durante l'esecuzione del metodo.

Riferimento oggetto Pro:

  • Interfaccia chiara che il metodo è associato al tipo di riferimento Oggetto, rendendo difficile il passaggio accidentale di valori non correlati / non validi
  • La ridenominazione di un campo / getter richiede modifiche in tutte le invocazioni del metodo e non solo nella sua implementazione
  • Se viene aggiunta una nuova proprietà e deve essere passata, non sono necessarie modifiche nella firma del metodo
  • Il metodo può mutare lo stato dell'oggetto
  • Passare troppe variabili di tipi primitivi simili rende confuso il chiamante per quanto riguarda l'ordine (problema del modello del costruttore)

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

  1. Passa parametri individuali: in generale, se il metodo ha non nulla a che fare con il tipo di oggetto, è meglio passare un singolo elenco di parametri in modo che sia applicabile a un pubblico più ampio.
  2. Introdurre un nuovo oggetto modello: se l'elenco di parametri diventa grande (più di 3), è meglio introdurre un nuovo oggetto modello appartenente all'API chiamata (modello builder preferito)
  3. Passa riferimento agli oggetti: se il metodo è correlato agli oggetti del dominio, è meglio dal punto di vista della manutenibilità e della leggibilità passare i riferimenti agli oggetti.

0

Da un lato hai un Account e un oggetto Altro. Dall'altro lato, hai la possibilità di inserire un valore in un database, dato l'id di un account e l'id di un Altro. 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, la persona chiamata deve conoscere i metodi di Conto e Altro. Inoltre, non c'è modo di inserire nient'altro in un database se non il valore di un oggetto Altro e non c'è modo di usare questo metodo se si possiede 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 maggiori dettagli di un Account o di Altro oltre al semplice ID da inserire nel database, nel qual caso questa soluzione è totalmente inutile. D'altra parte, si spera che non sia necessaria alcuna conoscenza di Account e Altro nella chiamata e c'è maggiore flessibilità.

Il tuo giudizio: è necessaria maggiore flessibilità? Spesso non si tratta di una chiamata, ma sarebbe coerente con tutto il software: o usi gli ID dell'account per la maggior parte del tempo o usi gli oggetti. Mischiarti ti procura il peggio 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, quindi hai entrambi i modi con zero overhead.

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.