Devrions-nous éviter les objets personnalisés en tant que paramètres?

49

Supposons que j'ai un objet personnalisé, étudiant :

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

Et une classe, Window , utilisée pour afficher les informations d'un étudiant :

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

Cela semble assez normal, mais j’ai trouvé que Window n’était pas assez facile à tester individuellement, car il avait besoin d’un véritable objet Student pour appeler la fonction. J'essaie donc de modifier showInfo pour qu'il n'accepte pas d' objet Student directement:

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

pour qu'il soit plus facile de tester Windows individuellement:

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

Mais j'ai trouvé que la version modifiée posait un autre problème:

  1. Modifier un étudiant (par exemple: ajouter de nouvelles propriétés) nécessite de modifier la signature de méthode de showInfo

  2. Si Student avait plusieurs propriétés, la méthode-signature de Student serait très longue.

Donc, en utilisant des objets personnalisés en tant que paramètre ou en acceptant chaque propriété d'objets en paramètre, laquelle est la plus facile à gérer?

ggrr
la source
40
Et votre 'amélioration' showInfonécessite une vraie chaîne, un vrai float et deux vrais ints. En quoi la fourniture d'un Stringobjet réel est-elle meilleure que celle d'un Studentobjet réel ?
Bart van Ingen Schenau
28
Un problème majeur avec la transmission directe des paramètres: vous avez maintenant deux intparamètres. Sur le site de l'appel, il n'est pas vérifié que vous les transmettez dans le bon ordre. Et si vous échangez idet age, ou firstNameet lastName? Vous introduisez un point d'échec potentiel qui peut être très difficile à détecter jusqu'à ce qu'il explose, et vous l'ajoutez à chaque site d'appel .
Chris Hayes
38
@ChrisHayes ah, l'ancienne showForm(bool, bool, bool, bool, int)méthode - j'adore ceux-ci ...
Boris l'Araignée
3
@ChrisHayes au moins ce n'est pas JS ...
Jens Schauder
2
une propriété sous-appréciée des tests: s'il est difficile de créer / utiliser vos propres objets dans les tests, votre API pourrait éventuellement
nécessiter du

Réponses:

131

L'utilisation d'un objet personnalisé pour regrouper les paramètres associés est en fait un modèle recommandé. En tant que refactoring, il s’appelle Introduce Parameter Object .

Votre problème est ailleurs. Tout d'abord, générique Windowne doit rien savoir sur Student. Au lieu de cela, vous devriez avoir une sorte de StudentWindowqui sait seulement afficher Students. Deuxièmement, la création d'une Studentinstance à tester ne pose aucun problème StudentWindowtant Studentqu'elle ne contient aucune logique complexe qui compliquerait considérablement les tests StudentWindow. Si cette logique existe, Studentil est préférable de créer une interface et de s'en moquer.

Euphorique
la source
14
Cela vaut la peine de vous avertir que vous pouvez vous mettre en difficulté si le nouvel objet ne constitue pas vraiment un groupe logique. N'essayez pas de cadrer chaque paramètre dans un seul objet. décider au cas par cas. L'exemple de la question semble assez clairement en être un bon candidat. Le Studentregroupement est logique et est susceptible de se retrouver dans d'autres domaines de l'application.
JPMc26
Pédantiquement, si vous avez déjà l'objet, par exemple Student, ce serait un objet entier à conserver
abuzittin gillifirca
4
Rappelez-vous également la loi de Demeter . Il y a un équilibre à trouver mais le tldr est à ne pas faire a.b.csi votre méthode prend a. Si votre méthode en arrive au point où il vous faut environ plus de 4 paramètres ou 2 niveaux d’accession à la propriété, il faudra probablement en tenir compte. Notez également qu'il s'agit d'une directive - comme toutes les autres directives, elle requiert la discrétion de l'utilisateur. Ne le suivez pas aveuglément.
Dan Pantry
7
J'ai trouvé la première phrase de cette réponse extrêmement difficile à analyser.
Helrich
5
@ Qwerky, je suis très fortement en désaccord. L'étudiant sonne VRAIMENT comme une feuille dans le graphe d'objets (à l'exception d'autres objets triviaux tels que peut-être Name, DateOfBirth, etc.), simplement un conteneur pour l'état de l'étudiant. Il n'y a aucune raison que ce soit un étudiant devrait être difficile à construire, parce que ce devrait être un type d'enregistrement. Créer des doubles de tests pour un élève semble être une recette pour des tests difficiles à maintenir et / ou une forte dépendance à l'égard d'un cadre d'isolation sophistiqué.
Sara
26

Vous dites que c'est

pas tout à fait facile de tester individuellement, car il faut un objet Student réel pour appeler la fonction

Mais vous pouvez simplement créer un objet étudiant à transmettre à votre fenêtre:

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

Cela ne semble pas beaucoup plus complexe d'appeler.

Tom.Bowen89
la source
7
Le problème survient lorsque se Studentréfère à a University, qui fait référence à de nombreux Facultys et Campuss, avec Professors et Buildings, dont aucun showInfon’utilise réellement, mais vous n’avez défini aucune interface permettant aux tests de "savoir" cela et de ne fournir que l’élève correspondant. données, sans construire l'ensemble de l'organisation. L'exemple Studentest un objet de données simple et, comme vous le dites, les tests devraient être heureux de fonctionner avec cet objet.
Steve Jessop
4
Le problème vient du fait que l'étudiant se réfère à une université, qui fait référence à de nombreuses facultés et campus, avec des professeurs et des bâtiments. Pas de repos pour les méchants.
Abuzittin gillifirca
1
@abuzittingillifirca, "Mère d'objet" est une solution, mais votre objet étudiant est peut-être trop complexe. Il serait peut-être préférable d'avoir simplement UniversityId et un service (utilisant l'injection de dépendance) qui donnera un objet University à partir d'un UniversityId.
Ian
12
Si Student est très complexe ou difficile à initialiser, moquez-vous de ça. Les tests sont beaucoup plus puissants avec des frameworks tels que Mockito ou d'autres langages équivalents.
Borjab,
4
Si showInfo ne s'intéresse pas à l'Université, il suffit alors de le définir sur null. Les annulations sont horribles dans la production et un envoi divin dans les tests. Pouvoir spécifier un paramètre comme nul dans un test communique une intention et dit que "cette chose n'est pas nécessaire ici". J'envisagerais également de créer une sorte de modèle de vue pour les étudiants contenant uniquement les données pertinentes, mais considérant que showInfo ressemble à une méthode dans une classe d'interface utilisateur.
Sara
22

En termes simples:

  • Ce que vous appelez un "objet personnalisé" est généralement appelé simplement un objet.
  • Vous ne pouvez pas éviter de passer des objets en tant que paramètres lors de la conception d'un programme ou d'une API non triviale, ou de l'utilisation d'une API ou d'une bibliothèque non triviale.
  • C'est parfaitement correct de passer des objets en tant que paramètres. Regardez l'API Java et vous verrez beaucoup d'interfaces qui reçoivent des objets en tant que paramètres.
  • Les classes dans les bibliothèques que vous utilisez ont été écrites par de simples mortels comme vous et moi, donc celles que nous écrivons ne sont pas "personnalisées" , elles le sont tout simplement.

Modifier:

Comme @ Tom.Bowen89 l'a déclaré, tester la méthode showInfo n'est pas beaucoup plus complexe:

showInfo(new Student(8812372,"Peter Parker",16,8.9));
Tulains Córdova
la source
3
  1. Dans votre exemple d'étudiant, je suppose qu'il est trivial d'appeler le constructeur Student pour créer un étudiant à transmettre à showInfo. Donc, il n'y a pas de problème.
  2. En supposant que l'exemple Student soit délibérément banalisé pour cette question et qu'il soit plus difficile à construire, vous pouvez utiliser un test double . Il existe un certain nombre d'options pour les doubles de tests, les simulacres, les talons, etc. dont l'article de Martin Fowler fait état.
  3. Si vous voulez rendre la fonction showInfo plus générique, vous pouvez la faire parcourir les variables publiques, ou peut-être les accesseurs publics de l'objet transmis et exécuter la logique d'affichage pour toutes. Ensuite, vous pouvez transmettre n'importe quel objet conforme à ce contrat et cela fonctionnera comme prévu. Ce serait un bon endroit pour utiliser une interface. Par exemple, transmettez un objet Showable ou ShowInfoable à la fonction showInfo qui peut afficher non seulement les informations sur les étudiants, mais également les informations sur les objets qui implémentent l'interface (ces interfaces ont évidemment besoin de meilleurs noms, en fonction de la spécificité ou du caractère générique de l'objet que vous pouvez transmettre. être et ce qu'un étudiant est une sous-classe de).
  4. Il est souvent plus facile de passer des primitives, et parfois nécessaire pour les performances, mais plus vous pourrez regrouper des concepts similaires, plus votre code sera compréhensible. La seule chose à surveiller est d'essayer de ne pas trop le faire et de se retrouver avec fizzbuzz d'entreprise .
Encaitar
la source
3

Steve McConnell dans Code Complete a traité de cette question en abordant les avantages et les inconvénients de la transmission d'objets à des méthodes au lieu d'utiliser des propriétés.

Pardonnez-moi si je me trompe dans certains détails, je travaille de mémoire car cela fait plus d'un an que j'ai accès au livre:

Il en arrive à la conclusion qu'il vaut mieux ne pas utiliser d'objet, mais envoyer uniquement les propriétés absolument nécessaires à la méthode. La méthode ne devrait pas avoir à connaître quoi que ce soit à propos de l'objet en dehors des propriétés qu'il utilisera dans le cadre de ses opérations. De plus, avec le temps, si l'objet est modifié, cela pourrait avoir des conséquences inattendues sur la méthode utilisant l'objet.

Il a également expliqué que si vous vous retrouviez avec une méthode qui acceptait beaucoup d'arguments différents, c'est probablement un signe que la méthode en fait trop et qu'elle devrait être décomposée en plusieurs méthodes plus petites.

Cependant, parfois, parfois, vous avez réellement besoin de beaucoup de paramètres. L'exemple qu'il donne serait celui d'une méthode qui construit une adresse complète, en utilisant de nombreuses propriétés d'adresse différentes (bien que cela puisse être obtenu en utilisant un tableau de chaînes lorsque vous y réfléchissez).

utilisateur1666620
la source
7
J'ai le code complet 2. Il contient une page entière consacrée à ce problème. La conclusion est que les paramètres doivent être au niveau d'abstraction correct. Parfois, cela nécessite de passer un objet entier, parfois juste des attributs individuels.
VENEZ DU
UV. Le code de référencement complet est un double avantage. Une bonne considération de la conception sur l'opportunité de tester. Préférer le design, je pense que McConnell dirait dans notre contexte. Une excellente conclusion serait donc "intégrer l'objet paramètre dans la conception" (ou Studentdans ce cas). Et c’est de cette façon que les tests informent le design , englobant complètement la réponse la plus votée tout en maintenant l’intégrité du design.
radarbob
2

Il est beaucoup plus facile d'écrire et de lire des tests si vous passez l'objet entier:

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

En comparaison,

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

La ligne pourrait être écrite, si vous passez les valeurs séparément, comme:

showAStudentWithAFailingGrade(view);

où l'appel de méthode réelle est enterré quelque part comme

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

Pour être précis, le fait que vous ne puissiez pas placer l'appel de méthode dans le test est un signe que votre API est mauvaise.

abuzittin gillifirca
la source
1

Vous devriez transmettre ce qui a du sens, quelques idées:

Plus facile à tester. Si le ou les objets doivent être édités, qu'est-ce qui nécessite le moins de refactoring? La réutilisation de cette fonction à d’autres fins est-elle utile? Quelle est la quantité minimale d'informations dont j'ai besoin pour fournir cette fonction? (En le séparant (cela peut vous permettre de réutiliser ce code), méfiez-vous de la possibilité de créer cette fonction, puis de tout goulot d'étranglement pour utiliser exclusivement cet objet.)

Toutes ces règles de programmation ne sont que des guides pour vous aider à réfléchir dans la bonne direction. Ne construisez tout simplement pas une bête de code - si vous n'êtes pas sûr et que vous avez juste besoin de continuer, choisissez une direction / la vôtre ou une suggestion ici, et si vous arrivez à un point où vous pensez "oh, j'aurais dû le faire ainsi chemin '- vous pouvez probablement ensuite revenir en arrière et le refacturer assez facilement. (Par exemple, si vous avez une classe d'enseignant - elle nécessite uniquement la même propriété que Étudiant et vous modifiez votre fonction pour accepter n'importe quel objet du formulaire Personne.)

Je serais plus enclin à laisser l'objet principal transmis, car la façon dont je le code va expliquer plus facilement le fonctionnement de cette fonction.

Lilly
la source
1

Une méthode courante consiste à insérer une interface entre les deux processus.

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

    }
}

Cela devient parfois un peu compliqué, mais les choses deviennent un peu plus ordonnées en Java si vous utilisez une classe interne.

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

        };
    }
}

Vous pouvez ensuite tester la Windowclasse en lui donnant simplement un faux HasInfoobjet.

Je soupçonne que ceci est un exemple du modèle de décorateur .

Ajoutée

Il semble y avoir une certaine confusion causée par la simplicité du code. Voici un autre exemple qui peut mieux démontrer la technique.

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

    }
}
Vieux curcuma
la source
Si showInfo ne souhaitait afficher que le nom de l'étudiant, pourquoi ne pas simplement lui passer le nom? encapsuler un champ nommé sémantiquement significatif dans une interface abstraite contenant une chaîne sans aucune idée de ce que la chaîne représente ressemble à un déclassement ÉNORME, à la fois en termes de maintenabilité et de compréhensibilité.
sara
@kai - L'utilisation de Studentet Stringici pour le type de retour est purement à des fins de démonstration. Il y aurait probablement des paramètres supplémentaires getInfotels que Panedessiner. Le concept ici est de transmettre des composants fonctionnels en tant que décorateurs de l'objet d'origine .
OldCurmudgeon
dans ce cas, vous seriez en train de coupler étroitement l'entité étudiante à votre interface utilisateur, cela semble encore pire ...
Sara
1
@ Kai - Bien au contraire. Mon interface utilisateur ne connaît que les HasInfoobjets. Studentsait être un.
OldCurmudgeon
Si vous donnez de getInforetour passe vide un Paneà tirer à, puis la mise en œuvre (dans la Studentclasse) est soudainement couplé à balancer ou tout ce que vous utilisez. Si vous lui faites renvoyer une chaîne et que vous prenez 0 paramètres, votre interface utilisateur ne saura pas quoi faire avec la chaîne sans hypothèses magiques et couplage implicite. Si vous faites getInforéellement renvoyer un modèle de vue avec des propriétés pertinentes, votre Studentclasse est à nouveau couplée à la logique de présentation. Je pense qu'aucune de ces alternatives n'est souhaitable
Sara
1

Vous avez déjà beaucoup de bonnes réponses, mais voici quelques suggestions supplémentaires qui pourraient vous permettre de voir une solution alternative:

  • Votre exemple montre qu'un étudiant (clairement un objet du modèle) est passé à une fenêtre (apparemment un objet au niveau de la vue). Un objet Contrôleur ou Présentateur intermédiaire peut être utile si vous n'en avez pas déjà, ce qui vous permet d'isoler votre interface utilisateur de votre modèle. Le contrôleur / présentateur doit fournir une interface pouvant être utilisée pour le remplacer pour les tests d'interface utilisateur, et doit utiliser des interfaces pour faire référence aux objets de modèle et aux objets de vue afin de pouvoir l'isoler des deux à des fins de test. Vous devrez peut-être fournir un moyen abstrait de créer ou de charger ceux-ci (par exemple, des objets Factory, des objets Repository, ou similaire).

  • Le transfert de parties pertinentes de vos objets de modèle dans un objet de transfert de données est une approche utile pour l’interfaçage lorsque votre modèle devient trop complexe.

  • Il se peut que votre élève enfreigne le principe de séparation des interfaces. Si tel est le cas, il pourrait être avantageux de le scinder en plusieurs interfaces plus faciles à utiliser.

  • Le chargement différé peut faciliter la construction de graphiques d'objets volumineux.

Jules
la source
0

C'est en fait une question décente. Le vrai problème ici est l'utilisation du terme générique "objet", qui peut être un peu ambigu.

Généralement, dans un langage POO classique, le terme "objet" en est venu à signifier "instance de classe". Les instances de classe peuvent être assez lourdes - propriétés publiques et privées (et entre celles-ci), méthodes, héritage, dépendances, etc. Vous ne voudriez pas vraiment utiliser quelque chose comme ça pour simplement transmettre certaines propriétés.

Dans ce cas, vous utilisez un objet en tant que conteneur contenant simplement certaines primitives. En C ++, les objets tels que ceux-ci étaient appelés structs(et existent toujours dans des langages tels que C #). En fait, les structures ont été conçues exactement pour l'usage dont vous parlez - elles ont regroupé des objets et des primitives apparentés lorsqu'ils avaient une relation logique.

Cependant, dans les langages modernes, il n'y a vraiment aucune différence entre une structure et une classe lorsque vous écrivez le code , vous pouvez donc utiliser un objet. (Dans les coulisses, cependant, il y a certaines différences dont vous devriez être conscient - par exemple, une structure est un type de valeur, pas un type de référence.) Fondamentalement, tant que vous gardez votre objet simple, il sera facile tester manuellement. Les langages et les outils modernes vous permettent toutefois d’atténuer un peu ces problèmes (via des interfaces, des frameworks moqueurs, l’injection de dépendances, etc.)

lunchmeat317
la source
1
Passer une référence n’est pas coûteux, même si l’objet a une taille d’un milliard de téraoctets, car la référence n’a encore que la taille d’un int dans la plupart des langues. Vous devriez vous préoccuper davantage de savoir si la méthode de réception est exposée à une API trop grande et si vous couplez les choses de manière indésirable. J'envisagerais de créer une couche de mappage traduisant les objets métier ( Student) en modèles de vue ( StudentInfoou StudentInfoViewModelautres), mais cela pourrait ne pas être nécessaire.
Sara
Les classes et les structures sont très différentes. L'un est passé par valeur (c'est-à-dire que la méthode qui le reçoit obtient une copie ) et l'autre est passé par référence (le destinataire obtient simplement un pointeur sur l'original). Ne pas comprendre cette différence est dangereux.
RubberDuck
@ kai Je comprends que passer une référence n'est pas cher. Ce que je dis, c'est que la création d'une fonction nécessitant une instance de classe complète peut être plus difficile à tester en fonction des dépendances de cette classe, de ses méthodes, etc.
lunchmeat317
Personnellement, je ne me moque de rien, sauf des classes de limites qui accèdent à des systèmes externes (entrées / sorties de fichiers / réseau) ou non déterministes (par exemple, aléatoire, système basé sur le temps, etc.). Si une classe que je teste a une dépendance qui ne correspond pas à la fonctionnalité actuellement testée, je préfère simplement passer à null si possible. Mais si vous testez une méthode qui prend un objet paramètre, si cet objet comporte de nombreuses dépendances, je m'inquiéterais de la conception globale. De tels objets doivent être légers.
Sara