web-dev-qa-db-fra.com

Faut-il éviter les objets personnalisés comme paramètres?

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

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

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

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

Cela a l'air tout à fait normal, mais j'ai trouvé Window n'est pas assez facile à tester individuellement, car il a besoin d'un vrai Student objet pour appeler la fonction. J'essaie donc de modifier showInfo pour qu'il n'accepte pas directement un objet Student:

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

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

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

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

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

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

Ainsi, en utilisant des objets personnalisés en tant que paramètre ou en acceptant chaque propriété dans les objets en tant que paramètre, laquelle est plus maintenable?

49
ggrr

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 est appelé Introduce Parameter Object .

Votre problème est ailleurs. Tout d'abord, le générique Window ne devrait rien savoir sur Student. Au lieu de cela, vous devriez avoir une sorte de StudentWindow qui ne connaît que l'affichage de Students. Deuxièmement, il n'y a absolument aucun problème à créer une instance de Student pour tester StudentWindow tant que Student ne contient aucune logique complexe qui compliquerait considérablement le test de StudentWindow. S'il a cette logique, faire de Student une interface et la moquer devrait être préféré.

131
Euphoric

Tu dis que c'est

pas assez facile à tester individuellement, car il a besoin d'un vrai objet Student pour appeler la fonction

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

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

Il ne semble pas beaucoup plus complexe d'appeler.

26
Tom.Bowen89

En termes simples:

  • Ce que vous appelez un "objet personnalisé" est généralement simplement appelé 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.
  • Il est parfaitement correct de passer des objets en tant que paramètres. Jetez un œil à l'API Java et vous verrez de nombreuses interfaces qui reçoivent des objets en tant que paramètres.
  • Les classes dans les bibliothèques que vous utilisez sont écrites par de simples mortels comme vous et moi, donc celles que nous écrivons ne sont pas "personnalisées" , elles le sont.

Éditer:

Comme @ Tom.Bowen89 déclare qu'il n'est pas beaucoup plus complexe de tester la méthode showInfo:

showInfo(new Student(8812372,"Peter Parker",16,8.9));
22
Tulains Córdova
  1. Dans votre exemple d'étudiant, je suppose qu'il est trivial d'appeler le constructeur Student pour créer un étudiant à transmettre à showInfo. Il n'y a donc aucun problème.
  2. En supposant que l'exemple Student est délibérément banalisé pour cette question et qu'il est plus difficile à construire, alors vous pouvez utiliser un test double . Il existe un certain nombre d'options pour tester les doubles, les simulacres, les talons, etc., dont on parle dans l'article de Martin Fowler.
  3. Si vous souhaitez rendre la fonction showInfo plus générique, vous pouvez la faire itérer sur les variables publiques, ou peut-être que les accesseurs publics de l'objet sont passés et exécutent la logique d'exposition pour chacun d'eux. Ensuite, vous pourriez passer n'importe quel objet conforme à ce contrat et cela fonctionnerait comme prévu. Ce serait un bon endroit pour utiliser une interface. Par exemple, passez un objet Showable ou ShowInfoable à la fonction showInfo qui peut afficher non seulement les informations des étudiants, mais aussi les informations de tout objet qui implémente l'interface (évidemment, ces interfaces ont besoin de meilleurs noms en fonction de la spécificité ou du générique que vous souhaitez pour l'objet auquel vous pouvez passer. être et ce qu'un étudiant est une sous-classe de).
  4. Il est souvent plus facile de faire circuler les primitives et parfois nécessaire pour les performances, mais plus vous pouvez regrouper des concepts similaires, plus votre code sera généralement compréhensible. . La seule chose à surveiller est d'essayer de ne pas trop le faire et de se retrouver avec fizzbuzz d'entreprise .
3
Encaitar

Steve McConnell dans Code Complete a abordé ce problème, discutant des avantages et des inconvénients de la transmission d'objets dans des méthodes au lieu d'utiliser des propriétés.

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

Il 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 sur l'objet en dehors des propriétés qu'il utilisera dans le cadre de ses opérations. De plus, au fil du temps, si l'objet est modifié, cela peut avoir des conséquences inattendues sur la méthode utilisant l'objet.

Il a également expliqué que si vous vous retrouvez avec une méthode qui accepte beaucoup d'arguments différents, c'est probablement un signe que la méthode fait trop et 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 pensez).

3
user1666620

Il est beaucoup plus facile d'écrire et de lire des tests si vous réussissez tout l'objet:

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 peut être écrite, si vous passez des valeurs séparément, comme:

showAStudentWithAFailingGrade(view);

où l'appel de méthode réel 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 au point, que vous ne pouvez pas mettre l'appel de méthode réel dans le test est un signe que votre API est mauvaise.

2

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 modèle) est passé à une fenêtre (apparemment un objet au niveau de la vue). Un objet Controller ou Presenter intermédiaire peut être avantageux si vous n'en avez pas déjà un, vous permettant d'isoler votre interface utilisateur de votre modèle. Le contrôleur/présentateur doit fournir une interface qui peut être utilisée pour la remplacer pour les tests d'interface utilisateur, et doit utiliser des interfaces pour faire référence aux objets de modèle et afficher les objets afin de pouvoir l'isoler des deux pour les tests. Vous devrez peut-être fournir une méthode abstraite de création ou de chargement de ces éléments (par exemple, des objets d'usine, des objets de référentiel ou similaires).

  • 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 étudiant viole le principe de ségrégation d'interface. Si tel est le cas, il pourrait être avantageux de le diviser en plusieurs interfaces plus faciles à utiliser.

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

1
Jules

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

Plus facile à tester. Si le ou les objets doivent être modifié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 donner à cette fonction sa fonction? (En le décomposant - il peut vous permettre de réutiliser ce code - méfiez-vous de tomber dans le trou de conception de la création de 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 faire réfléchir dans la bonne direction. Ne construisez pas de bête de code - si vous n'êtes pas sûr et que vous devez simplement continuer, choisissez une direction/la vôtre ou une suggestion ici, et si vous atteignez un point où vous pensez `` oh, j'aurais dû le faire façon '- vous pouvez probablement revenir en arrière et refactoriser assez facilement. (Par exemple, si vous avez une classe Teacher - elle a juste besoin des mêmes propriétés que Student, et vous changez votre fonction pour accepter n'importe quel objet du formulaire Personne)

Je serais plus enclin à garder l'objet principal transmis - car comment je code, il va expliquer plus facilement ce que fait cette fonction.

1
Lilly

Une voie courante autour de cela 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 dans 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 classe Window en lui donnant simplement un faux objet HasInfo.

Je soupçonne que c'est un exemple du Motif Décorateur .

Ajouté

Il semble y avoir une certaine confusion causée par la simplicité du code. Voici un autre exemple qui peut mieux illustrer 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) {

    }
}
1
OldCurmudgeon

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 classique OOP, le terme "objet" a fini par signifier "instance de classe". Les instances de classe peuvent être assez lourdes - propriétés publiques et privées (et celles entre les deux), méthodes , l'héritage, les dépendances, etc. Vous ne voudriez pas vraiment utiliser quelque chose comme ça pour simplement passer certaines propriétés.

Dans ce cas, vous utilisez un objet comme conteneur qui contient simplement des primitives. En C++, de tels objets étaient connus sous le nom de structs (et ils existent toujours dans des langages comme C #). Les structures ont, en fait, été conçues exactement pour l'usage dont vous parlez - elles ont regroupé des objets et des primitives connexes lorsqu'elles 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 correctement un objet. (Dans les coulisses, cependant, il y a quelques différences dont vous devez ê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, ce sera facile pour tester manuellement. Les langages et outils modernes vous permettent d'atténuer un peu cela, cependant (via des interfaces, des cadres de simulation, l'injection de dépendances, etc.)

0
lunchmeat317