Est-ce une mauvaise habitude de (sur) utiliser la réflexion?

16

Est-ce une bonne pratique d'utiliser la réflexion si elle réduit considérablement la quantité de code passe-partout?

Fondamentalement, il existe un compromis entre les performances et peut-être la lisibilité d'un côté et l'abstraction / l'automatisation / la réduction du code passe-partout de l'autre côté.

Edit: Voici un exemple d'utilisation recommandée de la réflexion .

Pour donner un exemple, supposons qu'il existe une classe abstraite Basequi a 10 champs et 3 sous SubclassA- classes , SubclassBet SubclassCchacune avec 10 champs différents; ce sont tous de simples haricots. Le problème est que vous obtenez deux Baseréférences de type et que vous voulez voir si leurs objets correspondants sont du même (sous-) type et sont égaux.

En tant que solutions, il existe la solution brute dans laquelle vous vérifiez d'abord si les types sont égaux, puis vérifiez tous les champs ou vous pouvez utiliser la réflexion et voir dynamiquement s'ils sont du même type et parcourir toutes les méthodes commençant par "get" (convention sur la configuration), appelez-les sur les deux objets et appelez égaux sur les résultats.

boolean compare(Base base1, Base, base2) {
    if (base1 instanceof SubclassA && base2 instanceof SubclassA) { 
         SubclassA subclassA1 = (SubclassA) base1;
         SubclassA subclassA2 = (SubclassA) base2;
         compare(subclassA1, subclassA2);
    } else if (base1 instanceof SubclassB && base2 instanceof SubclassB) {
         //the same
    }
    //boilerplate
}

boolean compare(SubclassA subA1, SubclassA subA2) {
    if (!subA1.getField1().equals(subA2.getField1)) {
         return false;
    }
    if (!subA1.getField2().equals(subA2.getField2)) {
         return false;
    }
    //boilerplate
}

boolean compare(SubclassB subB1, SubclassB subB2) {
    //boilerplate
}

//boilerplate

//alternative with reflection 
boolean compare(Base base1, Base base2) {
        if (!base1.getClass().isAssignableFrom(base2.getClass())) {
            System.out.println("not same");
            System.exit(1);
        }
        Method[] methods = base1.getClass().getMethods();
        boolean isOk = true;
        for (Method method : methods) {
            final String methodName = method.getName();
            if (methodName.startsWith("get")) {
                Object object1 = method.invoke(base1);
                Object object2 = method.invoke(base2);
                if(object1 == null || object2 == null)  {
                    continue;
                }
                if (!object1.equals(object2)) {
                    System.out.println("not equals because " + object1 + " not equal with " + object2);
                    isOk = false;
                }
            }
        }

        if (isOk) {
            System.out.println("is OK");
        }
}
m3th0dman
la source
20
La surutilisation de quoi que ce soit est une mauvaise habitude.
Tulains Córdova
1
@ user61852 C'est vrai, trop de liberté mène à la dictature. Un vieux grec le savait déjà.
ott--
6
«Trop d'eau serait mauvais pour vous. Évidemment, trop est précisément cette quantité qui est excessive - c'est ce que cela signifie! »- Stephen Fry
Jon Purdy
4
"A chaque fois que vous vous retrouvez en train d'écrire du code du formulaire" si l'objet est de type T1, alors faites quelque chose, mais si c'est de type T2, alors faites autre chose ", claquez-
rm5248

Réponses:

25

La réflexion a été créée dans un but spécifique, pour découvrir la fonctionnalité d'une classe qui était inconnue au moment de la compilation, similaire à ce que font les fonctions dlopenet dlsymen C. Toute utilisation en dehors de cela devrait être soigneusement examinée.

Vous est-il déjà venu à l'esprit que les concepteurs Java eux-mêmes ont rencontré ce problème? C'est pourquoi pratiquement chaque classe a une equalsméthode. Différentes classes ont différentes définitions de l'égalité. Dans certaines circonstances, un objet dérivé peut être égal à un objet de base. Dans certaines circonstances, l'égalité pourrait être déterminée sur la base de champs privés sans obturateurs. Tu ne sais pas.

C'est pourquoi chaque objet qui souhaite une égalité personnalisée doit implémenter une equalsméthode. Finalement, vous voudrez mettre les objets dans un ensemble, ou les utiliser comme index de hachage, alors vous devrez equalsquand même l' implémenter . D'autres langages le font différemment, mais Java l'utilise equals. Vous devez vous en tenir aux conventions de votre langue.

De plus, le code "passe-partout", s'il est placé dans la bonne classe, est assez difficile à bousiller. La réflexion ajoute une complexité supplémentaire, ce qui signifie des chances supplémentaires de bugs. Dans votre méthode, par exemple, deux objets sont considérés comme égaux si l'un retourne nullpour un certain champ et l'autre non. Que se passe-t-il si l'un de vos getters retourne l'un de vos objets, sans un approprié equals? Votre if (!object1.equals(object2))échouera. Le fait que la réflexion soit rarement utilisée est également sujette aux bogues, de sorte que les programmeurs ne sont pas aussi familiers avec ses pièges.

Karl Bielefeldt
la source
12

L'utilisation excessive de la réflexion dépend probablement de la langue utilisée. Ici, vous utilisez Java. Dans ce cas, la réflexion doit être utilisée avec précaution car souvent, ce n'est qu'une solution de contournement pour une mauvaise conception.

Donc, vous comparez différentes classes, c'est un problème parfait pour remplacer la méthode . Notez que les instances de deux classes différentes ne doivent jamais être considérées comme égales. Vous ne pouvez comparer l'égalité que si vous avez des instances de la même classe. Voir /programming/27581/overriding-equals-and-hashcode-in-java pour un exemple de mise en œuvre correcte de la comparaison d'égalité.

Sulthan
la source
16
+1 - Certains d'entre nous pensent que TOUTE utilisation de la réflexion est un drapeau rouge indiquant une mauvaise conception.
Ross Patterson
1
Dans ce cas, très probablement, une solution basée sur des égaux est souhaitable, mais comme idée générale, quel est le problème avec la solution de réflexion? En fait, il est très générique et les méthodes égales n'ont pas besoin d'être explicitement écrites dans chaque classe et sous-classe (bien qu'elles puissent facilement être générées par un bon IDE).
m3th0dman du
2
@ RossPatterson Pourquoi?
m3th0dman du
2
@ m3th0dman Parce que cela conduit à des choses comme votre compare()méthode en supposant que toute méthode commençant par "get" est un getter et est sûre et appropriée à appeler dans le cadre d'une opération de comparaison. C'est une violation de la définition d'interface prévue de l'objet, et bien que cela puisse être utile, c'est presque toujours faux.
Ross Patterson
4
@ m3th0dman Il viole l'encapsulation - la classe de base doit accéder aux attributs de ses sous-classes. Et s'ils sont privés (ou privés de getter)? Et le polymorphisme? Si je décide d'ajouter une autre sous-classe et que je souhaite faire la comparaison différemment? Eh bien, la classe de base le fait déjà pour moi et je ne peux pas le changer. Et si les getters chargeaient paresseusement quelque chose? Est-ce que je veux une méthode de comparaison pour faire ça? Comment puis-je même savoir qu'une méthode commençant par getest un getter et non une méthode personnalisée renvoyant quelque chose?
Sulthan
1

Je pense que vous avez deux problèmes ici.

  1. Quelle quantité de code dynamique et statique dois-je avoir?
  2. Comment exprimer une version personnalisée de l'égalité?

Code dynamique vs code statique

Il s'agit d'une question permanente et la réponse est très avisée.

D'une part, votre compilateur est très bon pour intercepter toutes sortes de mauvais code. Il le fait à travers diverses formes d'analyse, l'analyse de type étant courante. Il sait que vous ne pouvez pas utiliser un Bananaobjet dans le code qui attend un Cog. Il vous le dit par une erreur de compilation.

Maintenant, il ne peut le faire que lorsqu'il peut déduire à la fois le type accepté et le type donné à partir du contexte. La quantité qui peut être déduite et le degré de généralité de cette inférence dépendent en grande partie de la langue utilisée. Java peut déduire des informations de type via des mécanismes tels que l'héritage, les interfaces et les génériques. Le kilométrage varie, certaines autres langues offrent moins de mécanismes et d'autres plus. Cela revient toujours à ce que le compilateur peut savoir être vrai.

D'un autre côté, votre compilateur ne peut pas prédire la forme du code étranger, et parfois un algorithme général peut être exprimé sur de nombreux types qui ne peuvent pas être facilement exprimés en utilisant le système de type du langage. Dans ces cas, le compilateur ne peut pas toujours connaître le résultat à l'avance et il peut même ne pas savoir quelle question poser. La réflexion, les interfaces et la classe Object sont la façon dont Java gère ces problèmes. Vous devrez fournir les vérifications et la manipulation correctes, mais ce n'est pas malsain d'avoir ce type de code.

Que ce soit pour rendre votre code très spécifique ou très général se résume au problème que vous essayez de gérer. Si vous pouvez facilement l'exprimer à l'aide du système de saisie, faites-le. Laissez le compilateur jouer à ses forces et vous aider. Si le système de type ne pouvait pas savoir à l'avance (code étranger), ou si le système de type est mal adapté à une implémentation générale de votre algorithme, la réflexion (et d'autres moyens dynamiques) sont le bon outil à utiliser.

Sachez simplement que sortir du système de typage de votre langue est surprenant. Imaginez vous approcher de votre ami et entamer une conversation en anglais. Laissez tomber soudainement quelques mots d'espagnol, de français et de cantonais qui expriment précisément vos pensées. Le contexte en dira beaucoup à votre ami, mais il peut également ne pas savoir comment gérer ces mots, ce qui conduit à toutes sortes de malentendus. Est-il préférable de traiter ces malentendus que d'expliquer ces idées en anglais en utilisant plus de mots?

Égalité personnalisée

Bien que je comprenne que Java s'appuie fortement sur la equalsméthode pour comparer généralement deux objets, ce n'est pas toujours approprié dans un contexte donné.

Il y a une autre manière, et c'est aussi une norme Java. Son appelé un comparateur .

Quant à la façon dont vous implémentez votre comparateur dépendra de ce que vous comparez, et comment.

  • Il peut être appliqué à deux objets quelle que soit leur equalsimplémentation spécifique .
  • Il pourrait implémenter une méthode de comparaison générale (basée sur la réflexion) pour gérer deux objets quelconques.
  • Les fonctions de comparaison standard peuvent être ajoutées pour les types d'objets couramment comparés, garantissant ainsi la sécurité et l'optimisation des types.
Kain0_0
la source
1

Je préfère éviter autant que possible la programmation réfléchissante car elle

  • rend le code plus difficile à vérifier statiquement par le compilateur
  • rend le code plus difficile à raisonner
  • rend le code plus difficile à refactoriser

Il est également beaucoup moins performant que les appels de méthode simples; il était auparavant plus lent d'un ordre de grandeur ou plus.

Vérification statique du code

Tout code réfléchissant recherche des classes et des méthodes à l'aide de chaînes; dans l'exemple d'origine, il recherche toute méthode commençant par "get"; il renverra des getters mais d'autres méthodes en plus, comme "gettysburgAddress ()". Les règles pourraient être resserrées dans le code, mais le fait demeure qu'il s'agit d'une vérification d'exécution ; un IDE et le compilateur ne peuvent pas aider. En général, je n'aime pas le code "typé à la chaîne" ou "obsédé par les primitifs".

Plus difficile à raisonner

Le code réfléchissant est plus détaillé que les appels de méthode simples. Plus de code = plus de bogues, ou au moins plus de potentiel pour les bogues, plus de code à lire, à tester, etc. Moins c'est plus.

Plus difficile à refactoriser

Parce que le code est basé sur une chaîne / dynamiquement; dès que la réflexion est sur la scène, vous ne pouvez pas refactoriser le code avec 100% de confiance en utilisant les outils de refactorisation d'un IDE car l'IDE ne peut pas détecter les utilisations réfléchissantes.

Fondamentalement, évitez autant que possible la réflexion dans le code général; recherchez une conception améliorée.

David Kerr
la source
0

La surutilisation de quoi que ce soit par définition est mauvaise, non? Alors débarrassons-nous du (terminé) pour l'instant.

Diriez-vous que l'utilisation interne étonnamment lourde de la réflexion au printemps est une mauvaise habitude?

Spring apprivoise la réflexion en utilisant des annotations - ainsi que Hibernate (et probablement des dizaines / centaines d'autres outils).

Suivez ces modèles si vous l'utilisez dans votre propre code. Utilisez des annotations pour vous assurer que l'EDI de votre utilisateur peut toujours l'aider (même si vous êtes le seul "utilisateur" de votre code, une utilisation imprudente de la réflexion reviendra probablement pour vous mordre dans le cul finalement).

Cependant, sans tenir compte de la façon dont votre code sera utilisé par les développeurs, même l'utilisation la plus simple de la réflexion est probablement une utilisation excessive.

Bill K
la source
0

Je pense que la plupart de ces réponses ne sont pas pertinentes.

  1. Oui, vous devriez probablement écrire equals()et hashcode(), comme l'a noté @KarlBielefeldt.

  2. Mais, pour une classe avec de nombreux domaines, cela peut être fastidieux.

  3. Donc ça dépend .

    • Si vous n'avez besoin que d'égal et de hashcode rarement , il est pragmatique, et probablement correct, d'utiliser un calcul de réflexion à usage général. Au moins comme un premier passage rapide et sale.
    • mais si vous avez besoin de beaucoup, c'est-à-dire que ces objets sont placés dans HashTables, de sorte que les performances seront un problème, vous devez absolument écrire le code. ce sera beaucoup plus rapide.
  4. Une autre possibilité: si vos classes ont vraiment tellement de champs que l'écriture d'un égal est fastidieuse, envisagez

    • mettre les champs dans une carte.
    • vous pouvez toujours écrire des getters et setters personnalisés
    • utiliser Map.equals()pour votre comparaison. (ou Map.hashcode())

par exemple ( note : ignorer les vérifications nulles, devrait probablement utiliser des énumérations au lieu des clés de chaîne, ce qui n'est pas souvent montré ...)

class TooManyFields {
  private HashMap<String, Object> map = new HashMap<String, Object>();

  public setFoo(int i) { map.put("Foo", Integer.valueOf(i)); }
  public int getFoo()  { return map.get("Foo").intValue(); }

  public setBar(Sttring s) { map.put("Bar", s); }
  public String getBar()  { return map.get("Bar").toString(); }

  ... more getters and setters ...

  public boolean equals(Object o) {
    return (o instanceof TooManyFields) &&
           this.map.equals( ((TooManyFields)o).map);
}
user949300
la source