Dovremmo evitare oggetti personalizzati come parametri?

49

Supponiamo di avere un oggetto personalizzato, Studente :

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

E una classe, Window , che è usata per mostrare le informazioni di uno Studente :

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

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

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

in modo che sia più facile testare Window singolarmente:

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

Ma ho trovato che la versione modificata ha altri problemi:

  1. Modifica studente (ad es. nuove proprietà nuove) richiede la modifica della firma del metodo di showInfo

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

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

    
posta ggrr 12.05.2016 - 05:12
fonte

10 risposte

131

L'utilizzo di un oggetto personalizzato per raggruppare i parametri correlati è in realtà uno schema consigliato. Come refactoring, si chiama Introduce oggetto parametro .

Il tuo problema sta altrove. Innanzitutto, generico Window non dovrebbe sapere nulla di Student. Invece, dovresti avere un qualche tipo di StudentWindow che sa solo di visualizzare Students . In secondo luogo, non c'è assolutamente alcun problema nel creare Student instance per testare StudentWindow finché Student non contiene alcuna logica complessa che complicherebbe drasticamente il test di StudentWindow . Se ha questa logica, allora deve essere preferita l'interfaccia di Student e deriderla.

    
risposta data 12.05.2016 - 06:02
fonte
26

Dici che è

not quite easy to test individually, because it needs a real Student object to call the function

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.

    
risposta data 12.05.2016 - 10:12
fonte
22

In parole povere:

  • Quello che tu chiami un "oggetto personalizzato" viene in genere chiamato semplicemente oggetto.
  • Non puoi evitare di passare oggetti come parametri durante la progettazione di un programma o API non banale o di utilizzare 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 librerie che usi dove scritte da semplici mortali come te e me, quindi quelle che scriviamo non sono "personalizzate" , sono solo così.

Modifica:

Poiché @ Tom.Bowen89 afferma che non è molto più complesso testare il metodo showInfo:

showInfo(new Student(8812372,"Peter Parker",16,8.9));
    
risposta data 12.05.2016 - 13:08
fonte
3
  1. Nell'esempio del tuo studente presumo che sia banale chiamare il costruttore Studente per creare uno studente da passare a showInfo. Quindi non ci sono problemi.
  2. Assumendo l'esempio Studente è deliberatamente banalizzato per questa domanda ed è più difficile da costruire, quindi potresti usare un test doppio . Ci sono un certo numero di opzioni per duplicati di test, mock, stub, ecc. Di cui si parla nell'articolo di Martin Fowler tra cui scegliere.
  3. Se vuoi rendere la funzione showInfo più generica, puoi farlo iterare sulle variabili pubbliche, o forse i metodi di accesso pubblici dell'oggetto passato ed eseguire la logica di visualizzazione per tutti loro. Quindi è possibile passare in qualsiasi oggetto conforme a tale contratto e funzionerebbe come previsto. Questo sarebbe un buon posto per usare un'interfaccia. Ad esempio, passa in un oggetto Showable o ShowInfoable alla funzione showInfo che può mostrare non solo le informazioni degli studenti ma qualsiasi informazione dell'oggetto che implementa l'interfaccia (ovviamente quelle interfacce hanno bisogno di nomi migliori a seconda di quanto specifico o generico vuoi l'oggetto che puoi passare essere e ciò che uno studente è una sottoclasse di).
  4. Spesso è più semplice passare le primitive, e a volte è necessario per le prestazioni, ma più è possibile raggruppare concetti simili e più comprensibile sarà il codice. L'unica cosa a cui fare attenzione è cercare di non farlo e finire con enterprise fizzbuzz .
risposta data 12.05.2016 - 11:14
fonte
3

Steve McConnell in Code Complete ha affrontato proprio questo problema, discutendo i vantaggi e gli svantaggi di passare oggetti in metodi invece di usare proprietà.

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

Arriva alla conclusione che stai meglio non usare un oggetto, invece di inviare solo quelle proprietà assolutamente necessarie per il metodo. Il metodo non deve 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 risposto che se si finisce con un metodo che accetta molti argomenti diversi, probabilmente questo è un segnale che il metodo sta facendo troppo e dovrebbe essere scomposto in metodi più piccoli.

Tuttavia, a volte, a volte, in realtà hai bisogno di molti parametri. L'esempio che fornisce sarebbe un metodo che costruisce un indirizzo completo, utilizzando molte diverse proprietà di indirizzo (anche se questo potrebbe essere ottenuto utilizzando un array di stringhe quando ci pensi).

    
risposta data 12.05.2016 - 12:01
fonte
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());

riga potrebbe essere scritta, se si passano i valori separatamente, come:

showAStudentWithAFailingGrade(view);

dove la chiamata al metodo reale è sepolta da qualche parte come

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 puoi mettere la chiamata al metodo nel test è un segno che l'API è sbagliata.

    
risposta data 12.05.2016 - 12:52
fonte
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 minima quantità di informazioni che devo dare a questa funzione per fare lo scopo? (Spezzandolo, potrebbe permetterti di riutilizzare questo codice - fai attenzione a non cadere nel foro di progettazione di questa funzione e poi a strozzare tutto per usare esclusivamente questo oggetto.)

Tutte queste regole di programmazione sono solo delle guide per farti pensare nella giusta direzione. Non costruire una bestia del codice - se non sei sicuro e devi solo procedere, scegli una direzione / la tua o un suggerimento qui, e se colpisci un punto in cui pensi "oh, avrei dovuto farcela modo '- probabilmente puoi tornare indietro e refactoring abbastanza facilmente. (Ad esempio se hai la classe Insegnante - ha solo bisogno della stessa proprietà impostata come Studente, e tu cambi la tua funzione per accettare qualsiasi oggetto del modulo Persona)

Sarei più propenso a mantenere l'oggetto principale che viene passato - perché il mio codice spiega più facilmente cosa sta facendo questa funzione.

    
risposta data 12.05.2016 - 22:16
fonte
1

Un percorso comune è 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) {

    }
}

A volte diventa un po 'complicato, 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 classe Window semplicemente assegnandogli un oggetto HasInfo falso.

Sospetto che questo sia un esempio del pattern Decorator .

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) {

    }
}
    
risposta data 13.05.2016 - 10:39
fonte
1

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

  • L'esempio mostra uno studente (chiaramente un oggetto modello) passato a una finestra (apparentemente un oggetto a livello di vista). Un oggetto controllore o presentatore intermedio può essere utile se non ne hai già uno, permettendoti di isolare l'interfaccia utente dal tuo modello. Il controller / presentatore deve fornire un'interfaccia che può essere utilizzata per sostituirlo per la verifica dell'interfaccia utente e deve utilizzare le interfacce per fare riferimento agli oggetti del modello e visualizzare gli oggetti per poterli isolare da entrambi quelli per il test. Potrebbe essere necessario fornire un modo astratto per creare o caricare questi (ad esempio oggetti Factory, oggetti repository o simili).

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

  • È possibile che il tuo studente vìoli il principio di segregazione dell'interfaccia. Se è così, potrebbe essere utile dividerlo in più interfacce con cui è più facile lavorare.

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

risposta data 14.05.2016 - 09:16
fonte
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, ereditarietà, dipendenze, ecc. Non si vorrebbe veramente usare qualcosa del genere per passare semplicemente alcune proprietà.

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

Tuttavia, nei linguaggi moderni, non c'è davvero alcuna differenza tra una struct 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 a conoscenza - per esempio, una struct è un tipo di valore, non un tipo di riferimento.) Fondamentalmente, finché si mantiene il proprio oggetto semplice, sarà facile testare manualmente. I linguaggi e gli strumenti moderni ti consentono di mitigare un bel po 'questo problema (tramite interfacce, strutture di derisione, iniezione delle dipendenze, ecc.)

    
risposta data 13.05.2016 - 01:29
fonte

Leggi altre domande sui tag