J'ai lu que lorsque votre programme a besoin de savoir de quelle classe est un objet, il indique généralement un défaut de conception, donc je veux savoir quelle est la bonne pratique pour gérer cela. J'implémente une classe Shape avec différentes sous-classes héritées d'elle comme Circle, Polygon ou Rectangle et j'ai différents algorithmes pour savoir si un cercle entre en collision avec un polygone ou un rectangle. Supposons ensuite que nous ayons deux instances de Shape et que nous voulons savoir si l'une entre en collision avec l'autre, dans cette méthode, je dois déduire quel type de sous-classe est l'objet que je heurte afin de savoir quel algorithme je dois appeler, mais, c'est un mauvaise conception ou mauvaise pratique? C'est ainsi que je l'ai résolu.
abstract class Shape {
ShapeType getType();
bool collide(Shape other);
}
class Circle : Shape {
getType() { return Type.Circle; }
bool collide(Shape other) {
if(other.getType() == Type.Rect) {
collideCircleRect(this, (Rect) other);
} else if(other.getType() == Type.Polygon) {
collideCirclePolygon(this, (Polygon) other);
}
}
}
Ceci est un mauvais modèle de conception? Comment pourrais-je résoudre ce problème sans avoir à déduire les types de sous-classe?
la source
Shape
objets abstraits . Votre logique dépend des éléments internes d'autres objets, sauf si vous vérifiez la collision pour les points de délimitationbool collide(x, y)
(un sous-ensemble de points de contrôle peut être un bon compromis). Sinon, vous devez vérifier le type d'une manière ou d'une autre - s'il y a vraiment besoin d'abstractions, la production deCollision
types (pour les objets dans la zone de l'acteur actuel) devrait être la bonne approche.Réponses:
Polymorphisme
Tant que vous utilisez
getType()
ou quelque chose du genre, vous n'utilisez pas le polymorphisme.Je comprends que vous ayez besoin de savoir quel type vous avez. Mais tout travail que vous voudriez faire en sachant que cela devrait vraiment être poussé dans la classe. Ensuite, vous lui dites simplement quand le faire.
Ce principe s'appelle dire, ne demandez pas . Le suivre vous aide à ne pas diffuser de détails comme le type et à créer une logique qui agit sur eux. Faire cela transforme une classe à l'envers. Il est préférable de conserver ce comportement dans la classe afin qu'il puisse changer lorsque la classe change.
Encapsulation
Vous pouvez me dire qu'aucune autre forme ne sera jamais nécessaire, mais je ne vous crois pas et vous non plus.
Un bon effet de l'encapsulation suivante est qu'il est facile d'ajouter de nouveaux types car leurs détails ne sont pas répartis dans le code où ils apparaissent
if
et dans laswitch
logique. Le code d'un nouveau type doit tous être au même endroit.Un système de détection de collision de type ignorant
Permettez-moi de vous montrer comment je concevrais un système de détection de collision performant et fonctionnant avec n'importe quelle forme 2D sans se soucier du type.
Dis que tu étais censé dessiner ça. Semble simple. Ce sont tous des cercles. Il est tentant de créer une classe de cercle qui comprend les collisions. Le problème est que cela nous renvoie à une ligne de pensée qui s'effondre lorsque nous avons besoin de 1000 cercles.
Nous ne devrions pas penser aux cercles. Nous devrions penser aux pixels.
Et si je vous disais que le même code que vous utilisez pour dessiner ces gars-là est ce que vous pouvez utiliser pour détecter quand ils se touchent ou même ceux sur lesquels l'utilisateur clique.
Ici, j'ai dessiné chaque cercle avec une couleur unique (si vos yeux sont assez bons pour voir le contour noir, ignorez simplement cela). Cela signifie que chaque pixel de cette image cachée correspond à ce qui l'a attiré. Un hashmap s'en occupe bien. Vous pouvez réellement faire du polymorphisme de cette façon.
Cette image que vous n'avez jamais à montrer à l'utilisateur. Vous le créez avec le même code qui a dessiné le premier. Juste avec des couleurs différentes.
Lorsque l'utilisateur clique sur un cercle, je sais exactement quel cercle car un seul cercle est de cette couleur.
Lorsque je dessine un cercle au-dessus d'un autre, je peux rapidement lire chaque pixel que je suis sur le point d'écraser en les déversant dans un ensemble. Lorsque j'ai terminé les points de consigne pour chaque cercle avec lequel il est entré en collision, je n'ai plus qu'à les appeler une fois pour l'avertir de la collision.
Un nouveau type: les rectangles
Tout cela a été fait avec des cercles mais je vous demande: cela fonctionnerait-il différemment avec des rectangles?
Aucune connaissance du cercle ne s'est infiltrée dans le système de détection. Peu importe le rayon, la circonférence ou le point central. Il se soucie des pixels et des couleurs.
La seule partie de ce système de collision qui doit être enfoncée dans les formes individuelles est une couleur unique. En dehors de cela, les formes peuvent simplement penser à dessiner leurs formes. C'est ce qu'ils sont bons de toute façon.
Maintenant, lorsque vous écrivez la logique de collision, vous ne vous souciez pas du sous-type que vous avez. Vous lui dites d'entrer en collision et il vous indique ce qu'il a trouvé sous la forme qu'il prétend dessiner. Pas besoin de connaître le type. Et cela signifie que vous pouvez ajouter autant de sous-types que vous le souhaitez sans avoir à mettre à jour le code dans d'autres classes.
Choix d'implémentation
Vraiment, il n'a pas besoin d'être d'une couleur unique. Il pourrait s'agir de références d'objets réelles et enregistrer un niveau d'indirection. Mais ceux-ci ne seraient pas aussi beaux lorsqu'ils sont dessinés dans cette réponse.
Ceci n'est qu'un exemple d'implémentation. Il y en a certainement d'autres. Ce que cela était censé montrer, c'est que plus vous laissez ces sous-types de formes s'en tenir à leur seule responsabilité, mieux tout le système fonctionne. Il existe probablement des solutions plus rapides et moins gourmandes en mémoire, mais si elles m'obligent à diffuser les connaissances sur les sous-types, je serais réticent à les utiliser même avec les gains de performances. Je ne les utiliserais que si j'en avais clairement besoin.
Double expédition
Jusqu'à présent, j'ai complètement ignoré la double expédition . Je l'ai fait parce que je le pouvais. Tant que la logique de collision ne se soucie pas des deux types en collision, vous n'en avez pas besoin. Si vous n'en avez pas besoin, ne l'utilisez pas. Si vous pensez que vous pourriez en avoir besoin, remettez-le à plus tard. Cette attitude s'appelle YAGNI .
Si vous décidez que vous avez vraiment besoin de différents types de collisions, demandez-vous si n sous-types de forme ont vraiment besoin de n 2 types de collisions. Jusqu'à présent, j'ai travaillé très dur pour faciliter l'ajout d'un autre sous-type de forme. Je ne veux pas le gâcher avec une implémentation à double répartition qui oblige les cercles à savoir que des carrés existent.
Combien de types de collisions y a-t-il de toute façon? Un peu de spéculation (une chose dangereuse) invente des collisions élastiques (rebondissantes), inélastiques (collantes), énergétiques (explosives) et destructrices (damageuses). Il pourrait y en avoir plus mais si celui-ci est inférieur à n 2, il ne faut pas trop concevoir nos collisions.
Cela signifie que lorsque ma torpille frappe quelque chose qui accepte des dégâts, elle n'a pas à SAVOIR qu'elle a touché un vaisseau spatial. Il suffit de lui dire: "Ha ha! Vous avez subi 5 points de dégâts."
Les objets qui infligent des dégâts envoient des messages de dégâts aux objets qui acceptent les messages de dégâts. De cette façon, vous pouvez ajouter de nouvelles formes sans en parler aux autres formes. Vous finissez par vous propager autour de nouveaux types de collisions.
Le vaisseau spatial peut renvoyer à la torp "Ha ha! Vous avez pris 100 points de dégâts." ainsi que "Vous êtes maintenant collé à ma coque". Et le torp peut renvoyer "Eh bien, j'ai fini pour ainsi m'oublier".
À aucun moment ne sait exactement ce que chacun est. Ils savent juste se parler via une interface de collision.
Maintenant, bien sûr, la double répartition vous permet de contrôler les choses plus intimement que cela, mais voulez-vous vraiment cela ?
Si vous le faites, pensez au moins à faire une double répartition par le biais d'abstractions des types de collisions qu'une forme accepte et non sur la mise en œuvre réelle de la forme. En outre, le comportement de collision est quelque chose que vous pouvez injecter en tant que dépendance et déléguer à cette dépendance.
Performance
La performance est toujours critique. Mais cela ne signifie pas que c'est toujours un problème. Testez les performances. Ne vous contentez pas de spéculer. Sacrifier tout le reste au nom de la performance ne conduit généralement pas à un code de perforation de toute façon.
la source
La description du problème semble que vous devez utiliser des méthodes multimédias (alias Multiple dispatch), dans ce cas particulier - Double dispatch . La première réponse est allée longuement sur la façon de traiter de manière générique les formes en collision dans le rendu raster, mais je pense qu'OP voulait une solution "vectorielle" ou peut-être que le problème entier a été reformulé en termes de formes, qui est un exemple classique dans les explications de la POO.
Même l'article de Wikipédia cité utilise la même métaphore de collision, permettez-moi de citer (Python n'a pas de multiméthodes intégrées comme certains autres langages):
Donc, la question suivante est de savoir comment obtenir la prise en charge des multiméthodes dans votre langage de programmation.
la source
Ce problème nécessite une refonte à deux niveaux.
Tout d'abord, vous devez extraire la logique de détection de la collision entre les formes hors des formes. Ainsi, vous ne violeriez pas l' OCP chaque fois que vous devez ajouter une nouvelle forme au modèle. Imaginez que vous avez déjà défini Circle, Square et Rectangle. Vous pourriez alors le faire comme ceci:
Ensuite, vous devez organiser la méthode appropriée à appeler en fonction de la forme qui l'appelle. Vous pouvez le faire en utilisant le polymorphisme et le modèle de visiteur . Pour ce faire, nous devons avoir en place le modèle d'objet approprié. Tout d'abord, toutes les formes doivent adhérer à la même interface:
Ensuite, nous devons avoir une classe de visiteurs parents:
J'utilise une classe ici au lieu de l'interface, car j'ai besoin que chaque objet visiteur ait un attribut de
ShapeCollisionDetector
type.Chaque implémentation d'
IShape
interface instanciera le visiteur approprié et appellera laAccept
méthode appropriée de l'objet avec lequel l'objet appelant interagit, comme ceci:Et des visiteurs spécifiques ressembleraient à ceci:
De cette façon, vous n'avez pas besoin de modifier les classes de formes à chaque fois que vous ajoutez une nouvelle forme, et vous n'avez pas besoin de vérifier le type de forme pour appeler la méthode de détection de collision appropriée.
Un inconvénient de cette solution est que si vous ajoutez une nouvelle forme, vous devez étendre la classe ShapeVisitor avec la méthode pour cette forme (par exemple
VisitTriangle(Triangle triangle)
), et par conséquent, vous devrez implémenter cette méthode dans tous les autres visiteurs. Cependant, comme il s'agit d'une extension, dans le sens où aucune méthode existante n'est modifiée, mais seulement de nouvelles sont ajoutées, cela ne viole pas OCP et la surcharge de code est minime. En outre, en utilisant la classeShapeCollisionDetector
, vous évitez la violation de SRP et vous évitez la redondance de code.la source
Votre problème de base est que dans la plupart des langages de programmation OO modernes, la surcharge de fonctions ne fonctionne pas avec la liaison dynamique (c'est-à-dire que le type des arguments de fonction est déterminé au moment de la compilation). Vous auriez besoin d'un appel de méthode virtuel virtuel sur deux objets plutôt que sur un seul. Ces méthodes sont appelées multi-méthodes . Cependant, il existe des moyens d' émuler ce comportement dans des langages comme Java, C ++, etc. C'est là que la double répartition est très pratique.
L'idée de base est que vous utilisez le polymorphisme deux fois. Lorsque deux formes entrent en collision, vous pouvez appeler la méthode de collision correcte de l'un des objets par polymorphisme et passer l'autre objet du type de forme générique. Dans la méthode appelée, vous savez alors si cet objet est un cercle, un rectangle ou autre. Vous appelez ensuite la méthode de collision sur l'objet de forme passé et lui passez l' objet this . Ce deuxième appel trouve ensuite à nouveau le type d'objet correct par polymorphisme.
Cependant, un gros inconvénient de cette technique est que chaque classe de la hiérarchie doit connaître tous les frères et sœurs. Cela impose une lourde charge de maintenance si une nouvelle forme est ajoutée ultérieurement.
la source
Ce n'est peut-être pas la meilleure façon d'aborder ce problème
La collision mathématique des formes est particulière aux combinaisons de formes. Cela signifie que le nombre de sous-routines dont vous aurez besoin est le carré du nombre de formes que votre système prend en charge. Les collisions de formes ne sont pas en fait des opérations sur des formes, mais des opérations qui prennent des formes comme paramètres.
Stratégie de surcharge de l'opérateur
Si vous ne pouvez pas simplifier le problème mathématique sous-jacent, je recommanderais l'approche de surcharge de l'opérateur. Quelque chose comme:
Sur l'intialiseur statique, j'utiliserais la réflexion pour faire une carte des méthodes pour implémenter un dispather diynamique sur la méthode de collision générique (Shape s1, Shape s2). L'initialiseur statique peut également avoir une logique pour détecter les fonctions de collision manquantes et les signaler, refusant de charger la classe.
C'est un peu similaire à la surcharge de l'opérateur C ++. En C ++, la surcharge de l'opérateur est très déroutante car vous disposez d'un ensemble fixe de symboles que vous pouvez surcharger. Cependant, le concept est très intéressant et peut être reproduit avec des fonctions statiques.
La raison pour laquelle j'utiliserais cette approche est que la collision n'est pas une opération sur un objet. Une collision est une opération externe qui indique une relation entre deux objets arbitraires. De plus, l'initialiseur statique pourra vérifier si je manque une fonction de collision.
Simplifiez votre problème mathématique si possible
Comme je l'ai mentionné, le nombre de fonctions de collision est le carré du nombre de types de formes. Cela signifie que dans un système avec seulement 20 formes, vous aurez besoin de 400 routines, avec 21 formes 441 et ainsi de suite. Ce n'est pas facilement extensible.
Mais vous pouvez simplifier vos calculs . Au lieu d'étendre la fonction de collision, vous pouvez pixelliser ou trianguler chaque forme. De cette façon, le moteur de collision n'a pas besoin d'être extensible. La collision, la distance, l'intersection, la fusion et plusieurs autres fonctions seront universelles.
Trianguler
Avez-vous remarqué que la plupart des packages et jeux 3D triangulent tout? C'est l'une des formes de simplification des mathématiques. Cela s'applique également aux formes 2D. Les polys peuvent être triangulés. Les cercles et les splines peuvent être approximés en poligones.
Encore une fois ... vous aurez une seule fonction de collision. Votre classe devient alors:
Et vos opérations:
N'est-ce pas plus simple?
Pixelliser
Vous pouvez pixelliser votre forme pour avoir une seule fonction de collision.
La pixellisation peut sembler être une solution radicale, mais peut être abordable et rapide selon la précision de vos collisions de formes. S'ils n'ont pas besoin d'être précis (comme dans un jeu), vous pouvez avoir des bitmaps basse résolution. La plupart des applications n'ont pas besoin d'une précision absolue sur les mathématiques.
Les approximations peuvent être suffisantes. Le supercalculateur ANTON pour la simulation de la biologie en est un exemple. Ses calculs ignorent de nombreux effets quantiques difficiles à calculer et jusqu'à présent, les simulations effectuées sont cohérentes avec les expériences faites dans le monde réel. Les modèles d'infographie PBR utilisés dans les moteurs de jeu et les packages de rendu apportent des simplifications qui réduisent la puissance informatique nécessaire pour rendre chaque image. N'est pas réellement précis sur le plan physique, mais est suffisamment proche pour être convaincant à l'œil nu.
la source