Sollten wir benutzerdefinierte Objekte als Parameter vermeiden?

49

Angenommen, ich habe ein benutzerdefiniertes Objekt, Student :

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

Und eine Klasse, Window , mit der Informationen eines Schülers angezeigt werden :

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

Es sieht ganz normal aus, aber ich fand, dass Window nicht ganz einfach einzeln zu testen ist, da es ein echtes Student- Objekt benötigt, um die Funktion aufzurufen. Deshalb versuche ich, showInfo so zu ändern, dass es ein Student- Objekt nicht direkt akzeptiert :

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

Damit es einfacher ist, Window einzeln zu testen :

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

Aber ich fand die geänderte Version hat noch andere Probleme:

  1. Modify Student (z. B. Hinzufügen neuer Eigenschaften) erfordert das Ändern der Methodensignatur von showInfo

  2. Wenn Student viele Eigenschaften hätte, wäre die Methodensignatur von Student sehr lang.

Wenn Sie also benutzerdefinierte Objekte als Parameter verwenden oder jede Eigenschaft in Objekten als Parameter akzeptieren, welche ist besser zu warten?

ggrr
quelle
40
Und deine 'Verbesserung' showInfoerfordert einen echten String, einen echten Float und zwei echte Ints. Wie ist es Stringbesser , ein reales Objekt bereitzustellen als ein reales StudentObjekt?
Bart van Ingen Schenau
28
Ein großes Problem bei der direkten Übergabe der Parameter: Sie haben jetzt zwei intParameter. Auf der Anrufseite gibt es keine Bestätigung, dass Sie sie tatsächlich in der richtigen Reihenfolge übergeben. Was ist, wenn Sie idund age, oder firstNameund tauschen lastName? Sie führen eine potenzielle Fehlerquelle ein, die sehr schwer zu erkennen ist, bis sie in Ihrem Gesicht explodiert, und Sie fügen sie an jedem Anrufstandort hinzu .
Chris Hayes
38
@ ChrisHayes ah, die alte showForm(bool, bool, bool, bool, int)Methode - ich liebe diese ...
Boris die Spinne
3
@ ChrisHayes zumindest ist es nicht JS ...
Jens Schauder
2
Eine unterschätzte Eigenschaft von Tests: Wenn es schwierig ist, eigene Objekte in Tests zu erstellen / zu verwenden , könnte Ihre API möglicherweise etwas Arbeit
vertragen

Antworten:

131

Die Verwendung eines benutzerdefinierten Objekts zum Gruppieren verwandter Parameter ist eigentlich ein empfohlenes Muster. Als Refactoring wird es als Introduce Parameter Object bezeichnet .

Dein Problem liegt woanders. Erstens sollte generic Windownichts über Student wissen. Stattdessen sollten Sie eine Art von StudentWindowWissen haben , das nur das Anzeigen betrifft Students. Zweitens ist es absolut kein Problem, eine Studentzu testende Instanz zu erstellen , StudentWindowsolange Studentsie keine komplexe Logik enthält, die das Testen von drastisch erschwert StudentWindow. Wenn es diese Logik hat, Studentsollte es bevorzugt werden , eine Schnittstelle zu erstellen und sie zu verspotten.

Euphorisch
quelle
14
Es lohnt sich zu warnen, dass Sie Probleme bekommen können, wenn das neue Objekt keine logische Gruppierung ist. Versuchen Sie nicht, jeden Parametersatz in ein einzelnes Objekt zu zerlegen. entscheiden Sie von Fall zu Fall. Das Beispiel in der Frage scheint ziemlich eindeutig ein guter Kandidat dafür zu sein. Die StudentGruppierung ist sinnvoll und wird wahrscheinlich in anderen Bereichen der App auftreten.
jpmc26
Wenn Sie das Objekt bereits haben Student, handelt es sich um ein Objekt, das
vollständig erhalten bleibt
4
Denken Sie auch an das Gesetz von Demeter . Es gibt ein Gleichgewicht zu finden, aber das tldr wird nicht ausgeführt, a.b.cwenn Ihre Methode es erfordert a. Wenn Ihre Methode an den Punkt gelangt, an dem Sie ungefähr mehr als 4 Parameter oder 2 Ebenen des Eigentumserwerbs benötigen, muss sie wahrscheinlich herausgerechnet werden. Beachten Sie auch, dass dies eine Richtlinie ist - wie alle anderen Richtlinien auch, erfordert es die Diskretion des Benutzers. Folge ihm nicht blindlings.
Dan Pantry
7
Ich fand den ersten Satz dieser Antwort außerordentlich schwierig zu analysieren.
Helrich
5
@ Qwerky würde ich sehr stark widersprechen. Student klingt WIRKLICH wie ein Blatt im Objektdiagramm (abgesehen von anderen trivialen Objekten wie Name, DateOfBirth usw.), nur ein Container für den Status eines Studenten. Es gibt keinen Grund, warum ein Student schwer zu konstruieren sein sollte, da es sich um einen Datensatztyp handeln sollte. Das Erstellen von Test-Doubles für einen Schüler klingt nach einem Rezept für schwer zu wartende Tests und / oder nach einer starken Abhängigkeit von einem ausgefallenen Isolations-Framework.
Sara
26

Du sagst es ist

nicht ganz einfach einzeln zu testen, da es ein echtes Student-Objekt braucht, um die Funktion aufzurufen

Sie können jedoch auch ein Schülerobjekt erstellen, das an Ihr Fenster übergeben wird:

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

Es scheint nicht viel komplexer zu sein.

Tom.Bowen89
quelle
7
Das Problem tritt auf, wenn Studentauf a Bezug genommen wird University, das auf viele Facultys und Campuss verweist , wobei Professors und Buildings keines von ihnen showInfotatsächlich verwendet, Sie jedoch keine Schnittstelle definiert haben, mit der die Tests dies "wissen" und nur den relevanten Schüler beliefern können Daten, ohne die gesamte Organisation aufzubauen. Das Beispiel Studentist ein einfaches Datenobjekt, und wie Sie sagen, sollten Tests gerne damit arbeiten.
Steve Jessop
4
Das Problem entsteht, wenn Student sich auf eine Universität bezieht, die sich auf viele Fakultäten und Campuss bezieht, mit Professoren und Gebäuden, keine Ruhe für die Bösen.
abuzittin gillifirca
1
@abuzittingillifirca, "Objektmutter" ist eine Lösung, auch Ihr Schülerobjekt ist möglicherweise zu komplex. Es ist möglicherweise besser, nur eine Universitäts-ID und einen Dienst (mit Abhängigkeitsinjektion) zu haben, der ein Universitätsobjekt von einer Universitäts-ID liefert.
Ian
12
Wenn Student sehr komplex oder schwer zu initialisieren ist, verspotten Sie es einfach. Testen ist mit Frameworks wie Mockito oder anderen Sprachäquivalenten viel leistungsfähiger.
Borjab
4
Wenn showInfo die Universität nicht interessiert, setzen Sie sie einfach auf null. Nullen sind schrecklich in der Produktion und ein Gott-Send in Tests. In der Lage zu sein, einen Parameter als null in einem Test anzugeben, teilt die Absicht mit und sagt, dass "dieses Ding hier nicht benötigt wird". Obwohl ich auch erwägen würde, eine Art Ansichtsmodell für Schüler zu erstellen, die nur die relevanten Daten enthalten, klingt showInfo wie eine Methode in einer UI-Klasse.
Sara
22

In den Begriffen des Laien:

  • Was Sie als "benutzerdefiniertes Objekt" bezeichnen, wird normalerweise einfach als Objekt bezeichnet.
  • Sie können es nicht vermeiden, Objekte als Parameter zu übergeben, wenn Sie ein nicht triviales Programm oder API entwerfen oder eine nicht triviale API oder Bibliothek verwenden.
  • Es ist vollkommen in Ordnung, Objekte als Parameter zu übergeben. Schauen Sie sich die Java-API an und Sie werden viele Schnittstellen sehen, die Objekte als Parameter empfangen.
  • Die Klassen in den Bibliotheken, die Sie benutzen, wurden von bloßen Sterblichen wie Ihnen und mir geschrieben. Diejenigen, die wir schreiben, sind also nicht "benutzerdefiniert" , sondern nur.

Bearbeiten:

Wie @ Tom.Bowen89 feststellt , ist es nicht viel komplexer, die showInfo-Methode zu testen:

showInfo(new Student(8812372,"Peter Parker",16,8.9));
Tulains Córdova
quelle
3
  1. In Ihrem Studentenbeispiel ist es wahrscheinlich trivial, den Student-Konstruktor aufzurufen, um einen Studenten zu erstellen, der an showInfo übergeben wird. Es gibt also kein Problem.
  2. Angenommen, der Student ist absichtlich für diese Frage banalisiert und es ist schwieriger zu konstruieren, dann könnten Sie ein Test- Double verwenden . Es gibt eine Reihe von Optionen für Test-Doubles, Mocks, Stubs usw., die in Martin Fowlers Artikel zur Auswahl stehen.
  3. Wenn Sie die showInfo-Funktion allgemeiner gestalten möchten, können Sie sie über die öffentlichen Variablen oder möglicherweise über die öffentlichen Zugriffsmethoden des übergebenen Objekts iterieren lassen und die show-Logik für alle ausführen. Dann könnten Sie jedes Objekt übergeben, das diesem Vertrag entspricht, und es würde wie erwartet funktionieren. Dies wäre ein guter Ort, um eine Schnittstelle zu verwenden. Übergeben Sie beispielsweise ein Showable- oder ShowInfoable-Objekt an die showInfo-Funktion, die nicht nur die Schülerinformationen, sondern auch die Informationen aller Objekte, die die Schnittstelle implementieren, anzeigen kann (offensichtlich benötigen diese Schnittstellen bessere Namen, je nachdem, wie spezifisch oder generisch das Objekt sein soll, an das Sie übergeben können sein und was ein Student ist eine Unterklasse von).
  4. Es ist oft einfacher, Primitive weiterzugeben, und manchmal ist dies für die Leistung erforderlich. Je mehr Sie jedoch ähnliche Konzepte gruppieren können, desto verständlicher wird Ihr Code im Allgemeinen. Das Einzige, worauf Sie achten müssen , ist zu versuchen, es nicht zu übertreiben und am Ende ein Problem mit dem Unternehmen zu haben .
Encaitar
quelle
3

Steve McConnell ging in Code Complete genau auf dieses Problem ein und diskutierte die Vor- und Nachteile der Übergabe von Objekten an Methoden anstelle der Verwendung von Eigenschaften.

Verzeihen Sie mir, wenn ich einige Details falsch sehe. Ich arbeite aus dem Gedächtnis, da ich seit über einem Jahr keinen Zugriff mehr auf das Buch hatte:

Er kommt zu dem Schluss, dass Sie besser dran sind, kein Objekt zu verwenden, sondern nur die Eigenschaften zu senden, die für die Methode unbedingt erforderlich sind. Die Methode sollte außerhalb der Eigenschaften, die sie als Teil ihrer Operationen verwendet, nichts über das Objekt wissen müssen. Sollte sich das Objekt im Laufe der Zeit ändern, kann dies unbeabsichtigte Folgen für die Methode haben, die das Objekt verwendet.

Er sprach auch an, dass, wenn Sie eine Methode haben, die viele verschiedene Argumente akzeptiert, dies wahrscheinlich ein Zeichen dafür ist, dass die Methode zu viel tut und in mehrere, kleinere Methoden unterteilt werden sollte.

Manchmal, manchmal brauchen Sie jedoch tatsächlich viele Parameter. Das Beispiel, das er gibt, wäre eine Methode, die eine vollständige Adresse unter Verwendung vieler verschiedener Adresseigenschaften erstellt (obwohl dies durch die Verwendung eines String-Arrays umgangen werden könnte, wenn Sie darüber nachdenken).

user1666620
quelle
7
Ich habe den Code vervollständigt 2. Es gibt eine ganze Seite darin, die diesem Problem gewidmet ist. Die Schlussfolgerung ist, dass sich die Parameter auf der richtigen Abstraktionsebene befinden sollten. Manchmal muss dazu ein ganzes Objekt übergeben werden, manchmal nur einzelne Attribute.
KOMMEN SIE VOM
UV. Der Verweis auf Code Complete ist doppelt gut. Eine nette Überlegung des Designs über die Zweckmäßigkeit des Testens. Bevorzugen Sie Design, würde McConnell in unserem Kontext sagen. Ein hervorragendes Fazit wäre also "das Parameterobjekt in das Design integrieren" ( Studentin diesem Fall von). Auf diese Weise informiert das Testen das Design und berücksichtigt die Antworten mit den meisten Stimmen bei gleichzeitiger Wahrung der Designintegrität.
Radarbob
2

Es ist viel einfacher, Tests zu schreiben und zu lesen, wenn Sie das gesamte Objekt bestehen:

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() {
        ...
    }
}

Zum Vergleich,

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

Zeile könnte geschrieben werden, wenn Sie Werte separat übergeben, als:

showAStudentWithAFailingGrade(view);

wo der eigentliche Methodenaufruf irgendwo wie begraben ist

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);
}

Um auf den Punkt zu kommen, dass Sie den tatsächlichen Methodenaufruf nicht in den Test stellen können, ist dies ein Zeichen dafür, dass Ihre API fehlerhaft ist.

abuzittin gillifirca
quelle
1

Sie sollten, was Sinn macht, einige Ideen weitergeben:

Einfacher zu testen. Was erfordert das geringste Refactoring, wenn die Objekte bearbeitet werden müssen? Ist die Wiederverwendung dieser Funktion für andere Zwecke nützlich? Was ist die geringste Menge an Informationen, die ich benötige, um dieser Funktion ihren Zweck zu geben? (Wenn Sie den Code auflösen, können Sie ihn möglicherweise wiederverwenden. Seien Sie also vorsichtig, wenn Sie die Entwurfslücke für die Ausführung dieser Funktion schließen und dann den Flaschenhals für die ausschließliche Verwendung dieses Objekts ausfüllen.)

Alle diese Programmierregeln sind nur Richtlinien, die Sie zum richtigen Denken anregen. Bauen Sie einfach kein Code-Biest - wenn Sie sich nicht sicher sind und nur fortfahren müssen, wählen Sie eine Richtung / Ihre eigene oder einen Vorschlag hier aus, und wenn Sie einen Punkt erreicht haben, an dem Sie denken: "Oh, ich hätte das schaffen sollen." way '- Sie können es dann wahrscheinlich ganz einfach umgestalten. (Wenn Sie beispielsweise eine Lehrerklasse haben, muss nur die gleiche Eigenschaft wie für Schüler festgelegt werden, und Sie ändern Ihre Funktion, um alle Objekte des Personenformulars zu akzeptieren.)

Am ehesten würde ich das übergebene Hauptobjekt beibehalten, da durch die Codierung leichter erklärt werden kann, was diese Funktion bewirkt.

Lilly
quelle
1

Eine gängige Vorgehensweise ist das Einfügen einer Schnittstelle zwischen den beiden Prozessen.

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

    }
}

Dies wird manchmal etwas chaotisch, aber in Java wird es ein wenig aufgeräumter, wenn Sie eine innere Klasse verwenden.

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;
            }

        };
    }
}

Sie können die WindowKlasse dann testen, indem Sie ihr ein falsches HasInfoObjekt geben.

Ich vermute, dies ist ein Beispiel für das Decorator-Pattern .

Hinzugefügt

Die Einfachheit des Codes scheint Verwirrung zu stiften. Hier ist ein weiteres Beispiel, das die Technik besser demonstrieren könnte.

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

    }
}
OldCurmudgeon
quelle
Wenn showInfo nur den Namen des Schülers anzeigen wollte, warum nicht einfach den Namen übergeben? Das Einschließen eines semantisch bedeutungsvollen benannten Felds in eine abstrakte Schnittstelle, die eine Zeichenfolge enthält, die keine Ahnung davon hat, was die Zeichenfolge darstellt, fühlt sich in Bezug auf Wartbarkeit und Verständlichkeit wie eine GROSSE Herabstufung an.
Sara
@kai - Die Verwendung von Studentund Stringhier für den Rückgabetyp dient nur zur Demonstration. Es würde wahrscheinlich zusätzliche Parameter geben, auf getInfodie Panegezeichnet werden kann, wenn gezeichnet wird. Hier geht es darum , funktionale Komponenten als Dekoratoren des ursprünglichen Objekts zu übergeben .
OldCurmudgeon
In diesem Fall würden Sie die studentische Entität eng an Ihr UI-Framework koppeln, was noch schlimmer klingt ...
sara
1
@kai - Ganz im Gegenteil. Meine Benutzeroberfläche kennt nur HasInfoObjekte. Studentweiß, wie man eins ist.
OldCurmudgeon
Wenn Sie getInforeturn void pass Panezum Zeichnen geben, wird die Implementierung (in der StudentKlasse) plötzlich an swing oder was auch immer gekoppelt, das Sie verwenden. Wenn Sie dafür sorgen, dass eine Zeichenfolge zurückgegeben wird und 0-Parameter verwendet werden, weiß Ihre Benutzeroberfläche nicht, was mit der Zeichenfolge ohne magische Annahmen und implizite Kopplung zu tun ist. Wenn Sie getInfotatsächlich ein Ansichtsmodell mit relevanten Eigenschaften zurückgeben, ist Ihre StudentKlasse wieder an die Präsentationslogik gekoppelt. Ich halte keine dieser Alternativen für wünschenswert
sara
1

Sie haben bereits viele gute Antworten, aber hier sind noch ein paar Vorschläge, mit denen Sie eine alternative Lösung finden können:

  • In Ihrem Beispiel wird ein Student (eindeutig ein Modellobjekt) an ein Window (anscheinend ein Objekt auf Ansichtsebene) übergeben. Ein zwischengeschaltetes Controller- oder Presenter-Objekt kann hilfreich sein, wenn Sie noch keines haben, sodass Sie Ihre Benutzeroberfläche von Ihrem Modell isolieren können. Der Controller / Presenter sollte eine Schnittstelle bereitstellen, die zum Ersetzen für das Testen der Benutzeroberfläche verwendet werden kann, und Schnittstellen verwenden, um auf Modellobjekte zu verweisen und Objekte anzuzeigen, um sie von beiden zu Testzwecken isolieren zu können. Möglicherweise müssen Sie eine abstrakte Methode zum Erstellen oder Laden dieser Objekte angeben (z. B. Factory-Objekte, Repository-Objekte oder ähnliches).

  • Das Übertragen relevanter Teile Ihrer Modellobjekte in ein Datenübertragungsobjekt ist ein nützlicher Ansatz für die Schnittstelle, wenn Ihr Modell zu komplex wird.

  • Möglicherweise verstößt Ihr Schüler gegen das Prinzip der Schnittstellentrennung. In diesem Fall kann es hilfreich sein, die Oberfläche in mehrere Benutzeroberflächen zu unterteilen, mit denen einfacher zu arbeiten ist.

  • Lazy Loading kann das Erstellen großer Objektdiagramme vereinfachen.

Jules
quelle
0

Das ist eigentlich eine anständige Frage. Das eigentliche Problem hierbei ist die Verwendung des Oberbegriffs "Objekt", der etwas mehrdeutig sein kann.

Im Allgemeinen bedeutet der Begriff "Objekt" in einer klassischen OOP-Sprache "Klasseninstanz". Klasseninstanzen können ziemlich umfangreich sein - öffentliche und private Eigenschaften (und solche dazwischen), Methoden, Vererbung, Abhängigkeiten usw. Sie möchten so etwas nicht wirklich verwenden, um einfach einige Eigenschaften zu übergeben.

In diesem Fall verwenden Sie ein Objekt als Container, der lediglich einige Grundelemente enthält. In C ++ wurden Objekte wie diese als structs(und sie existieren immer noch in Sprachen wie C #) bezeichnet. Strukturen wurden in der Tat genau für die Verwendung entworfen, von der Sie sprechen - sie gruppierten verwandte Objekte und Grundelemente, wenn sie eine logische Beziehung hatten.

In modernen Sprachen gibt es jedoch keinen Unterschied zwischen einer Struktur und einer Klasse, wenn Sie den Code schreiben. Sie können also ein Objekt verwenden. (Hinter den Kulissen gibt es jedoch einige Unterschiede, die Sie beachten sollten - beispielsweise ist eine Struktur ein Werttyp und kein Referenztyp.) Grundsätzlich ist es einfach, solange Sie Ihr Objekt einfach halten manuell testen. Moderne Sprachen und Tools ermöglichen es Ihnen jedoch, dies erheblich zu verringern (über Schnittstellen, spöttische Frameworks, Abhängigkeitsinjektion usw.).

lunchmeat317
quelle
1
Das Übergeben einer Referenz ist nicht teuer, auch wenn das Objekt eine Milliarde Terabyte groß ist, da die Referenz in den meisten Sprachen immer noch nur die Größe eines Int hat. Sie sollten sich mehr Gedanken darüber machen, ob die Empfangsmethode einer zu großen API ausgesetzt ist und ob Sie Dinge auf unerwünschte Weise koppeln. Ich würde in Betracht ziehen, einen Mapping-Layer zu erstellen , der Geschäftsobjekte ( Student) in Ansichtsmodelle ( StudentInfooder StudentInfoViewModelähnliches) übersetzt, dies ist jedoch möglicherweise nicht erforderlich.
Sara
Klassen und Strukturen sind sehr unterschiedlich. Einer wird als Wert übergeben (dh die Methode, die eine Kopie erhält ) und der andere wird als Referenz übergeben (der Empfänger erhält nur einen Zeiger auf das Original). Diesen Unterschied nicht zu verstehen ist gefährlich.
RubberDuck
@kai Ich verstehe, dass das Übergeben einer Referenz nicht teuer ist. Ich sage, dass das Erstellen einer Funktion, für die eine vollständige Klasseninstanz erforderlich ist, je nach den Abhängigkeiten dieser Klasse, ihren Methoden usw. möglicherweise schwieriger zu testen ist, da Sie diese Klasse irgendwie verspotten müssten.
lunchmeat317
Ich persönlich bin dagegen, so gut wie alles zu verspotten, außer Grenzklassen, die auf externe Systeme (Datei- / Netzwerk-E / A) zugreifen, oder solche, die nicht deterministisch sind (z. B. zufällig, systemzeitbasiert usw.). Wenn eine Klasse, die ich teste, eine Abhängigkeit aufweist, die für das aktuell getestete Feature nicht relevant ist, ziehe ich es vor, wenn möglich nur Null zu übergeben. Wenn Sie jedoch eine Methode testen, die ein Parameterobjekt akzeptiert, und wenn dieses Objekt eine Reihe von Abhängigkeiten aufweist, würde ich mir Sorgen um den Gesamtentwurf machen. Solche Gegenstände sollten leicht sein.
Sara