Serait-il sensé d'utiliser des objets (au lieu de types primitifs) pour tout en C ++?

17

Lors d'un récent projet sur lequel j'ai travaillé, j'ai dû utiliser beaucoup de fonctions qui ressemblent à ceci:

static bool getGPS(double plane_latitude, double plane_longitude, double plane_altitude,
                       double plane_roll, double plane_pitch, double plane_heading,
                       double gimbal_roll, double gimbal_pitch, double gimbal_yaw, 
                       int target_x, int target_y, double zoom,
                       int image_width_pixels, int image_height_pixels,
                       double & Target_Latitude, double & Target_Longitude, double & Target_Height);

Je veux donc le refactoriser pour qu'il ressemble à ceci:

static GPSCoordinate getGPS(GPSCoordinate plane, Angle3D planeAngle, Angle3D gimbalAngle,
                            PixelCoordinate target, ZoomLevel zoom, PixelSize imageSize)

Cela me semble beaucoup plus lisible et sûr que la première méthode. Mais est-il judicieux de créer PixelCoordinateet de PixelSizeclasser? Ou serais-je mieux d'utiliser simplement std::pair<int,int>pour chacun. Et est-il sensé d'avoir une ZoomLevelclasse, ou devrais-je simplement utiliser un double?

Mon intuition derrière l'utilisation de classes pour tout est basée sur ces hypothèses:

  1. S'il y a des classes pour tout, il serait impossible de passer un ZoomLeveldans où un Weightobjet était attendu, il serait donc plus difficile de fournir les mauvais arguments à une fonction
  2. De même, certaines opérations illégales entraîneraient des erreurs de compilation, telles que l'ajout d'un GPSCoordinateà un ZoomLevelou un autreGPSCoordinate
  3. Les opérations juridiques seront faciles à représenter et à sécuriser. c'est-à-dire que la soustraction de deux GPSCoordinates donnerait unGPSDisplacement

Cependant, la plupart du code C ++ que j'ai vu utilise beaucoup de types primitifs, et j'imagine qu'il doit y avoir une bonne raison à cela. Est-ce une bonne idée d'utiliser des objets pour quoi que ce soit, ou a-t-il des inconvénients que je ne connais pas?

zackg
la source
8
"la plupart du code C ++ que j'ai vu utilise beaucoup de types primitifs" - deux pensées me viennent à l'esprit: beaucoup de code C ++ peut avoir été écrit par des programmeurs familiers avec C qui a une abstraction plus faible; aussi Loi de Sturgeon
congusbongus
3
Et je pense que c'est parce que les types primitifs sont simples, directs, rapides, élégants et efficaces. Maintenant, dans l'exemple de l'OP, c'est certainement une bonne idée d'introduire de nouvelles classes. Mais la réponse à la question du titre (où il est dit "tout") est un non retentissant!
M. Lister
1
@Max typedeffing les doubles ne fait absolument rien pour résoudre le problème de l'OP.
M. Lister
1
@CongXu: J'avais presque la même pensée, mais plus dans le sens "beaucoup de code dans n'importe quel langage peut avoir été écrit par des programmeurs ayant des problèmes avec la construction d'abstractions".
Doc Brown
1
Comme le dit le dicton "si vous avez une fonction avec 17 paramètres, vous en avez probablement manqué quelques-uns".
Joris Timmermans

Réponses:

24

Oui définitivement. Les fonctions / méthodes qui prennent trop d'arguments sont une odeur de code et indiquent au moins l'un des éléments suivants:

  1. La fonction / méthode fait trop de choses à la fois
  2. La fonction / méthode nécessite l'accès à beaucoup de choses parce qu'elle demande, ne dit pas ou ne viole pas une loi de conception OO
  3. Les arguments sont en fait étroitement liés

Si le dernier est le cas (et votre exemple le suggère certainement), il est grand temps de faire une partie de cette "abstraction" de fantaisie dont les enfants cool parlaient oh, il y a seulement quelques décennies. En fait, j'irais plus loin que votre exemple et ferais quelque chose comme:

static GPSCoordinate getGPS(Plane plane, Angle3D gimbalAngle, GPSView view)

(Je ne connais pas le GPS, donc ce n'est probablement pas une abstraction correcte; par exemple, je ne comprends pas ce que le zoom a à voir avec une fonction appelée getGPS.)

Veuillez préférer les cours comme PixelCoordinateplus std::pair<T, T>, même si les deux ont les mêmes données. Il ajoute une valeur sémantique, plus un peu de sécurité supplémentaire (le compilateur vous empêchera de passer de a ScreenCoordinateà a PixelCoordinatemême si les deux sont pairs.

congusbongus
la source
1
sans oublier que vous pouvez passer les plus grandes structures par référence pour ce petit peu de micro-optimisation que tous les enfants cool essaient de faire, mais ne devraient vraiment pas
ratchet freak
6

Avertissement: je préfère le C au C ++

Au lieu de classes, j'utiliserais des structures. Ce point est au mieux pédant, car les structures et les classes sont presque identiques (voir ici pour un graphique simple, ou cette question SO ), mais je pense qu'il y a du mérite dans la différenciation.

Les programmeurs C connaissent les structures en tant que blocs de données qui ont une plus grande signification lorsqu'ils sont regroupés, alors que lorsque je vois des clasas, je suppose qu'il existe un état interne et des méthodes pour manipuler cet état. Une coordonnée GPS n'a pas d'état, juste des données.

D'après votre question, il semble que c'est exactement ce que vous recherchez, un conteneur général, pas une structure avec état. Comme les autres l'ont mentionné, je ne prendrais pas la peine de mettre Zoom dans une classe à part; Personnellement, je déteste les classes conçues pour contenir un type de données (mes professeurs adorent créer Heightet les Idclasses).

S'il y a des classes pour tout, il serait impossible de passer un ZoomLevel là où un objet Weight était attendu, il serait donc plus difficile de fournir les mauvais arguments à une fonction

Je ne pense pas que ce soit un problème. Si vous réduisez le nombre de paramètres en regroupant les choses évidentes comme vous l'avez fait, les en-têtes devraient être suffisants pour éviter ces problèmes. Si vous êtes vraiment inquiet, séparez les primitives par une structure, qui devrait vous donner des erreurs de compilation pour les erreurs courantes. Il est facile d'échanger deux paramètres l'un à côté de l'autre, mais plus difficile s'ils sont plus éloignés l'un de l'autre.

De même, certaines opérations illégales entraîneraient des erreurs de compilation, telles que l'ajout d'un GPSCoordinate à un ZoomLevel ou à un autre GPSCoordinate

Bonne utilisation des surcharges de l'opérateur.

Les opérations juridiques seront faciles à représenter et à sécuriser. c'est-à-dire que la soustraction de deux coordonnées GPS donnerait un déplacement GPS

Aussi bon usage des surcharges de l'opérateur.

Je pense que vous êtes sur la bonne voie. Une autre chose à considérer est de savoir comment cela s'intègre dans le reste du programme.

beatgammit
la source
3

L'utilisation de types dédiés a l'avantage qu'il est beaucoup plus difficile de bousiller l'ordre des arguments. La première version de votre exemple de fonction, par exemple, commence par une chaîne de 9 doubles. Lorsque quelqu'un utilise cette fonction, il est très facile de bousiller la commande et de passer les angles du cardan à la place des coordonnées GPS et vice versa. Cela peut conduire à des bogues très étranges et difficiles à tracer. Lorsque vous ne passez que trois arguments avec des types différents à la place, cette erreur peut être détectée par le compilateur.

J'envisagerais en fait d'aller plus loin et d'introduire des types techniquement identiques mais ayant une signification sémantique différente. Par exemple, en ayant une classe PlaneAngle3det une classe distincte GimbalAngle3d, même si les deux sont une collection de trois doubles. Cela rendrait impossible l'utilisation de l'un comme de l'autre, à moins qu'il ne soit intentionnel.

Je recommanderais d'utiliser des typedefs qui ne sont que des alias pour les types primitifs ( typedef double ZoomLevel) non seulement pour cette raison, mais aussi pour une autre: cela vous permet facilement de changer le type plus tard. Lorsque vous avez un jour l'idée que l'utilisation floatpourrait être aussi bonne que, doublemais pourrait être plus efficace en termes de mémoire et de CPU, il vous suffit de changer cela en un seul endroit, et non en des dizaines de fois.

Philipp
la source
+1 pour la suggestion typedef. Vous offre le meilleur des deux mondes: types et objets primitifs.
Karl Bielefeldt
Il n'est pas plus difficile de changer le type d'un membre de classe qu'un typedef; ou vouliez-vous dire par rapport aux primitifs?
MaHuJa
2

Bonnes réponses ici déjà, mais il y a une chose que je voudrais ajouter:

L'utilisation PixelCoordinateet la PixelSizeclasse rendent les choses très spécifiques. Un général Coordinateou une Point2Dclasse peut probablement mieux répondre à vos besoins. A Point2Ddoit être une classe implémentant les opérations ponctuelles / vectorielles les plus courantes. Vous pouvez implémenter une telle classe même de manière générique ( Point2D<T>où T peut être intou doubleautre chose).

Les opérations 2D ponctuelles / vectorielles sont si courantes pour de nombreuses tâches de programmation de géométrie 2D que, selon mon expérience, une telle décision augmentera fortement les chances de réutiliser les opérations 2D existantes. Vous éviterez ainsi la nécessité de les re-mise en œuvre pour chaque PixelCoordinate, GPSCoordinateencore et encore « whatsover » classe -Coordonner.

Doc Brown
la source
1
vous pouvez également le faire struct PixelCoordinate:Point2D<int>si vous voulez une sécurité de type appropriée
ratchet freak
0

Je veux donc le refactoriser pour qu'il ressemble à ceci:

static GPSCoordinate getGPS(GPSCoordinate plane, Angle3D planeAngle, Angle3D gimbalAngle,
                        PixelCoordinate target, ZoomLevel zoom, PixelSize imageSize)

Cela me semble beaucoup plus lisible et sûr que la première méthode.

C'est [plus lisible]. C'est aussi une bonne idée.

Mais est-il judicieux de créer des classes PixelCoordinate et PixelSize?

S'ils représentent des concepts différents, cela a du sens (c'est-à-dire «oui»).

Ou serais-je mieux en utilisant simplement std :: pair pour chacun.

Vous pourriez commencer par faire typedef std::pair<int,int> PixelCoordinate, PixelSize;. De cette façon, vous couvrez la sémantique (vous travaillez avec des coordonnées et des tailles de pixels, pas avec des paires std :: dans votre code client) et si vous avez besoin d'étendre, vous remplacez simplement le typedef par une classe et votre code client devrait nécessitent des changements minimes.

Et est-il logique d'avoir une classe ZoomLevel, ou dois-je simplement utiliser un double?

Vous pouvez également le taper, mais j'utiliserais un doublejusqu'à ce que j'aie besoin de fonctionnalités associées qui n'existent pas pour un double (par exemple, avoir une ZoomLevel::defaultvaleur nécessiterait que vous définissiez une classe, mais jusqu'à ce que vous ayez quelque chose comme ça, gardez le double).

Mon intuition derrière l'utilisation de classes pour tout est basée sur ces hypothèses [omis pour plus de brièveté]

Ce sont des arguments solides (vous devez toujours vous efforcer de rendre vos interfaces faciles à utiliser correctement et difficiles à utiliser incorrectement).

Cependant, la plupart du code C ++ que j'ai vu utilise beaucoup de types primitifs, et j'imagine qu'il doit y avoir une bonne raison à cela.

Il y a des raisons; dans de nombreux cas cependant, ces raisons ne sont pas bonnes. Les performances peuvent être une raison valable pour cela, mais cela signifie que vous devez voir ce type de code comme une exception, pas comme une règle.

Est-ce une bonne idée d'utiliser des objets pour quoi que ce soit, ou a-t-il des inconvénients que je ne connais pas?

C'est généralement une bonne idée de vous assurer que si vos valeurs apparaissent toujours ensemble (et n'ont aucun sens séparément), vous devez les regrouper (pour les mêmes raisons que vous avez mentionnées dans votre message).

Autrement dit, si vous avez des coordonnées qui ont toujours x, y et z, alors il vaut mieux avoir une coordinateclasse que de transmettre trois paramètres partout.

utnapistim
la source