Quand est-ce que le test de type est OK?


53

En supposant un langage avec une sécurité de type inhérente (par exemple, pas de JavaScript):

Avec une méthode qui accepte a SuperType, nous savons que dans la plupart des cas, nous pourrions être tentés d'effectuer des tests de type pour choisir une action:

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    o.doSomethingA()
  } else {
    o.doSomethingB();
  }
}

Nous devrions généralement, si pas toujours, créer une seule méthode pouvant être remplacée par le SuperTypeet faire ceci:

public void DoSomethingTo(SuperType o) {
  o.doSomething();
}

... dans lequel chaque sous-type reçoit sa propre doSomething()implémentation. Le reste de notre application peut alors ignorer de manière appropriée si une donnée SuperTypeest vraiment un SubTypeAou SubTypeB.

Merveilleux.

Mais nous avons toujours des is aopérations similaires à celles de la plupart, sinon de la totalité, des langages sécurisés. Et cela suggère un besoin potentiel d'essais de type explicites.

Dans quelles situations devrions- nous ou devons- nous effectuer des tests de type explicites?

Pardonnez mon absence ou mon manque de créativité. Je sais que je l'ai déjà fait mais honnêtement, il y a si longtemps, je ne me souviens plus si ce que j'ai fait était bon! Et de mémoire récente, je ne pense pas avoir eu le besoin de tester des types en dehors de JavaScript, mon cow-boy.



4
Il est intéressant de noter que ni Java ni C # n'avaient de génériques dans leur première version. Vous deviez transtyper depuis et vers un objet pour utiliser des conteneurs.
Doval

6
La "vérification de type" consiste presque toujours à vérifier si le code respecte les règles de typage statiques du langage. Ce que vous voulez dire est généralement appelé test de type .


3
C'est pourquoi j'aime écrire en C ++ et laisser RTTI désactivé. Quand on ne peut littéralement pas tester les types d'objet au moment de l'exécution, cela oblige le développeur à adhérer à une bonne conception orientée objet en ce qui concerne la question posée ici.

Réponses:


48

"Jamais", la réponse canonique à "quand est-ce que le test de type est acceptable?" Il n'y a aucun moyen de prouver ou d'infirmer cela. cela fait partie d'un système de croyances sur ce qui fait un "bon design" ou un "bon design orienté objet". C'est aussi hokum.

Pour être sûr, si vous avez un ensemble intégré de classes et plus d'une ou deux fonctions nécessitant ce type de test de type direct, vous êtes probablement en train de le faire. Ce dont vous avez vraiment besoin, c'est d'une méthode implémentée différemment dans SuperTypeses sous-types. Cela fait partie intégrante de la programmation orientée objet, et les classes de la raison et l'héritage existent.

Dans ce cas, explicitement, le test de type est faux, non pas parce que le test de type est intrinsèquement faux, mais parce que le langage a déjà une manière propre, extensible et idiomatique de réaliser la discrimination de type et que vous ne l'avez pas utilisé. Au lieu de cela, vous êtes tombé dans un idiome primitif, fragile et non extensible.

Solution: Utilisez l'idiome. Comme vous l'avez suggéré, ajoutez une méthode à chacune des classes, puis laissez les algorithmes d'héritage et de sélection de méthodes standard déterminer le cas qui s'applique. Ou si vous ne pouvez pas changer les types de base, sous-classe et ajoutez votre méthode ici.

Voilà pour la sagesse conventionnelle et quelques réponses. Quelques cas où le test de type explicite a du sens:

  1. C'est une pièce unique. Si vous avez beaucoup de discrimination à faire, vous pouvez étendre les types ou la sous-classe. Mais tu ne le fais pas. Vous n'avez besoin que de tests explicites à un ou deux endroits. Par conséquent, il ne vaut pas la peine de revenir en arrière et de parcourir la hiérarchie des classes pour ajouter les fonctions en tant que méthodes. Ou bien, il ne vaut pas la peine, en pratique, d'ajouter le type de généralité, de test, de révision de conception, de documentation ou d'autres attributs des classes de base pour une utilisation aussi simple et limitée. Dans ce cas, l'ajout d'une fonction effectuant des tests directs est rationnel.

  2. Vous ne pouvez pas ajuster les classes. Vous pensez au sous-classement - mais vous ne pouvez pas. Par exemple, de nombreuses classes en Java sont désignées final. Vous essayez de lancer a public class ExtendedSubTypeA extends SubTypeA {...} et le compilateur vous dit, en termes non équivoques, que ce que vous faites n'est pas possible. Désolé, grâce et sophistication du modèle orienté objet! Quelqu'un a décidé que vous ne pouvez pas étendre leurs types! Malheureusement, la plupart des bibliothèques standard le sont final, et la création de classes finalest un guide de conception courant. Une fonction end-run est ce qui reste à votre disposition.

    BTW, cela ne se limite pas aux langages statiques. Langage dynamique Python a un certain nombre de classes de base qui, sous les couvertures implémentées en C, ne peuvent pas vraiment être modifiées. Comme Java, cela inclut la plupart des types standard.

  3. Votre code est externe. Vous développez avec des classes et des objets provenant d'une gamme de serveurs de base de données, de moteurs middleware et d'autres bases de code que vous ne pouvez ni contrôler ni ajuster. Votre code n’est qu’un consommateur modeste d’objets générés ailleurs. Même si vous pouviez SuperTypecréer une sous - classe , vous ne pourrez pas obtenir les bibliothèques sur lesquelles vous comptez pour générer des objets dans vos sous-classes. Ils vont vous remettre des exemples des types qu’ils connaissent, pas vos variantes. Ce n'est pas toujours le cas ... parfois, ils sont conçus pour la flexibilité et ils instancient de manière dynamique les instances de classes que vous leur fournissez. Ou bien, ils fournissent un mécanisme pour enregistrer les sous-classes que vous souhaitez que leurs usines construisent. Les analyseurs syntaxiques XML semblent particulièrement efficaces pour fournir de tels points d’entrée; voir par exemple ou lxml en Python . Mais la plupart des bases de code ne fournissent pas de telles extensions. Ils vont vous rendre les cours pour lesquels ils ont été construits et qu'ils connaissent. En général, il n’est pas judicieux d’intégrer leurs résultats dans vos résultats personnalisés afin que vous puissiez utiliser un sélecteur de type uniquement orienté objet. Si vous voulez faire de la discrimination de type, vous devrez le faire de façon relativement grossière. Votre code de test de type semble alors tout à fait approprié.

  4. Générique du pauvre / envoi multiple. Vous souhaitez accepter une variété de types différents dans votre code et estimez qu'avoir un tableau de méthodes très spécifiques à un type n'est pas gracieux. public void add(Object x)semble logique, mais pas un tableau de addByte, addShort, addInt, addLong, addFloat, addDouble, addBoolean, addChar, et addStringvariantes (pour ne citer que quelques - uns). Disposant de fonctions ou de méthodes qui prennent un super-type élevé, puis déterminent quoi faire, type par type, ils ne vous gagneront pas le Purity Award lors du symposium annuel Booch-Liskov Design, Naming hongrois vous donnera une API plus simple. Dans un sens, votre is-aouis-instance-of testing simule une distribution générique ou multiple dans un contexte linguistique qui ne la prend pas en charge de manière native.

    La prise en charge intégrée de la langue pour les génériques et la saisie à l'aide de canards réduit le besoin de vérification de type en rendant plus probable la possibilité de "faire quelque chose de gracieux et approprié". La sélection de plusieurs envois / interfaces proposée dans des langues telles que Julia et Go remplace de la même manière les tests de type directs par des mécanismes intégrés pour la sélection de type "Que faire". Mais toutes les langues ne les supportent pas. Java, par exemple, est généralement à envoi unique, et ses idiomes ne sont pas très faciles à utiliser.

    Mais même avec toutes ces fonctionnalités de discrimination de type - héritage, génériques, dactylographie et envoi multiple - il est parfois pratique de disposer d'une seule routine consolidée qui rend le fait que vous fassiez quelque chose en fonction du type d'objet. clair et immédiat. En métaprogrammation, je l’ai trouvé essentiellement inévitable. Que ce soit le type de pragmatisme en action ou de "codage en blanc" dépendra de votre philosophie de conception et de vos convictions.


Si une opération ne peut pas être effectuée sur un type particulier, mais sur deux types différents pour lesquels elle serait implicitement convertible, la surcharge ne convient que si les deux conversions produisent des résultats identiques. Sinon, on aboutit à des comportements crasseux comme dans .NET: (1.0).Equals(1.0f)céder true [argument favorise to double], mais (1.0f).Equals(1.0)céder false [argument favorise to object]; en Java, on Math.round(123456789*1.0)obtient 123456789, mais on Math.round(123456789*1.0)obtient 123456792 [l'argument favorise floatplutôt que double].
Supercat

C'est l'argument classique contre le transtypage / escalade de type automatique. Résultats négatifs et paradoxaux, du moins dans les cas extrêmes. Je suis d'accord, mais je ne sais pas comment vous voulez que cela réponde à ma réponse.
Jonathan Eunice

Je répondais à votre point 4 qui semblait préconiser la surcharge plutôt que l’utilisation de méthodes portant des noms différents et de types différents.
Supercat

2
@supercat Blame ma pauvre vision, mais les deux expressions avec Math.roundsont identiques à moi. Quelle est la différence?
Lily Chung

2
@IstvanChung: Ooops ... ce dernier aurait dû être Math.round(123456789)[indicatif de ce qui pourrait arriver si quelqu'un réécrit Math.round(thing.getPosition() * COUNTS_PER_MIL)pour renvoyer une valeur de position non mise à l'échelle, ne réalisant pas que getPositionintlong
renvoyant

25

La situation principale à laquelle j’ai eu besoin était lorsque l’on compare deux objets, comme dans une equals(other)méthode, qui peut nécessiter des algorithmes différents selon le type exact de other. Même alors, c'est assez rare.

L'autre situation dans laquelle j'ai eu à venir, encore une fois très rarement, est après la désérialisation ou l'analyse syntaxique, où vous en avez parfois besoin pour une conversion sécurisée vers un type plus spécifique.

De plus, vous avez parfois besoin d’un hack pour contourner un code tiers que vous ne contrôlez pas. C'est une de ces choses que vous ne voulez pas vraiment utiliser régulièrement, mais vous êtes content que ce soit là quand vous en avez vraiment besoin.


1
Le cas de désérialisation me ravit momentanément, je me suis souvenu à tort de l’avoir utilisé là; mais, maintenant je ne peux pas imaginer comment j'aurais! Je sais que j'ai fait des recherches de type étranges pour cela; mais je ne suis pas sûr que cela constitue un test de type . La sérialisation est peut-être un parallèle plus étroit: devoir interroger des objets pour déterminer leur type concret.
svidgen

1
La sérialisation est généralement faisable en utilisant le polymorphisme. Lors de la désérialisation, vous faites souvent quelque chose comme BaseClass base = deserialize(input), parce que vous ne connaissez pas encore le type, vous devez alors le if (base instanceof Derived) derived = (Derived)basestocker en tant que type dérivé exact.
Karl Bielefeldt

1
L'égalité a du sens. D'après mon expérience, ces méthodes ont souvent la forme suivante: «Si ces deux objets ont le même type concret, indiquez si tous leurs champs sont égaux; sinon, retourne false (ou non comparable) ».
Jon Purdy

2
En particulier, le fait que vous vous trouviez en train de tester le type est un bon indicateur du fait que les comparaisons d'égalité polymorphes sont un nid de vipères.
Steve Jessop

12

Le cas standard (mais espérons-le rare) ressemble à ceci: si dans la situation suivante

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    DoSomethingA((SubTypeA) o )
  } else {
    DoSomethingB((SubTypeB) o );
  }
}

les fonctions DoSomethingAou DoSomethingBne peuvent pas être facilement implémentées en tant que fonctions membres de l'arbre d'héritage de SuperType/ SubTypeA/ SubTypeB. Par exemple, si

  • les sous-types font partie d'une bibliothèque que vous ne pouvez pas modifier, ou
  • si ajouter le code DoSomethingXXXà cette bibliothèque reviendrait à introduire une dépendance interdite.

Notez qu'il existe souvent des situations où vous pouvez contourner ce problème (par exemple, en créant un wrapper ou un adaptateur pour SubTypeAet SubTypeBou, ou en essayant de le DoSomethingréimplémenter complètement en termes d'opérations de base de SuperType), mais parfois ces solutions ne valent pas la peine des choses plus compliquées et moins extensibles que de faire le test de type explicite.

Un exemple tiré de mon travail d’hier: j’étais dans une situation où j’allais paralléliser le traitement d’une liste d’objets (de type SuperType, avec exactement deux sous-types différents, où il est extrêmement improbable qu’il y en ait de plus). La version non parallèle contenait deux boucles: une boucle pour les objets du sous-type A appelant DoSomethingAet une seconde boucle pour les objets du sous-type B appelant DoSomethingB.

Les méthodes "DoSomethingA" et "DoSomethingB" sont toutes deux des calculs gourmands en temps, qui utilisent des informations de contexte qui ne sont pas disponibles au niveau des sous-types A et B. (il est donc inutile de les implémenter en tant que fonctions membres des sous-types). Du point de vue de la nouvelle "boucle parallèle", cela facilite beaucoup les choses en les traitant de manière uniforme. J'ai donc implémenté une fonction similaire à celle DoSomethingTod'en haut. Cependant, l'examen des implémentations de "DoSomethingA" et "DoSomethingB" montre qu'elles fonctionnent très différemment en interne. Donc, essayer de mettre en œuvre un " SuperTypequelque chose de générique" générique en utilisant beaucoup de méthodes abstraites ne fonctionnait pas vraiment, ou signifierait de sur-concevoir complètement les choses.


2
Avez-vous une chance d'ajouter un petit exemple concret pour convaincre mon cerveau embué que ce scénario n'est pas artificiel?
svidgen

Pour être clair, je ne dis pas qu'il est artificiel. Je soupçonne que je suis vraiment extrêmement brumeux aujourd'hui.
svidgen

@svidgen: c'est loin d'être artificiel, en fait, j'ai rencontré cette situation aujourd'hui (bien que je ne puisse pas poster cet exemple ici car il contient des éléments internes à l'entreprise). Et je suis d’accord avec les autres réponses selon lesquelles l’utilisation de l’opérateur "is" devrait être une exception et ne devrait être utilisée que dans de rares cas.
Doc Brown

Je pense que votre montage, que j'ai négligé, donne un bon exemple. ... Y at - il des cas où cela est OK , même lorsque vous faites le contrôle SuperTypeet les sous - classes de elle?
svidgen

6
Voici un exemple concret: le conteneur de niveau supérieur d'une réponse JSON provenant d'un service Web peut être un dictionnaire ou un tableau. En règle générale, vous avez un outil qui transforme le JSON en objets réels (par exemple, NSJSONSerializationdans Obj-C), mais vous ne voulez pas simplement vous assurer que la réponse contient le type que vous attendez, vous devez donc le vérifier avant de l'utiliser (par exemple if ([theResponse isKindOfClass:[NSArray class]])...) .
Caleb

5

Comme oncle Bob l'appelle:

When your compiler forgets about the type.

Dans l'un de ses épisodes de Clean Coder, il a donné l'exemple d'un appel de fonction utilisé pour renvoyer l' Employeeart. Managerest un sous-type de Employee. Supposons que nous ayons un service d'application qui accepte un Manageridentifiant et le convoque au bureau :) La fonction getEmployeeById()renvoie un super-type Employee, mais je souhaite vérifier si un responsable est renvoyé dans ce cas d'utilisation.

Par exemple:

var manager = employeeRepository.getEmployeeById(empId);
if (!(manager is Manager))
   throw new Exception("Invalid Id specified.");
manager.summon();

Ici, je vérifie si l’employé renvoyé par requête est en fait un responsable (c’est-à-dire que j’attends qu’il s’agisse d’un responsable et que, dans le cas contraire, il échoue rapidement).

Ce n'est pas le meilleur exemple, mais c'est l'oncle Bob après tout.

Mise à jour

J'ai mis à jour l'exemple autant que je m'en souvienne de mémoire.


1
Pourquoi Managerla mise en œuvre de summon()simplement ne pas jeter l'exception dans cet exemple?
svidgen

@svidgen peut-être que le serveur CEOpeut appeler Manager.
user253751

@svidgen, il ne serait alors pas aussi clair que employeeRepository.getEmployeeById (empId) doit renvoyer un responsable
Ian

@ Ian, je ne vois pas cela comme un problème. Si le code d'appel demande un Employee, il doit seulement veiller à obtenir quelque chose qui se comporte comme un Employee. Si différentes sous-classes de Employeeont des autorisations, des responsabilités, etc. différentes, pourquoi le test de type est-il une meilleure option qu'un système d'autorisations réel?
svidgen

3

Quand la vérification du type est-elle correcte?

Jamais.

  1. En ayant le comportement associé à ce type dans une autre fonction, vous enfreignez le principe d'ouverture fermée , car vous êtes libre de modifier le comportement existant du type en modifiant la isclause, ou (dans certaines langues, ou en fonction de la scénario) car vous ne pouvez pas étendre le type sans modifier les éléments internes de la fonction effectuant la isvérification.
  2. Plus important encore, les ischèques sont un signe fort que vous violez le principe de substitution de Liskov . Tout ce qui fonctionne avec SuperTypedevrait ignorer complètement les sous-types possibles.
  3. Vous associez implicitement un comportement au nom du type. Cela rend votre code plus difficile à gérer car ces contrats implicites sont répartis dans tout le code et ne sont pas garantis d'être appliqués de manière universelle et cohérente comme le sont les membres de la classe.

Cela dit, les ischèques peuvent être moins dommageables que les autres alternatives. Placer toutes les fonctionnalités communes dans une classe de base est fastidieux et conduit souvent à des problèmes plus graves. Utiliser une seule classe qui a un drapeau ou une énumération indiquant le "type" de l'instance ... est pire qu'horrible, car vous étendez maintenant le contournement du système de types à tous les consommateurs.

En bref, vous devez toujours considérer les contrôles de type comme une odeur de code forte. Mais comme pour toutes les directives, il y aura des moments où vous serez obligé de choisir entre la violation de la directive la moins offensante.


3
Il y a un inconvénient mineur: implémenter des types de données algébriques dans des langages qui ne les prennent pas en charge. Cependant, l’utilisation de l’héritage et du contrôle de typage n’est là qu’un détail d’implémentation astucieuse; L'intention n'est pas d'introduire des sous-types, mais de catégoriser les valeurs. J'en parle uniquement parce que les TDA sont utiles et que le qualificatif n'est jamais très fort, mais sinon, je suis entièrement d'accord. instanceoffuit les détails de mise en œuvre et casse l'abstraction.
Doval

17
"Jamais" est un mot que je n'aime pas vraiment dans un tel contexte, surtout quand il contredit ce que vous écrivez ci-dessous.
Doc Brown

10
Si par "jamais" vous voulez dire "parfois", alors vous avez raison.
Caleb

2
OK signifie admissible, acceptable, etc., mais pas nécessairement idéal ou optimal. Contrastez avec pas OK : si quelque chose ne va pas, vous ne devriez pas le faire du tout. Comme vous l'avez indiqué, la nécessité de vérifier le type de quelque chose peut indiquer des problèmes plus profonds dans le code, mais il y a des moments où c'est l'option la plus appropriée, la moins mauvaise, et dans de telles situations, il est évidemment correct d'utiliser les outils de votre choix. disposition. (S'il était facile de les éviter dans toutes les situations, elles ne seraient probablement pas là.) La question se résume à identifier ces situations et ne les aide jamais .
Caleb

2
@supercat: Une méthode devrait exiger le type le moins spécifique possible qui réponde à ses exigences . IEnumerable<T>ne promet pas un "dernier" élément existe. Si votre méthode a besoin d'un tel élément, elle devrait nécessiter un type qui en garantisse l'existence. Et les sous-types de ce type peuvent alors fournir des implémentations efficaces de la méthode "Last".
cHao

2

Si vous avez une base de code volumineuse (plus de 100 000 lignes de code) et que vous êtes sur le point d'expédier ou si vous travaillez dans une branche qui devra être fusionnée par la suite, il y a donc un coût / risque considérable à changer beaucoup de gestion.

Vous avez alors parfois la possibilité d’utiliser un grand réfracteur du système ou un simple «test de type» localisé. Cela crée une dette technique qui devrait être remboursée le plus rapidement possible, mais ce n’est souvent pas le cas.

(Il est impossible de trouver un exemple, car tout code suffisamment petit pour être utilisé comme exemple est également suffisamment petit pour que la meilleure conception soit clairement visible.)

Ou en d'autres termes, lorsque l'objectif est de toucher votre salaire plutôt que d'obtenir «des votes” pour la propreté de votre conception.


L'autre cas courant est le code de l'interface utilisateur, par exemple lorsque vous affichez une interface utilisateur différente pour certains types d'employés, mais que vous ne souhaitez manifestement pas que les concepts d'interface utilisateur échappent à toutes vos classes de «domaine».

Vous pouvez utiliser le "test de type" pour choisir la version de l'interface utilisateur à afficher ou disposer d'une table de conversion sophistiquée convertissant les "classes de domaine" en "classes d'interface utilisateur". La table de consultation est juste un moyen de cacher le "test de type" en un seul endroit.

(Le code de mise à jour de la base de données peut poser les mêmes problèmes que le code de l'interface utilisateur. Cependant, vous n'avez généralement qu'un seul jeu de code de mise à jour de la base de données, mais vous pouvez avoir de nombreux écrans différents qui doivent s'adapter au type d'objet affiché.)


Le modèle de visiteur est souvent un bon moyen de résoudre votre problème d'interface utilisateur.
Ian Goldby

@IanGoldby, cela peut parfois convenir, mais vous faites toujours un "test de type", juste caché un peu.
Ian

Caché dans le même sens que c'est caché lorsque vous appelez une méthode virtuelle classique? Ou voulez-vous dire autre chose? Le modèle de visiteur que j'ai utilisé ne contient aucune instruction conditionnelle qui dépend du type. Tout est fait par la langue.
Ian Goldby

@ IanGoldby, je voulais dire caché dans le sens où cela peut rendre le code WPF ou WinForms plus difficile à comprendre. Je m'attends à ce que certaines interfaces utilisateur de base Web fonctionnent très bien.
Ian

2

L'implémentation de LINQ utilise de nombreuses vérifications de type pour les optimisations de performances possibles, puis un repli pour IEnumerable.

L’exemple le plus évident est probablement la méthode ElementAt (petit extrait de la source .NET 4.5):

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index) { 
    IList<TSource> list = source as IList<TSource>;

    if (list != null) return list[index];
    // ... and then an enumerator is created and MoveNext is called index times

Mais il existe de nombreux endroits dans la classe Enumerable où un modèle similaire est utilisé.

Alors peut-être que l'optimisation des performances pour un sous-type couramment utilisé est une utilisation valide. Je ne sais pas comment cela aurait pu être mieux conçu.


Il aurait pu être mieux conçu en fournissant un moyen pratique par lequel les interfaces peuvent fournir des implémentations par défaut, puis en IEnumerable<T>incluant de nombreuses méthodes telles que celles de List<T>, ainsi qu'une Featurespropriété indiquant les méthodes pouvant fonctionner normalement, lentement ou pas du tout. ainsi que diverses hypothèses qu'un consommateur peut faire en toute sécurité sur la collection (par exemple, sa taille et / ou son contenu existant sont-ils assurés de ne jamais changer [un type pourrait prendre Adden charge tout en garantissant que le contenu existant serait immuable]).
Supercat

Sauf dans les cas où un type pourrait avoir besoin de détenir l'une de deux choses complètement différentes à des moments différents et que les exigences sont clairement incompatibles (et que l'utilisation de champs distincts serait donc redondante), je considère généralement la nécessité d'essayer un transtypage est un signe les membres qui auraient dû faire partie d'une interface de base ne l'étaient pas. Cela ne veut pas dire que le code qui utilise une interface qui aurait dû inclure certains membres mais ne devrait pas utiliser try-cast pour contourner l'omission de l'interface de base, mais ces interfaces d'écriture de base devraient minimiser le besoin d'essais en diffusion par les clients.
Supercat

1

Il existe un exemple qui apparaît souvent dans les développements de jeux, en particulier dans la détection de collision, qu'il est difficile de traiter sans l'utilisation d'une certaine forme de test de type.

Supposons que tous les objets du jeu proviennent d'une classe de base commune GameObject. Chaque objet a une forme de collision de corps rigide CollisionShapequi peut fournir une interface commune ( - à - dire la position d'interrogation, orientation, etc.) , mais les formes de collision réels seront tous les sous - classes concrètes , telles que Sphere, Box, ConvexHull, etc. stocker spécifique de l' information pour ce type d'objet géométrique (voir ici pour un exemple réel)

Maintenant, pour tester les collisions, je dois écrire une fonction pour chaque paire de types de formes de collision:

detectCollision(Sphere, Sphere)
detectCollision(Sphere, Box)
detectCollision(Sphere, ConvexHull)
detectCollision(Box, ConvexHull)
...

qui contiennent les calculs spécifiques nécessaires pour effectuer une intersection de ces deux types géométriques.

À chaque "tick" de ma boucle de jeu, je dois vérifier les paires d'objets pour les collisions. Mais je n'ai accès qu'aux GameObjects et aux s correspondants CollisionShape. Il est clair que j'ai besoin de connaître des types concrets pour savoir quelle fonction de détection de collision appeler. Pas même la double dépêche (qui n’est logiquement pas différente de la vérification du type) ne peut aider ici *.

En pratique, dans cette situation, les moteurs physiques que j'ai vus (Bullet et Havok) reposent sur des tests de type d'une forme ou d'une autre.

Je ne dis pas que c'est nécessairement une bonne solution, c'est simplement que c'est peut-être la meilleure parmi un petit nombre de solutions possibles à ce problème.

* Techniquement, il est possible d’utiliser la double répartition d’une manière horrible et compliquée qui exigerait N (N + 1) / 2 combinaisons (où N est le nombre de types de formes que vous avez) et masquerait ce que vous faites réellement. recherche simultanément les types des deux formes, donc je ne considère pas cela comme une solution réaliste.


1

Parfois, vous ne souhaitez pas ajouter une méthode commune à toutes les classes car il ne leur appartient en réalité pas d'exécuter cette tâche spécifique.

Par exemple, vous souhaitez dessiner certaines entités mais ne souhaitez pas leur ajouter directement le code de dessin (ce qui est logique). Dans les langues ne prenant pas en charge l'envoi multiple, vous pouvez vous retrouver avec le code suivant:

void DrawEntity(Entity entity) {
    if (entity instanceof Circle) {
        DrawCircle((Circle) entity));
    else if (entity instanceof Rectangle) {
        DrawRectangle((Rectangle) entity));
    } ...
}

Cela devient problématique lorsque ce code apparaît à plusieurs endroits et que vous devez le modifier partout lorsque vous ajoutez un nouveau type d'entité. Si c'est le cas, vous pouvez éviter cela en utilisant le modèle Visiteur, mais il est parfois préférable de garder les choses simples et de ne pas trop les modifier. Ce sont les situations où le test de type est correct.


0

Le seul moment que j'utilise est en combinaison avec la réflexion. Mais même dans ce cas, la vérification dynamique est généralement non codée en dur dans une classe spécifique (ou uniquement codée en dur dans des classes spéciales telles que Stringou List).

Par vérification dynamique, je veux dire:

boolean checkType(Type type, Object object) {
    if (object.isOfType(type)) {

    }
}

et non codé en dur

boolean checkIsManaer(Object object) {
    if (object instanceof Manager) {

    }
}

0

Le test de type et le typage de type sont deux concepts étroitement liés. Si étroitement lié que je suis confiant de dire que vous ne devriez jamais faire de test de type sauf si votre intention est de dactylographier l'objet en fonction du résultat.

Lorsque vous pensez à une conception idéale orientée objet, les tests de type (et le transtypage) ne devraient jamais se produire. Mais avec un peu de chance, vous avez maintenant compris que la programmation orientée objet n'est pas idéale. Parfois, en particulier avec un code de niveau inférieur, le code ne peut rester fidèle à l'idéal. C'est le cas avec ArrayLists en Java; comme ils ne savent pas au moment de l'exécution quelle classe est stockée dans le tableau, ils créent des Object[]tableaux et les convertissent statiquement dans le type correct.

Il a été souligné qu'un besoin courant de tests de typage (et de transtypage) provient de la Equalsméthode, qui dans la plupart des langues est supposée prendre une forme Object. L'implémentation doit prévoir des vérifications détaillées si les deux objets sont du même type, ce qui nécessite de pouvoir tester leur type.

Les essais de type sont également fréquemment évoqués. Vous aurez souvent des méthodes qui renvoient Object[]ou un autre tableau générique, et vous voulez extraire tous les Fooobjets pour une raison quelconque. Ceci est une utilisation parfaitement légitime des tests de type et de la conversion.

En général, le test de type est mauvais lorsqu'il couple inutilement votre code à la manière dont une implémentation spécifique a été écrite. Cela peut facilement nécessiter un test spécifique pour chaque type ou combinaison de types, par exemple si vous souhaitez rechercher l'intersection de lignes, de rectangles et de cercles, et que la fonction d'intersection utilise un algorithme différent pour chaque combinaison. Votre objectif est de placer tous les détails spécifiques à un type d'objet au même endroit que cet objet, car cela facilitera la maintenance et l'extension de votre code.


1
ArrayListsJe ne connais pas la classe stockée au moment de l'exécution car Java ne contenait pas de génériques et quand ils ont finalement été introduits, Oracle a opté pour une compatibilité ascendante avec du code sans génériques. equalsa le même problème, et c'est une décision de conception discutable de toute façon; les comparaisons d'égalité n'ont pas de sens pour tous les types.
Doval

1
Techniquement, les collections Java ne transforment leur contenu en rien. Le compilateur injecte des typecasts à l'emplacement de chaque accès: par exempleString x = (String) myListOfStrings.get(0)

La dernière fois que j'ai consulté le code source Java (qui pouvait être 1.6 ou 1.5), il y avait une conversion explicite dans le code source ArrayList. Il génère un avertissement (supprimé) du compilateur pour une bonne raison, mais il est néanmoins autorisé. Je suppose que vous pourriez dire que, en raison de la manière dont les génériques sont mis en œuvre, il a toujours Objectété difficile d’y accéder; les génériques en Java ne fournissent que le casting implicite sécurisé par les règles du compilateur.
Meustrus

0

C'est acceptable dans le cas où vous devez prendre une décision impliquant deux types et que cette décision est encapsulée dans un objet situé en dehors de cette hiérarchie de types. Par exemple, supposons que vous planifiez le prochain objet traité dans une liste d'objets en attente de traitement:

abstract class Vehicle
{
    abstract void Process();
}

class Car : Vehicle { ... }
class Boat : Vehicle { ... }
class Truck : Vehicle { ... }

Maintenant, disons que notre logique commerciale est littéralement "toutes les voitures ont la priorité sur les bateaux et les camions". L'ajout d'une Prioritypropriété à la classe ne vous permet pas d'exprimer clairement cette logique métier, car vous obtiendrez ceci:

abstract class Vehicle
{
    abstract void Process();
    abstract int Priority { get }
}

class Car : Vehicle { public Priority { get { return 1; } } ... }
class Boat : Vehicle { public Priority { get { return 2; } } ... }
class Truck : Vehicle { public Priority { get { return 2; } } ... }

Le problème est que, maintenant, pour comprendre l'ordre de priorité, vous devez examiner toutes les sous-classes ou, en d'autres termes, vous avez ajouté le couplage aux sous-classes.

Vous devez bien sûr définir les priorités en constantes et les placer dans une classe par elles-mêmes, ce qui permet de maintenir la logique métier de planification ensemble:

static class Priorities
{
    public const int CAR_PRIORITY = 1;
    public const int BOAT_PRIORITY = 2;
    public const int TRUCK_PRIORITY = 2;
}

Cependant, en réalité, l’algorithme de planification est quelque chose qui pourrait changer à l’avenir et il pourrait éventuellement dépendre de plus que de la simple saisie. Par exemple, on pourrait dire que "les camions pesant plus de 5 000 kg ont priorité sur tous les autres véhicules". C'est pourquoi l'algorithme de planification appartient à sa propre classe et il est judicieux de vérifier le type pour déterminer lequel doit être utilisé en premier:

class VehicleScheduler : IScheduleVehicles
{
    public Vehicle WhichVehicleGoesFirst(Vehicle vehicle1, Vehicle vehicle2)
    {
        if(vehicle1 is Car) return vehicle1;
        if(vehicle2 is Car) return vehicle2;
        return vehicle1;
    }
}

C'est le moyen le plus simple d'implémenter la logique métier et reste le plus flexible pour les modifications futures.


Je ne suis pas d'accord avec votre exemple particulier, mais serais d'accord avec le principe de disposer d'une référence privée pouvant contenir différents types avec des significations différentes. Ce que je suggérerais comme meilleur exemple serait un domaine qui peut contenir nullun String, ou un String[]. Si 99% des objets nécessitent exactement une chaîne, l'encapsulation de chaque chaîne dans une construction séparée String[]peut ajouter une surcharge de stockage. Gérer le cas de chaîne unique en utilisant une référence directe à a Stringnécessitera plus de code, mais économisera de la mémoire et accélérera les choses.
Supercat

0

Le test de type est un outil, utilisez-le à bon escient et cela peut être un puissant allié. Utilisez-le mal et votre code va commencer à sentir.

Dans notre logiciel, nous avons reçu des messages sur le réseau en réponse à des demandes. Tous les messages désérialisés partageaient une classe de base commune Message.

Les classes elles-mêmes étaient très simples, juste la charge utile telle que les propriétés C # typées et les routines permettant de les regrouper et de les hiérarchiser (En fait, j'ai généré la plupart des classes à l'aide de modèles t4 à partir de la description XML du format du message)

Le code serait quelque chose comme:

Message response = await PerformSomeRequest(requestParameter);

// Server (out of our control) would send one message as response, but 
// the actual message type is not known ahead of time (it depended on 
// the exact request and the state of the server etc.)
if (response is ErrorMessage)
{ 
    // Extract error message and pass along (for example using exceptions)
}
else if (response is StuffHappenedMessage)
{
    // Extract results
}
else if (response is AnotherThingHappenedMessage)
{
    // Extract another type of result
}
// Half a dozen other possible checks for messages

Certes, on pourrait soutenir que l’architecture du message pourrait être mieux conçue, mais elle a été conçue il ya longtemps et non pour C #, c’est donc ce qu’elle est. Ici, le test de type a résolu un problème réel pour nous d'une manière pas trop chaotique.

Il est à noter que C # 7.0 est en train d’obtenir une correspondance de motif (ce qui, à bien des égards, est un test de type sur stéroïdes) ne peut pas être si mauvais ...


0

Prenez un analyseur JSON générique. Le résultat d'une analyse réussie est un tableau, un dictionnaire, une chaîne, un nombre, un booléen ou une valeur null. Ce peut être n'importe lequel de ceux-là. Et les éléments d'un tableau ou les valeurs d'un dictionnaire peuvent à nouveau être l'un de ces types. Étant donné que les données proviennent de l'extérieur de votre programme, vous devez accepter tous les résultats (c'est-à-dire que vous devez les accepter sans planter; vous pouvez rejeter un résultat qui ne correspond pas à vos attentes).


Certains désérialiseurs JSON tenteront d'instancier une arborescence d'objets si votre structure contient des informations de type pour des "champs d'inférence". Mais ouais. Je pense que c'est dans ce sens que la réponse de Karl B s'est dirigée.
svidgen
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.