Dovremmo evitare oggetti personalizzati come parametri?


49

Supponiamo che io abbia un oggetto personalizzato, Studente :

public class Student{
    public int _id;
    public String name;
    public int age;
    public float score;
}

E una classe, Finestra , che viene utilizzato per visualizzare le informazioni di uno Studente :

public class Window{
    public void showInfo(Student student);
}

Sembra abbastanza normale, ma ho scoperto che Window non è abbastanza facile da testare individualmente, perché ha bisogno di un vero oggetto Student per chiamare la funzione. Quindi provo a modificare showInfo in modo che non accetti direttamente un oggetto Student :

public void showInfo(int _id, String name, int age, float score);

in modo che sia più semplice testare Window singolarmente:

showInfo(123, "abc", 45, 6.7);

Ma ho scoperto che la versione modificata presenta altri problemi:

  1. Modifica Studente (ad esempio: aggiungi nuove proprietà) richiede la modifica della firma del metodo di showInfo

  2. Se Student avesse molte proprietà, la firma del metodo di Student sarebbe molto lunga.

Quindi, usando oggetti personalizzati come parametro o accettando ogni proprietà negli oggetti come parametro, quale è più gestibile?


40
E il tuo "miglioramento" showInforichiede una vera stringa, un vero float e due veri ints. In che modo fornire un Stringoggetto reale è meglio che fornire un Studentoggetto reale ?
Bart van Ingen Schenau,

28
Un grosso problema con il passaggio diretto dei parametri: ora hai due intparametri. Dal sito di chiamata, non vi è alcuna verifica che tu li stia effettivamente passando nell'ordine giusto. Cosa succede se si scambia ide age, o firstNamee e lastName? Stai introducendo un potenziale punto di errore che può essere molto difficile da rilevare fino a quando non ti esplode in faccia e lo stai aggiungendo in ogni sito di chiamata .
Chris Hayes,

38
@ChrisHayes ah, il vecchio showForm(bool, bool, bool, bool, int)metodo - Adoro quelli ...
Boris the Spider,

3
@ChrisHayes almeno non è JS ...
Jens Schauder,

2
una proprietà sottovalutata dei test: se è difficile creare / utilizzare i propri oggetti nei test, l'API potrebbe usare un po 'di lavoro :)
Eevee,

Risposte:


131

L'uso di un oggetto personalizzato per raggruppare i parametri correlati è in realtà un modello consigliato. Come refactoring, si chiama Introduce Parameter Object .

Il tuo problema sta altrove. In primo luogo, il generico non Windowdovrebbe sapere nulla di Student. Invece, dovresti avere un tipo di StudentWindowciò che sa solo della visualizzazione Students. In secondo luogo, non vi è assolutamente alcun problema nella creazione Studentdell'istanza da testare StudentWindowpurché Studentnon contenga alcuna logica complessa che complicherebbe drasticamente i test StudentWindow. Se ha quella logica, allora Studentè preferibile creare un'interfaccia e deriderla.


14
Vale la pena avvertire che puoi metterti nei guai se il nuovo oggetto non è davvero un raggruppamento logico. Non provare a calzare ogni parametro impostato in un singolo oggetto; decidere caso per caso. L'esempio nella domanda sembra abbastanza chiaramente essere un buon candidato per questo. Il Studentraggruppamento ha senso ed è probabile che cresca in altre aree dell'app.
jpmc26,

Pedanticamente parlando se hai già l'oggetto, ad esempio Student, sarebbe un Preserve Whole Object
abuzittin gillifirca

4
Ricorda anche la Legge di Demetra . C'è un equilibrio da raggiungere, ma il tldr non lo fa a.b.cse il tuo metodo richiede a. Se il tuo metodo arriva al punto in cui devi avere più di 4 parametri o 2 livelli di accesso alla proprietà, probabilmente deve essere preso in considerazione. Si noti inoltre che questa è una linea guida - come tutte le altre linee guida, richiede discrezione dell'utente. Non seguirlo ciecamente.
Dan Pantry,

7
Ho trovato estremamente difficile analizzare la prima frase di questa risposta.
helrich,

5
@Qwerky Non sarei molto d'accordo. Lo studente suona davvero come una foglia nel grafico degli oggetti (escluso altri oggetti banali come forse Nome, DateOfBirth ecc.), Semplicemente un contenitore per lo stato di uno studente. Non vi è alcun motivo per cui uno studente dovrebbe essere difficile da costruire, perché dovrebbe essere un tipo di record. La creazione di duplicati di test per uno studente suona come una ricetta per test difficili da mantenere e / o forte dipendenza da un quadro di isolamento elaborato.
Sara

26

Dici che lo è

non abbastanza facile da testare individualmente, perché ha bisogno di un vero oggetto Student per chiamare la funzione

Ma puoi semplicemente creare un oggetto studente da passare alla tua finestra:

showInfo(new Student(123,"abc",45,6.7));

Non sembra molto più complesso da chiamare.


7
Il problema si presenta quando si Studentriferisce a a University, che si riferisce a molti Facultys e Campuss, con Professors e Buildings, nessuno dei quali showInfoeffettivamente utilizza, ma non è stata definita alcuna interfaccia che consenta ai test di "conoscerlo" e di fornire solo lo studente rilevante dati, senza costruire l'intera organizzazione. L'esempio Studentè un semplice oggetto dati e, come dici tu, i test dovrebbero essere felici di lavorarci.
Steve Jessop,

4
Il problema si presenta quando Student si riferisce a un'università, che fa riferimento a molte Facoltà e Campuss, con professori ed edifici, senza riposo per i malvagi.
abuzittin gillifirca,

1
@abuzittingillifirca, "Object Mother" è una soluzione, anche il tuo oggetto studente potrebbe essere troppo complesso. Potrebbe essere meglio avere solo UniversityId e un servizio (usando l'iniezione delle dipendenze) che fornirà un oggetto University da un UniversityId.
Ian,

12
Se Student è molto complesso o è difficile inizializzarlo, prendilo in giro. I test sono molto più potenti con framework come Mockito o altri linguaggi equivalenti.
Borjab,

4
Se showInfo non ha importanza per l'Università, è sufficiente impostarlo su null. I Null sono orribili in produzione e sono una prova mittente. Essere in grado di specificare un parametro come null in un test comunica l'intento e dice che "questa cosa non è necessaria qui". Anche se prenderei in considerazione anche la creazione di una sorta di vista modelfor studenti contenente solo i dati rilevanti, considerando che showInfo sembra un metodo in una classe UI.
Sara

22

In parole povere:

  • Quello che chiami un "oggetto personalizzato" di solito è semplicemente chiamato un oggetto.
  • Non è possibile evitare il passaggio di oggetti come parametri durante la progettazione di programmi o API non banali o l'utilizzo di API o librerie non banali.
  • È perfettamente OK passare oggetti come parametri. Dai un'occhiata all'API Java e vedrai molte interfacce che ricevono oggetti come parametri.
  • Le classi nelle biblioteche che usi dove sono state scritte da semplici mortali come te e me, quindi quelle che scriviamo non sono "personalizzate" , lo sono e basta.

Modificare:

Come afferma @ Tom.Bowen89 , non è molto più complesso testare il metodo showInfo:

showInfo(new Student(8812372,"Peter Parker",16,8.9));

3
  1. Nel tuo esempio di studente suppongo che sia banale chiamare il costruttore Studente per creare uno studente da trasmettere a showInfo. Quindi non ci sono problemi.
  2. Supponendo che l'esempio Studente sia deliberatamente banalizzato per questa domanda ed è più difficile da costruire, allora potresti usare un doppio di prova . Ci sono una serie di opzioni per doppi di prova, beffe, tronconi ecc. Di cui si parla nell'articolo di Martin Fowler tra cui scegliere.
  3. Se si desidera rendere la funzione showInfo più generica, è possibile farla scorrere sulle variabili pubbliche, o forse gli accessi pubblici dell'oggetto passati ed eseguire la logica dello spettacolo per tutti. Quindi potresti passare qualsiasi oggetto conforme a quel contratto e funzionerebbe come previsto. Questo sarebbe un buon posto per usare un'interfaccia. Ad esempio, passare un oggetto Showable o ShowInfoable alla funzione showInfo in grado di mostrare non solo le informazioni sugli studenti, ma anche le informazioni di qualsiasi oggetto che implementano l'interfaccia (ovviamente quelle interfacce hanno bisogno di nomi migliori a seconda di come si desidera passare l'oggetto specifico o generico essere e ciò che uno studente è una sottoclasse di).
  4. Spesso è più semplice passare in rassegna le primitive, e talvolta è necessario per le prestazioni, ma più puoi raggruppare concetti simili insieme più sarà comprensibile il tuo codice. L'unica cosa a cui fare attenzione è cercare di non esagerare e finire con il fizzbuzz aziendale .

3

Steve McConnell in Code Complete ha affrontato proprio questo problema, discutendo i vantaggi e gli svantaggi del passaggio di oggetti nei metodi invece di utilizzare le proprietà.

Perdonami se sbaglio alcuni dei dettagli, sto lavorando a memoria poiché è passato più di un anno da quando ho avuto accesso al libro:

Arriva alla conclusione che è meglio non usare un oggetto, invece di inviare solo quelle proprietà assolutamente necessarie per il metodo. Il metodo non dovrebbe sapere nulla sull'oggetto al di fuori delle proprietà che utilizzerà come parte delle sue operazioni. Inoltre, nel tempo, se l'oggetto viene mai modificato, ciò potrebbe avere conseguenze indesiderate sul metodo che utilizza l'oggetto.

Inoltre ha affrontato il fatto che se si finisce con un metodo che accetta molti argomenti diversi, probabilmente questo è un segno che il metodo sta facendo troppo e dovrebbe essere suddiviso in metodi più piccoli.

Tuttavia, a volte, a volte, in realtà hai bisogno di molti parametri. L'esempio che fornisce sarebbe di un metodo che costruisce un indirizzo completo, usando molte proprietà dell'indirizzo diverse (sebbene ciò possa essere ottenuto usando una matrice di stringhe quando ci pensate).


7
Ho il codice completo 2. C'è un'intera pagina dedicata a questo problema. La conclusione è che i parametri dovrebbero essere al livello di astrazione corretto. A volte ciò richiede il passaggio di un intero oggetto, a volte solo singoli attributi.
VIENI DAL

UV. Il codice di riferimento completo è doppio più buono. Una buona considerazione del design rispetto alla convenienza dei test. Preferisco il design, penso che McConnell direbbe nel nostro contesto. Quindi una conclusione eccellente sarebbe "integrare l'oggetto parametro nella progettazione" ( Studentin questo caso). Ed è così che i test informano il design , abbracciando completamente la risposta più votata mantenendo l'integrità del design.
radarbob,

2

È molto più facile scrivere e leggere i test se si passa l'intero oggetto:

public class AStudentView {
    @Test 
    public void displays_failing_grade_warning_when_a_student_with_a_failing_grade_is_shown() {
        StudentView view = aStudentView();
        view.show(aStudent().withAFailingGrade().build());
        Assert.that(view, displaysFailingGradeWarning());
    }

    private Matcher<StudentView> displaysFailingGradeWarning() {
        ...
    }
}

Per confronto,

view.show(aStudent().withAFailingGrade().build());

la riga potrebbe essere scritta, se passi valori separatamente, come:

showAStudentWithAFailingGrade(view);

dove la vera chiamata del metodo è sepolta da qualche parte

private showAStudentWithAFailingGrade(StudentView view) {
    int someId = .....
    String someName = .....
    int someAge = .....
    // why have been I peeking and poking values I don't care about
    decimal aFailingGrade = .....
    view.show(someId, someName, someAge, aFailingGrade);
}

Per essere al punto, che non è possibile mettere la prova del metodo effettivo nel test è un segno che l'API è errata.


1

Dovresti passare ciò che ha senso, alcune idee:

Più facile da testare. Se gli oggetti devono essere modificati, cosa richiede il minimo refactoring? Riutilizzare questa funzione per altri scopi è utile? Qual è la quantità minima di informazioni di cui ho bisogno per dare questa funzione per fare il suo scopo? (Rompendolo - può farti riutilizzare questo codice - diffidare di cadere nel buco di progettazione di fare questa funzione e quindi di strozzare tutto per usare esclusivamente questo oggetto.)

Tutte queste regole di programmazione sono solo guide per farti pensare nella giusta direzione. Basta non costruire un codice bestia - se non sei sicuro e devi solo procedere, scegli una direzione / il tuo o un suggerimento qui, e se colpisci un punto in cui pensi 'oh, avrei dovuto farlo modo "- probabilmente puoi quindi tornare indietro e rifattorarlo abbastanza facilmente. (Ad esempio, se si dispone di una classe Insegnante, richiede solo la stessa proprietà impostata come Studente e si cambia la funzione per accettare qualsiasi oggetto del modulo Persona)

Sarei più propenso a mantenere l'oggetto principale passato, perché come codifico spiegherà più facilmente cosa sta facendo questa funzione.


1

Un percorso comune attorno a questo è quello di inserire un'interfaccia tra i due processi.

public class Student {

    public int id;
    public String name;
    public int age;
    public float score;
}

interface HasInfo {
    public String getInfo();
}

public class StudentInfo implements HasInfo {
    final Student student;

    public StudentInfo(Student student) {
        this.student = student;
    }

    @Override
    public String getInfo() {
        return student.name;
    }

}

public class Window {

    public void showInfo(HasInfo info) {

    }
}

Questo a volte diventa un po 'confuso ma le cose diventano un po' più ordinate in Java se usi una classe interna.

interface HasInfo {
    public String getInfo();
}

public class Student {

    public int id;
    public String name;
    public int age;
    public float score;

    public HasInfo getInfo() {
        return new HasInfo () {
            @Override
            public String getInfo() {
                return name;
            }

        };
    }
}

Puoi quindi testare la Windowclasse semplicemente assegnandole un HasInfooggetto falso .

Sospetto che questo sia un esempio del motivo decorativo .

aggiunto

Sembra esserci un po 'di confusione causata dalla semplicità del codice. Ecco un altro esempio che può dimostrare meglio la tecnica.

interface Drawable {

    public void Draw(Pane pane);
}

/**
 * Student knows nothing about Window or Drawable.
 */
public class Student {

    public int id;
    public String name;
    public int age;
    public float score;
}

/**
 * DrawsStudents knows about both Students and Drawable (but not Window)
 */
public class DrawsStudents implements Drawable {

    private final Student subject;

    public DrawsStudents(Student subject) {
        this.subject = subject;
    }

    @Override
    public void Draw(Pane pane) {
        // Draw a Student on a Pane
    }

}

/**
 * Window only knows about Drawables.
 */
public class Window {

    public void showInfo(Drawable info) {

    }
}

Se showInfo voleva solo mostrare il nome dello studente, perché non passare semplicemente il nome? racchiudere un campo denominato semanticamente significativo in un'interfaccia astratta contenente una stringa senza alcun indizio su ciò che la stringa rappresenta sembra un enorme downgrade, sia in termini di manutenibilità che comprensibilità.
Sara

@kai - L'uso di Studente Stringqui per il tipo restituito è puramente dimostrativo. Probabilmente ci sarebbero parametri aggiuntivi getInfocome quelli Panea cui attingere se si disegna. Il concetto qui è passare componenti funzionali come decoratori dell'oggetto originale .
OldCurmudgeon,

in quel caso dovresti accoppiare strettamente l'entità studentesca al tuo framework UI, suona anche peggio ...
Sara

1
@kai - Al contrario. La mia interfaccia utente conosce solo HasInfooggetti. Studentsa essere uno.
OldCurmudgeon,

Se dai il getInfovuoto di ritorno, passa un punto Panea cui attingere, quindi l'implementazione (nella Studentclasse) viene improvvisamente accoppiata allo swing o qualunque cosa tu stia usando. Se lo fai restituire una stringa e prendere 0 parametri, la tua UI non saprà cosa fare con la stringa senza ipotesi magiche e accoppiamento implicito. Se si effettua getInfoeffettivamente la restituzione di alcuni modelli di vista con proprietà pertinenti, la Studentclasse viene nuovamente accoppiata alla logica di presentazione. Non credo che nessuna di queste alternative sia desiderabile
Sara

1

Hai già molte risposte valide, ma qui ci sono alcuni suggerimenti che potrebbero permetterti di vedere una soluzione alternativa:

  • Il tuo esempio mostra uno Studente (chiaramente un oggetto modello) che viene passato a una Finestra (apparentemente un oggetto a livello di vista). Un oggetto Controller o Presentatore intermedio può essere utile se non ne hai già uno, consentendoti di isolare l'interfaccia utente dal tuo modello. Il controller / presentatore deve fornire un'interfaccia che può essere utilizzata per sostituirlo per i test dell'interfaccia utente e deve utilizzare le interfacce per fare riferimento agli oggetti del modello e visualizzare gli oggetti per essere in grado di isolarli da entrambi per il test. Potrebbe essere necessario fornire un modo astratto per crearli o caricarli (ad esempio oggetti Factory, oggetti Repository o simili).

  • Il trasferimento di parti pertinenti degli oggetti modello in un oggetto trasferimento dati è un approccio utile per l'interfacciamento quando il modello diventa troppo complesso.

  • È possibile che il tuo studente violi il principio di segregazione dell'interfaccia. In tal caso, potrebbe essere utile suddividerlo in più interfacce con cui è più facile lavorare.

  • Il caricamento lento può semplificare la costruzione di grafici di oggetti di grandi dimensioni.


0

Questa è in realtà una domanda decente. Il vero problema qui è l'uso del termine generico "oggetto", che può essere un po 'ambiguo.

Generalmente, in un linguaggio OOP classico, il termine "oggetto" è diventato "istanza di classe". Le istanze di classe possono essere piuttosto pesanti: proprietà pubbliche e private (e quelle intermedie), metodi, eredità, dipendenze, ecc. Non vorrai davvero usare qualcosa del genere per passare semplicemente in alcune proprietà.

In questo caso, stai usando un oggetto come contenitore che contiene semplicemente alcune primitive. In C ++, oggetti come questi erano noti come structs(ed esistono ancora in linguaggi come C #). Le strutture erano, infatti, progettate esattamente per l'uso di cui parli: raggruppavano oggetti e primitivi correlati quando avevano una relazione logica.

Tuttavia, nei linguaggi moderni, non c'è davvero alcuna differenza tra una struttura e una classe quando scrivi il codice , quindi stai bene usando un oggetto. (Dietro le quinte, tuttavia, ci sono alcune differenze di cui dovresti essere consapevole - per esempio, una struttura è un tipo di valore, non un tipo di riferimento.) Fondamentalmente, finché manterrai il tuo oggetto semplice, sarà facile per testare manualmente. I linguaggi e gli strumenti moderni ti consentono di mitigarlo un po ', però (tramite interfacce, framework di simulazione, iniezione di dipendenza, ecc.)


1
Passare un riferimento non è costoso, anche se l'oggetto è grande un miliardo di terabyte, perché il riferimento è ancora solo la dimensione di un int nella maggior parte delle lingue. Dovresti preoccuparti di più se il metodo di ricezione è esposto a un'api troppo grande e se si accoppiano le cose in modo indesiderato. Prenderei in considerazione la creazione di un livello di mappatura che traduce gli oggetti business ( Student) in modelli di visualizzazione ( StudentInfoo StudentInfoViewModelecc.), Ma potrebbe non essere necessario.
Sara

Classi e strutture sono molto diverse. Uno viene passato per valore (il che significa che il metodo che lo riceve ottiene una copia ) e l'altro viene passato per riferimento (il destinatario ottiene solo un puntatore all'originale). Non capire questa differenza è pericoloso.
RubberDuck,

@kai Capisco che passare un riferimento non è costoso. Quello che sto dicendo è che la creazione di una funzione che richiede un'istanza di classe completa potrebbe essere più difficile da testare a seconda delle dipendenze di quella classe, dei suoi metodi, ecc., Poiché in qualche modo dovresti deridere quella classe.
lunchmeat317,

Personalmente sono contrario a deridere qualsiasi cosa tranne le classi di confine che accedono a sistemi esterni (file / IO di rete) o non deterministici (ad esempio casuale, basato sul tempo di sistema, ecc.). Se una classe che sto testando ha una dipendenza che non è rilevante per la funzionalità corrente in fase di test, preferisco passare null solo dove possibile. Ma se stai testando un metodo che accetta un oggetto parametro, se quell'oggetto ha un sacco di dipendenze sarei preoccupato per il design complessivo. Tali oggetti dovrebbero essere leggeri.
Sara,
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.