Deux structures avec les mêmes membres mais avec des noms différents, est-ce une bonne idée?

49

J'écris un programme qui implique de travailler à la fois avec des coordonnées polaires et cartésiennes.

Est-il judicieux de créer deux structures différentes pour chaque type de points, l’un avec Xet les Ymembres et l’autre avec Ret les Thetamembres.

Ou est-ce trop et il vaut mieux avoir une seule structure avec firstet en secondtant que membres.

Ce que j'écris est simple et cela ne changera pas beaucoup. Mais je suis curieux de savoir ce qui est mieux du point de vue de la conception.

Je pense que la première option est meilleure. Cela semble plus lisible et je bénéficierai d’une vérification de type.

Moha le tout-puissant chameau
la source
11
Je crée toujours une nouvelle struct / classe par but. Besoin d'un vecteur 3D, créez un struct 3d_vector avec trois flottants. Si vous avez besoin d’une représentation uvw, créez une structure texture_coords avec trois floats. Besoin d'une position dans un environnement 3D, créez une position de structure avec trois flotteurs. Tu obtiens le point. Cela permet une bien meilleure lisibilité que d'utiliser la même chose partout. Si vous êtes dérangé par la définition de la même chose plusieurs fois, utilisez une structure de base 3-float et définissez plusieurs noms pour qu'ils soient identiques.
Kevin
S'ils ont des méthodes communes, alors peut-être une. Auriez-vous jamais besoin de comparer l'égalité des deux?
paparazzo
8
@ Sidney À moins que vous n'ayez absolument besoin de cette fonctionnalité. Vous devez effectuer une opération sin / arcsin pour effectuer la conversion entre les deux représentations. Cela va introduire un peu de bitrot dans les bits les moins significatifs chaque fois que vous effectuez la conversion. Je suis presque sûr que vous finiriez avec la même douleur que ce que j'ai vécu en essayant de traiter avec une classe qui fournissait à la fois une fréquence d'événements et une durée entre les événements (x et 1 / x). Je ne voudrais pas recommencer à chercher quelle représentation est canonique dans la classe et à gérer tous les maux de tête qui s’arrondissent.
Dan Neely
3
Un bon exemple de type de données pouvant représenter beaucoup de choses est la chaîne, mais "stringy typed" est un anti-modèle. Wrt votre exemple. Essayez d'implémenter le produit scalaire pour un type prenant en charge les deux systèmes de coordonnées.
Nathan Cooper
1
Vous devez vous efforcer d’utiliser des types différents, le cas échéant, pour tirer parti des avantages de la sécurité des types - cela vous évitera d’envoyer un type incorrect à / depuis une fonction (en utilisant la vérification de l’exactitude de la compilation, en fonction de votre langage de programmation). Cela facilitera la maintenance car vous pouvez trouver toutes les vraies utilisations de certains types (sans obtenir des utilisations erronées en raison de la fusion des types).
Erik Eidt

Réponses:

17

J'ai vu les deux solutions, donc cela dépend vraiment du contexte.

Pour des raisons de lisibilité, avoir plusieurs structures comme vous le suggérez est très efficace. Toutefois, dans certains environnements, vous souhaitez effectuer des manipulations courantes sur ces structures et vous vous retrouvez en train de dupliquer du code, tel que des opérations matrix * vector. Cela peut être frustrant lorsque l'opération en question n'est pas disponible dans votre type de vecteur car personne ne l'a transférée ici.

La solution extrême (que nous avons finalement retenue) consiste à avoir une classe de base basée sur un modèle CRTP dont les fonctions obtiennent <0> (), obtenant <1> () et obtenant <2> (), pour obtenir les éléments de manière générique. Ces fonctions sont ensuite définies dans la structure cartésienne ou polaire dérivée de cette classe de base. Cela résout tous les problèmes, mais a un prix assez dérisoire: avoir à apprendre la métaprogrammation des templates. Cependant, si la métaprogrammation des modèles est déjà un jeu équitable pour votre projet, cela pourrait être un bon match.

Cort Ammon
la source
1
Votre réponse est très intéressante. est-il possible de donner un exemple?
Moha le tout-puissant chameau
1
Je peux citer un exemple de ce que j'ai fait: polaires de filtrage FIR, cartésiens et leurs vecteurs. Les calculs étaient assez similaires, sans angle de (dé) wrapping, certaines parties du code étaient dupliquées de toute façon pour des raisons de performances et nous avons utilisé des modèles pour lesquels ils étaient identiques. Utilisé des noms différents pour toutes les choses. La "solution extrême" de Cort aurait pu éviter quelques duplications, mais pas toutes.
Eugene Ryabtsev
1
Ma première réaction a été qu'une telle situation serait mieux résolue par un casting, mais cela s'avère un peu risqué .
200_success
114

Oui, cela fait beaucoup de sens.

La valeur d'une structure ne réside pas simplement dans le fait qu'elle encapsule les données sous un nom pratique. La valeur est que cela codifie vos intentions afin que le compilateur puisse vous aider à vérifier que vous ne les violez pas un jour (par exemple, confondez un jeu de coordonnées polaires avec un jeu de coordonnées cartésiennes).

Les gens ont de la peine à se souvenir de ces détails insidieux, mais ils sont bons à créer des projets audacieux et inventifs. Les ordinateurs sont bons pour les détails et mauvais pour les projets créatifs. Par conséquent, il est toujours judicieux de confier autant de détails de maintenance insignifiants à l'ordinateur, tout en laissant l'esprit libre de travailler sur le grand projet.

Kilian Foth
la source
6
+1 Une description parfaite de l'utilisation de l'ordinateur pour faire ce que les ordinateurs font très bien et de laisser votre cerveau se concentrer sur votre propre travail.
BrianH
8
"Les programmes doivent être écrits pour que les gens puissent les lire et, accessoirement, pour que les machines fonctionnent." - de "Structure et interprétation des programmes informatiques" de Abelson et Sussman.
hlovdal
18

Oui, bien que cartésien et polaire soient (à leur place) des schémas de représentation coordonnée éminemment sensés, ils ne devraient idéalement jamais être mélangés (si vous avez un point cartésien {1,1}, il est très différent de Polar {1,1 }).

En fonction de vos besoins, il peut également être utile de mettre en œuvre une Coordonnée interface, avec des méthodes comme X(), Y(), Displacement()et Angle()(ou peut - être Radius()et Theta(), en fonction).

Vatine
la source
Cela est encore plus important si le PO crée des classes à partir de ces structures, car les opérations sur les coordonnées cartésiennes et polaires sont différentes.
Mindwin
1
+1 pour le dernier paragraphe, c'est la solution idéale. Un point est l'espace est un objet; la représentation interne de ce point ne devrait pas avoir d'importance. Bien entendu, les préoccupations du monde réel (performances, erreurs d’arrondi) pourraient l’avoir. Tout dépend de quoi cela est utilisé.
BlueRaja - Danny Pflughoeft
Et aussi, pour cet exemple, il est peu probable que cela change, mais s’il s’agissait de 2 autres classes, rien ne vous dit qu’elles pourraient diverger à certains moments.
Dyesdyes
8

En fin de compte, le but de la programmation est de basculer les bits de transistor pour effectuer un travail utile. Mais penser à un niveau aussi bas conduirait à une folie ingérable, ce qui explique pourquoi il existe des langages de programmation de niveau supérieur pour vous aider à masquer cette complexité.

Si vous ne faites qu'une structure avec des membres nommés firstet second, alors les noms ne veulent rien dire; vous les traiteriez essentiellement comme des adresses de mémoire. Cela va à l'encontre de l'objectif du langage de programmation de haut niveau.

En outre, le fait qu’ils soient tous représentables doublene signifie pas que vous pouvez les utiliser de façon interchangeable. Par exemple, θ est un angle sans dimension, alors que y a des unités de longueur. Comme les types ne sont pas logiquement substituables, ils doivent constituer deux structures incompatibles.

Si vous avez vraiment besoin de jouer à des astuces de réutilisation de la mémoire - et vous ne devriez certainement pas le faire - vous pouvez utiliser un uniontype en C pour préciser votre intention.

200_success
la source
Meilleure réponse, IMHO
Dean Radcliffe
2

Premièrement, ayez les deux explicitement, selon la réponse tout à fait saine de @ Kilian-foth.

Cependant, j'aimerais ajouter:

Posez la question suivante: Avez-vous vraiment des opérations qui sont génériques aux deux quand on les considère par paires double? Notez que ceci ne signifie pas que vous avez des opérations qui s’appliquent aux deux dans leurs propres termes. Par exemple, 'plot (Coord)' se soucie de savoir s'il Coordest polaire ou cartésien. De l'autre côté, continuer à archiver ne traite que les données telles quelles. Si vous avez vraiment des opérations génériques, envisager de définir soit une classe de base, ou la définition d' un convertisseur à un std::pair<double, double>ou tupleou tout ce que vous avez dans votre langue.

En outre, une approche pourrait consister à traiter un type de coordonnées comme plus fondamental et l’autre comme un simple support pour une interaction utilisateur ou externe.

Ainsi , vous pouvez assurer que toutes les opérations de base sont codés Cartesianet fournir un soutien pour la conversion Polarà Cartesian. Cela évite d'implémenter différentes versions de nombreuses opérations.

Keith
la source
1

Une solution possible, en fonction du langage et si vous savez que les deux classes auront des méthodes et des opérations similaires, serait de définir la classe une fois et d’utiliser des alias de types afin de nommer explicitement les types différemment.

Cela a également l’avantage que tant que les classes sont exactement les mêmes, vous ne pouvez en conserver qu’une, mais dès que vous devez les modifier, vous n’avez pas besoin de modifier le code en les utilisant, car les types ont déjà été utilisés. Distincly.

Une autre option, qui dépend encore une fois de l'utilisation des classes (si vous avez besoin de polymorphisme ou autre), consiste à utiliser l'héritage public vers les deux nouveaux types, afin qu'ils aient la même interface publique que le type commun qu'ils représentent. Cela permet également une évolution séparée des types.

Svalorzen
la source
Les noms des membres des deux classes ne sont pas les mêmes. en fait, les noms sont la seule différence entre les deux classes
Moha, le tout-puissant chameau
@ Mhd.Tahawi Vous pouvez implémenter les getters et les setters avec les noms appropriés, en vous assurant que la classe que vous utilisez est la même, mais en fournissant les noms appropriés pour les opérations que vous souhaitez utiliser. Cela devient un peu plus détaillé, mais vous devrez dupliquer moins de code.
Svalorzen
0

Je pense qu'avoir le même nom de membre est une mauvaise idée dans ce cas, car cela rend le code plus sujet aux erreurs.

Imaginez le scénario: vous avez deux points cartésiens: pntA et pntB. Ensuite, vous décidez, pour une raison quelconque, de les représenter mieux en coordonnées polaires, puis de modifier la déclaration et le constructeur.

Maintenant, si toutes vos opérations étaient juste des appels de méthodes comme:

double distance = pntA.distanceFrom(pntB);

alors tu vas bien. Mais si vous utilisiez les membres explicitement? Comparer

double leftMargin = abs(pntA.x - pntB.x);
double leftMargin = abs(pntA.first - pntB.first);

Dans le premier cas, le code ne sera pas compilé. Vous verrez immédiatement l'erreur et pourrez y remédier. Mais si vous avez les mêmes noms de membres, l'erreur ne sera que sur le plan logique, beaucoup plus difficile à détecter.

Si vous écrivez dans un langage non orienté objet, il est encore plus facile de transmettre une mauvaise structure à la fonction. Qu'est-ce qui vous empêche d'écrire le code suivant?

double distance = calculate_distance_polar(cartesianPointA, polarPointB);

D'autre part, différents types de données vous permettraient de rechercher l'erreur lors de la compilation.

IMil
la source