Comment Reflection ne pouvait-il pas conduire à des odeurs de code?

Je viens de langages de bas niveau – C ++ est le plus haut niveau que je programme.

Récemment, je suis tombé sur Reflection, et je ne peux pas comprendre comment il pourrait être utilisé sans odeurs de code.

L’idée d’inspecter une classe / méthode / fonction pendant l’exécution, à mon avis, indique une faille dans la conception – je pense que la plupart des problèmes que Reflection (essaie de) résoudre pourraient être utilisés avec Polymorphism ou utilisation correcte de l’inheritance.

Ai-je tort? Est-ce que je comprends mal le concept et l’utilité de Reflection?

Je cherche une bonne explication du moment où utiliser Reflection, où d’autres solutions échoueront ou seront trop lourdes à mettre en œuvre, ainsi que lorsque vous ne devez PAS les utiliser.

S’il vous plaît éclairer ce lubrifiant de bas niveau.

La reflection est le plus souvent utilisée pour contourner le système de type statique, mais elle présente également des cas d’utilisation intéressants:

Écrivons un ORM!

Si vous êtes familier avec NHibernate ou la plupart des autres ORM, vous écrivez des classes qui correspondent aux tables de votre firebase database, par exemple:

// used to hook into the ORMs innards public class ActiveRecordBase { public void Save(); } public class User : ActiveRecordBase { public int ID { get; set; } public ssortingng UserName { get; set; } // ... } 

Comment pensez-vous que la méthode Save() est écrite? Eh bien, dans la plupart des ORM, la méthode Save ne sait pas quels champs sont dans les classes dérivées, mais elle peut y accéder en utilisant la reflection.

Il est tout à fait possible d’avoir la même fonctionnalité de manière sécurisée, simplement en demandant à un utilisateur de remplacer une méthode pour copier des champs dans un object datarow, mais cela se traduirait par beaucoup de code passe-partout et de ballonnement.

Des bouts!

Rhino Mocks est un cadre moqueur. Vous passez un type d’interface dans une méthode et, en coulisse, le framework construit et instancie dynamicment un object de simulation implémentant l’interface.

Bien sûr, un programmeur pourrait écrire le code de passe-partout pour l’object simulé à la main, mais pourquoi voudrait-elle que le cadre le fasse pour elle?

Métadonnées!

Nous pouvons décorer des méthodes avec des atsortingbuts (métadonnées), qui peuvent servir à diverses fins:

 [FilePermission(Context.AllAccess)] // writes things to a file [Logging(LogMethod.None)] // logger doesn't log this method [MethodAccessSecurity(Role="Admin")] // user must be in "Admin" group to invoke method [Validation(ValidationType.NotNull, "reportName")] // throws exception if reportName is null public void RunDailyReports(ssortingng reportName) { ... } 

Vous devez réfléchir à la méthode pour inspecter les atsortingbuts. La plupart des frameworks AOP pour .NET utilisent des atsortingbuts pour l’injection de stratégies.

Bien sûr, vous pouvez écrire le même type de code en ligne, mais ce style est plus déclaratif.

Faisons un cadre de dépendance!

De nombreux conteneurs IoC nécessitent un certain degré de reflection pour fonctionner correctement. Par exemple:

 public class FileValidator { public FileValidator(ILogger logger) { ... } } // client code var validator = IoC.Resolve(); 

Notre conteneur IoC instanciera un validateur de fichier et transmettra une implémentation appropriée d’ILogger au constructeur. Quelle implémentation? Cela dépend de la façon dont il est implémenté.

Disons que j’ai donné le nom de l’assembly et de la classe dans un fichier de configuration. Le langage doit lire le nom de la classe en tant que chaîne et utiliser la reflection pour l’instancier.

À moins de connaître l’implémentation au moment de la compilation, il n’existe aucun moyen sûr d’instancier une classe en fonction de son nom.

Reliure tardive / Duck Typing

Il y a toutes sortes de raisons pour lesquelles vous souhaitez lire les propriétés d’un object à l’exécution. Je choisirais la consignation comme cas d’utilisation le plus simple – disons que vous écrivez un enregistreur qui accepte n’importe quel object et en extrait toutes les propriétés dans un fichier.

 public static void Log(ssortingng msg, object state) { ... } 

Vous pouvez remplacer la méthode Log pour tous les types statiques possibles ou vous pouvez simplement utiliser la reflection pour lire les propriétés à la place.

Certains langages, comme OCaml et Scala, prennent en charge le typage de type canard à vérification statique (appelé typage structurel ), mais il arrive parfois que vous ne connaissiez pas l’interface des objects à la compilation.

Ou, comme le savent les programmeurs Java, parfois, le système de caractères vous aidera à écrire toutes sortes de codes standard. Il existe un article bien connu qui décrit combien de motifs de conception sont simplifiés avec le typage dynamic .

En contournant de temps en temps le type de système, vous pouvez réorganiser votre code beaucoup plus loin que ce qui est possible avec les types statiques, ce qui donne un code un peu plus propre (de préférence caché derrière une API compatible avec le programmeur :)). De nombreux langages statiques modernes adoptent la règle d’or «typage statique si possible, typage dynamic si nécessaire», permettant aux utilisateurs de basculer entre le code statique et le code dynamic.

Des projets tels qu’hibernation (mappage O / R) et StructureMap (dependency injection) seraient impossibles sans Reflection. Comment les résoudre avec le polymorphism seul?

Ce qui rend ces problèmes si difficiles à résoudre autrement, c’est que les bibliothèques ne connaissent rien directement de votre hiérarchie de classes – elles ne le peuvent pas. Et pourtant, ils doivent connaître la structure de vos classes pour, par exemple, mapper une ligne de données arbitraire d’une firebase database vers une propriété de votre classe en utilisant uniquement le nom du champ et le nom de votre propriété.

La reflection est particulièrement utile pour les problèmes de cartographie . L’idée de convention plutôt que de code devient de plus en plus populaire et vous avez besoin d’un certain type de reflection pour le faire.

Dans .NET 3.5+, vous avez une alternative, qui consiste à utiliser des arbres d’expression. Celles-ci sont fortement typées et de nombreux problèmes résolus de manière classique à l’aide de Reflection ont été ré-implémentés à l’aide d’arbres lambdas et d’expression (voir Fluent NHibernate , Ninject ). Mais gardez à l’esprit que toutes les langues ne prennent pas en charge ces types de constructions. quand ils ne sont pas disponibles, vous êtes fondamentalement coincé avec Reflection.

D’une certaine manière (et j’espère que je ne dérange pas trop de plumes avec cela), Reflection est très souvent utilisé comme solution de contournement / hack dans les langages orientés object pour les fonctionnalités fournies gratuitement dans les langages fonctionnels. À mesure que les langages fonctionnels deviennent plus populaires et / ou que davantage de langages OO commencent à implémenter des fonctionnalités plus fonctionnelles (comme C #), nous commencerons probablement à voir Reflection de moins en moins utilisée. Mais je soupçonne que ce sera toujours le cas, pour des applications plus conventionnelles telles que les plugins (comme l’a fait remarquer l’un des autres intervenants).

En fait, vous utilisez déjà un système de reflection chaque jour : votre ordinateur.

Bien sûr, au lieu de classes, méthodes et objects, il a des programmes et des fichiers. Les programmes créent et modifient des fichiers, tout comme les méthodes créent et modifient des objects. Mais alors les programmes sont des fichiers eux-mêmes et certains programmes inspectent ou créent même d’ autres programmes!

Alors, pourquoi est-ce si correct pour une installation Linux d’être réfléchie que personne n’y pense, et effrayante pour les programmes OO?

J’ai vu de bons usages avec des atsortingbuts personnalisés. Comme un cadre de firebase database.

 [DatabaseColumn("UserID")] [PrimaryKey] public Int32 UserID { get; set; } 

La reflection peut alors être utilisée pour obtenir des informations supplémentaires sur ces champs. Je suis sûr que LINQ To SQL fait quelque chose de similaire …

D’autres exemples incluent des frameworks de test …

 [Test] public void TestSomething() { Assert.AreEqual(5, 10); } 

Sans reflection, vous devez souvent vous répéter.

Considérez ces scénarios:

  • Exécuter un ensemble de méthodes, par exemple les méthodes testXXX () dans un scénario de test
  • Générer une liste de propriétés dans un générateur d’interface graphique
  • Rendez vos classes scriptables
  • Implémenter un schéma de sérialisation

Vous ne pouvez généralement pas faire ces choses en C / C ++ sans répéter la liste complète des méthodes et propriétés affectées ailleurs dans le code.

En fait, les programmeurs C / C ++ utilisent souvent un langage de description d’interface pour exposer les interfaces lors de l’exécution (fournissant une forme de reflection).

L’utilisation judicieuse de la reflection et des annotations, associée à des conventions de codage bien définies, permet d’éviter la répétition de code et d’accroître la maintenabilité.

Je pense que la reflection est l’un de ces mécanismes puissants mais qui peuvent être facilement abusés. On vous donne les outils pour devenir un “utilisateur averti” à des fins très spécifiques, mais il n’est pas destiné à remplacer le design orienté object (tout comme la conception orientée object n’est pas une solution pour tout) ou à utiliser à la légère.

En raison de la structure de Java, vous payez déjà le prix de la représentation de la hiérarchie des classes en mémoire lors de l’exécution (comparez à C ++ où vous ne payez aucun coût à moins d’utiliser des méthodes virtuelles). Il n’y a donc pas de raison d’être pour le bloquer complètement.

Reflection est utile pour des choses comme la sérialisation – des choses comme Hibernate ou Digester peuvent l’utiliser pour déterminer la meilleure façon de stocker automatiquement les objects. De même, le modèle JavaBeans est basé sur des noms de méthodes (une décision discutable, je l’avoue), mais vous devez pouvoir inspecter les propriétés disponibles pour créer des éléments tels que des éditeurs visuels. Dans les versions plus récentes de Java, les reflections rendent les annotations utiles – vous pouvez écrire des outils et effectuer une métaprogrammation à l’aide de ces entités qui existent dans le code source, mais peuvent être accessibles à l’exécution.

Il est possible de passer toute une carrière en tant que programmeur Java et de ne jamais avoir recours à la reflection car les problèmes que vous rencontrez ne le nécessitent pas. Par contre, pour certains problèmes, c’est tout à fait nécessaire.

Comme mentionné ci-dessus, la reflection est principalement utilisée pour implémenter du code qui doit traiter des objects arbitraires. Les mappeurs ORM, par exemple, doivent instancier des objects à partir de classes définies par l’utilisateur et les remplir avec les valeurs des lignes de la firebase database. Le moyen le plus simple d’y parvenir est la reflection.

En fait, vous avez partiellement raison, la reflection est souvent une odeur de code. La plupart du temps, vous travaillez avec vos cours et n’avez pas besoin de reflection – si vous connaissez vos types, vous sacrifiez probablement inutilement la sécurité, la performance, la lisibilité et tout ce qui est bon dans ce monde. Cependant, si vous écrivez des bibliothèques, des frameworks ou des utilitaires génériques, vous rencontrerez probablement des situations mieux gérées par reflection.

Ceci est en Java, ce que je connais bien. D’autres langages offrent des fonctionnalités qui peuvent être utilisées pour atteindre les mêmes objectives, mais en Java, la reflection a des applications claires pour lesquelles c’est la meilleure (et parfois la seule) solution.

Les logiciels de test unitaires et les frameworks tels que NUnit utilisent la reflection pour obtenir une liste de tests à exécuter et les exécuter. Ils trouvent toutes les suites de tests dans un module / assembly / binary (en C # celles-ci sont représentées par des classes) et tous les tests dans ces suites (en C # ce sont des méthodes dans une classe). NUnit vous permet également de marquer un test avec une exception attendue au cas où vous testeriez des contrats d’exception.

Sans reflection, vous devez spécifier les suites de tests disponibles et les tests disponibles dans chaque suite. De plus, des choses comme les exceptions devraient être testées manuellement. Les frameworks de tests unitaires C ++ que j’ai vus ont utilisé des macros pour faire cela, mais certaines choses sont encore manuelles et cette conception est ressortingctive.

Paul Graham a un excellent essai qui peut le mieux dire:

Des programmes qui écrivent des programmes? Quand voudriez-vous jamais faire ça? Pas très souvent, si vous pensez en Cobol. Tout le temps, si vous pensez en Lisp. Ce serait pratique ici si je pouvais donner un exemple de macro puissante, et dire là! que diriez-vous de ça? Mais si je le faisais, cela ressemblerait simplement à du charabia à quelqu’un qui ne connaissait pas Lisp; il n’y a pas de place ici pour expliquer tout ce que vous devez savoir pour comprendre ce que cela signifie. Dans Ansi Common Lisp, j’ai essayé de faire avancer les choses aussi vite que possible et, malgré cela, je ne suis pas arrivé aux macros avant la page 160.

conclure avec. . .

Au cours des années où nous avons travaillé sur Viaweb, j’ai lu beaucoup de descriptions de poste. Un nouveau concurrent semblait émerger de la menuiserie à peu près tous les mois. La première chose que je ferais, après avoir vérifié si ils avaient une démo en ligne, était de regarder leurs offres d’emploi. Au bout de quelques années, j’ai pu savoir quelles entresockets s’inquiéter et lesquelles ne pas le faire. Plus les descriptions de travail avaient une saveur informatique, moins l’entreprise était dangereuse. Les plus sûrs étaient ceux qui voulaient une expérience Oracle. Vous n’avez jamais eu à vous en soucier. Vous étiez également en sécurité s’ils disaient qu’ils voulaient des développeurs C ++ ou Java. S’ils voulaient des programmeurs Perl ou Python, ce serait un peu effrayant – ça commence à ressembler à une société où le côté technique, au moins, est géré par de vrais hackers. Si j’avais déjà vu une offre d’emploi à la recherche de pirates Lisp, je serais vraiment inquiet.

Tout est question de développement rapide.

 var myObject = // Something with quite a few properties. var props = new Dictionary(); foreach (var prop in myObject.GetType().GetProperties()) { props.Add(prop.Name, prop.GetValue(myObject, null); } 

Les plugins sont un excellent exemple.

Les outils sont un autre exemple – des outils d’inspecteur, des outils de construction, etc.

Je vais donner un exemple de solution AC # qui m’a été donnée lorsque j’ai commencé à apprendre.

Il contenait des classes marquées avec l’atsortingbut [Exercise], chaque classe contenait des méthodes qui n’étaient pas implémentées (en lançant NotImplementedException). La solution comportait également des tests unitaires qui ont tous échoué.

L’objective était de mettre en œuvre toutes les méthodes et de réussir tous les tests unitaires.

La solution comportait également une interface utilisateur dans laquelle elle lisait toutes les classes marquées par Excercise et utilisait la reflection pour générer une interface utilisateur.

Nous avons ensuite été invités à implémenter nos propres méthodes, puis à comprendre comment l’interface utilisateur a été modifiée «comme par magie» pour inclure toutes les nouvelles méthodes que nous avons implémentées.

Extrêmement utile, mais souvent mal compris.

L’idée derrière cela était de pouvoir interroger toutes les propriétés des objects de l’interface graphique, de les fournir dans une interface graphique pour être personnalisés et préconfigurés. Maintenant, ses utilisations ont été étendues et se sont avérées réalisables.

EDIT: orthographe

C’est très utile pour l’dependency injection. Vous pouvez explorer les types d’assemblages chargés implémentant une interface donnée avec un atsortingbut donné. Combiné avec des fichiers de configuration appropriés, il s’avère être un moyen très puissant et propre d’append de nouvelles classes héritées sans modifier le code client.

En outre, si vous faites un éditeur qui ne s’intéresse pas vraiment au modèle sous-jacent, mais plutôt à la façon dont les objects sont structurés directement, ala System.Forms.PropertyGrid )

Sans reflection aucune architecture de plugin ne fonctionnera!

Exemple très simple en Python. Supposons que vous ayez une classe avec 3 méthodes:

 class SomeClass(object): def methodA(self): # some code def methodB(self): # some code def methodC(self): # some code 

Maintenant, dans une autre classe, vous voulez décorer ces méthodes avec un comportement supplémentaire (par exemple, vous voulez que cette classe imite SomeClass, mais avec une fonctionnalité supplémentaire). C’est aussi simple que:

 class SomeOtherClass(object): def __getattr__(self, attr_name): # do something nice and then call method that caller requested getattr(self.someclass_instance, attr_name)() 

Avec reflection, vous pouvez écrire une petite quantité de code indépendant du domaine qui n’a pas besoin de changer souvent, par rapport à l’écriture d’un code dépendant du domaine beaucoup plus important qui doit être modifié plus fréquemment (par exemple, lorsque des propriétés sont ajoutées / supprimées). Avec les conventions établies dans votre projet, vous pouvez exécuter des fonctions communes basées sur la présence de certaines propriétés, atsortingbuts, etc. La transformation de données d’objects entre différents domaines est un exemple où la reflection est vraiment utile.

Ou un exemple plus simple dans un domaine où vous souhaitez transformer des données de la firebase database en objects de données sans avoir à modifier le code de transformation lorsque les propriétés changent, tant que les conventions sont conservées (dans ce cas, les noms de propriété et un atsortingbut spécifique) :

  ///-------------------------------------------------------------------------------- /// Transform data from the input data reader into the output object. Each /// element to be transformed must have the DataElement atsortingbute associated with /// it. /// /// The database reader with the input data. /// The output object to be populated with the input data. /// Data elements to filter out of the transformation. ///-------------------------------------------------------------------------------- public static void TransformDataFromDbReader(DbDataReader inputReader, IDataObject outputObject, NameObjectCollection filterElements) { try { // add all public properties with the DataElement atsortingbute to the output object foreach (PropertyInfo loopInfo in outputObject.GetType().GetProperties()) { foreach (object loopAtsortingbute in loopInfo.GetCustomAtsortingbutes(true)) { if (loopAtsortingbute is DataElementAtsortingbute) { // get name of property to transform ssortingng transformName = DataHelper.GetSsortingng(((DataElementAtsortingbute)loopAtsortingbute).ElementName).Trim().ToLower(); if (transformName == Ssortingng.Empty) { transformName = loopInfo.Name.Trim().ToLower(); } // do transform if not in filter field list if (filterElements == null || DataHelper.GetSsortingng(filterElements[transformName]) == Ssortingng.Empty) { for (int i = 0; i < inputReader.FieldCount; i++) { if (inputReader.GetName(i).Trim().ToLower() == transformName) { // set value, based on system type loopInfo.SetValue(outputObject, DataHelper.GetValueFromSystemType(inputReader[i], loopInfo.PropertyType.UnderlyingSystemType.FullName, false), null); } } } } } } // add all fields with the DataElement attribute to the output object foreach (FieldInfo loopInfo in outputObject.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance)) { foreach (object loopAttribute in loopInfo.GetCustomAttributes(true)) { if (loopAttribute is DataElementAttribute) { // get name of field to transform string transformName = DataHelper.GetString(((DataElementAttribute)loopAttribute).ElementName).Trim().ToLower(); if (transformName == String.Empty) { transformName = loopInfo.Name.Trim().ToLower(); } // do transform if not in filter field list if (filterElements == null || DataHelper.GetString(filterElements[transformName]) == String.Empty) { for (int i = 0; i < inputReader.FieldCount; i++) { if (inputReader.GetName(i).Trim().ToLower() == transformName) { // set value, based on system type loopInfo.SetValue(outputObject, DataHelper.GetValueFromSystemType(inputReader[i], loopInfo.FieldType.UnderlyingSystemType.FullName, false)); } } } } } } } catch (Exception ex) { bool reThrow = ExceptionHandler.HandleException(ex); if (reThrow) throw; } } 

Une utilisation pas encore mentionnée: alors que la reflection est généralement considérée comme “lente”, il est possible d’utiliser Reflection pour améliorer l’efficacité du code qui utilise des interfaces comme IEquatable lorsqu’elles existent, et utilise d’autres moyens pour vérifier l’égalité ne pas. En l’absence de reflection, le code qui voulait tester si deux objects étaient égaux devrait soit utiliser Object.Equals(Object) soit vérifier à l’exécution si un object implémentant IEquatable et, si tel est le cas, lancer l’object à cette interface. Dans les deux cas, si le type de chose comparé était un type de valeur, au moins une opération de boxe serait requirejse. Utiliser Reflection permet d’avoir une classe EqualityComparer construit automatiquement une implémentation spécifique à IEqualityComparer pour tout type T , avec cette implémentation utilisant IEquatable si elle est définie ou utilisant Object.Equals(Object) si ce n’est pas le cas. La première fois que l’on utilise EqualityComparer.Default pour un type T particulier, le système devra faire plus de travail que ce qui serait nécessaire pour tester, une fois, si un type particulier implémente IEquatable . En revanche, une fois ce travail terminé, aucune vérification de type à l’exécution ne sera nécessaire puisque le système aura produit une implémentation personnalisée d’ EqualityComparer pour le type en question.