¿Tendría sentido usar objetos (en lugar de tipos primitivos) para todo en C ++?

17

Durante un proyecto reciente en el que he estado trabajando, tuve que usar muchas funciones que se ven así:

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);

Así que quiero refactorizarlo para que se vea así:

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

Esto me parece significativamente más legible y seguro que el primer método. ¿Pero tiene sentido crear PixelCoordinatey PixelSizeclases? ¿O sería mejor usar solo std::pair<int,int>para cada uno? ¿Y tiene sentido tener una ZoomLevelclase, o debería usar una double?

Mi intuición detrás del uso de clases para todo se basa en estos supuestos:

  1. Si hay clases para todo, sería imposible pasar un ZoomLevellugar donde Weightse esperaba un objeto, por lo que sería más difícil proporcionar argumentos incorrectos para una función
  2. Del mismo modo, algunas operaciones ilegales causarían errores de compilación, como agregar una GPSCoordinatea ZoomLevelotraGPSCoordinate
  3. Las operaciones legales serán fáciles de representar y seguras. es decir, restar dos GPSCoordinates produciría unGPSDisplacement

Sin embargo, la mayoría del código de C ++ que he visto usa muchos tipos primitivos, e imagino que debe haber una buena razón para eso. ¿Es una buena idea usar objetos para algo, o tiene inconvenientes que desconozco?

zackg
fuente
8
"La mayoría del código de C ++ que he visto usa muchos tipos primitivos". Dos pensamientos me vienen a la mente: muchos programadores familiarizados con C, que tienen una abstracción más débil, pueden haber escrito mucho código de C ++. también Ley de Sturgeon
congusbongus
3
Y creo que es porque los tipos primitivos son simples, directos, rápidos, elegantes y eficientes. Ahora, en el ejemplo del OP, definitivamente es una buena idea introducir algunas clases nuevas. ¡Pero la respuesta a la pregunta del título (donde dice "todo") es un rotundo no!
Sr. Lister
1
@Max typedeffing the doubles no hace absolutamente nada para resolver el problema del OP.
Sr. Lister
1
@CongXu: Tenía casi el mismo pensamiento, pero más en el sentido de que "un gran código en cualquier idioma puede haber sido escrito por programadores que tienen problemas para construir abstracciones".
Doc Brown
1
Como dice el refrán "si tiene una función con 17 parámetros, probablemente omitió algunos".
Joris Timmermans

Respuestas:

24

Sí definitivamente. Las funciones / métodos que toman demasiados argumentos son un olor a código e indican al menos uno de los siguientes:

  1. La función / método está haciendo demasiadas cosas a la vez
  2. La función / método requiere acceso a tantas cosas porque es preguntar, no decir o violar alguna ley de diseño OO
  3. Los argumentos están en realidad estrechamente relacionados.

Si el último es el caso (y su ejemplo ciertamente lo sugiere), es hora de hacer algo de esa "abstracción" de pantalones elegantes de la que hablaban los chicos geniales, oh, hace solo unas décadas. De hecho, iría más allá de su ejemplo y haría algo como:

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

(No estoy familiarizado con el GPS, por lo que probablemente no sea una abstracción correcta; por ejemplo, no entiendo qué tiene que ver el zoom con una función llamada getGPS).

Prefiere clases como PixelCoordinateover std::pair<T, T>, incluso si las dos tienen los mismos datos. Agrega valor semántico, más un poco de seguridad de tipo adicional (el compilador evitará que pase un ScreenCoordinatea PixelCoordinateaunque ambos sean pairs.

congusbongus
fuente
1
por no hablar de que puede pasar las estructuras más grandes como referencia con ese poco de micro-optimización de todos los niños frescos están tratando de hacer, pero en realidad no debería
monstruo de trinquete
6

Descargo de responsabilidad: prefiero C a C ++

En lugar de clases, usaría estructuras. Este punto es pedante en el mejor de los casos, ya que las estructuras y las clases son casi idénticas (vea aquí un gráfico simple, o esta pregunta SO ), pero creo que hay un mérito en la diferenciación.

Los programadores de C están familiarizados con las estructuras como bloques de datos que tienen un mayor significado cuando se agrupan, mientras que cuando veo un clasas, supongo que hay algún estado interno y métodos para manipular ese estado. Una coordenada GPS no tiene estado, solo datos.

Según su pregunta, parece que esto es exactamente lo que está buscando, un contenedor general, no una estructura con estado. Como los otros han mencionado, no me molestaría en poner Zoom en una clase propia; Personalmente odio las clases creadas para contener un tipo de datos (a mis profesores les encanta crear Heighty las Idclases).

Si hay clases para todo, sería imposible pasar un ZoomLevel donde se esperaba un objeto Weight, por lo que sería más difícil proporcionar argumentos incorrectos para una función

No creo que esto sea un problema. Si reduce la cantidad de parámetros al agrupar las cosas obvias como ha hecho, los encabezados deberían ser suficientes para evitar esos problemas. Si está realmente preocupado, separe las primitivas con una estructura, que debería proporcionarle errores de compilación para errores comunes. Es fácil intercambiar dos parámetros uno al lado del otro, pero más difícil si están más separados.

Del mismo modo, algunas operaciones ilegales causarían errores de compilación, como agregar un GPSCoordinate a un ZoomLevel u otro GPSCoordinate

Buen uso de sobrecargas del operador.

Las operaciones legales serán fáciles de representar y seguras. es decir, restar dos coordenadas GPS produciría un desplazamiento GPS

También buen uso de sobrecargas del operador.

Creo que estás en el camino correcto. Otra cosa a considerar es cómo encaja esto en el resto del programa.

beatgammit
fuente
3

El uso de tipos dedicados tiene la ventaja de que es mucho más difícil arruinar el orden de los argumentos. La primera versión de su función de ejemplo, por ejemplo, comienza con una cadena de 9 dobles. Cuando alguien usa esta función, es muy fácil atornillar el orden y pasar los ángulos del cardán en lugar de las coordenadas GPS y viceversa. Esto puede conducir a errores muy extraños y difíciles de rastrear. Cuando pasa solo tres argumentos con diferentes tipos, el compilador puede detectar este error.

De hecho, consideraría ir un paso más allá e introducir tipos que sean técnicamente idénticos pero que tengan un significado semántico diferente. Por ejemplo teniendo una clasePlaneAngle3d y una clase separada GimbalAngle3d, a pesar de que ambas son una colección de tres dobles. Esto haría imposible usar uno como el otro, a menos que sea intencional.

Recomendaría usar typedefs, que son solo alias para tipos primitivos ( typedef double ZoomLevel) no solo por ese motivo, sino también por otro: fácilmente le permite cambiar el tipo más adelante. Un día, cuando tenga la idea de que el uso floatpodría ser tan bueno como doublepodría ser más eficiente en cuanto a memoria y CPU, solo tiene que cambiar esto en un solo lugar, y no en docenas.

Philipp
fuente
+1 para la sugerencia typedef. Te ofrece lo mejor de ambos mundos: tipos y objetos primitivos.
Karl Bielefeldt
No es más difícil cambiar el tipo de un miembro de clase que un typedef; o querías decir en comparación con los primitivos?
MaHuJa
2

Buenas respuestas aquí ya, pero hay una cosa que me gustaría agregar:

Usar PixelCoordinatey PixelSizeclase hace las cosas muy específicas. Un general Coordinateo Point2Dclase probablemente se adapte mejor a sus necesidades. A Point2Ddebería ser una clase que implemente las operaciones de punto / vector más comunes. Podría implementar tal clase incluso genéricamente ( Point2D<T>donde T puede ser intodouble o algo más).

Las operaciones de punto / vector 2D son tan comunes para muchas tareas de programación de geometría 2D que, según mi experiencia, tal decisión aumentará las posibilidades de reutilizar las operaciones 2D existentes. Se evitará la necesidad de volver a ponerlas en práctica para cada uno PixelCoordinate, GPSCoordinate"whatsover de" clase -Coordinar y otra vez.

Doc Brown
fuente
1
también se puede hacer struct PixelCoordinate:Point2D<int>si desea que la seguridad de tipos adecuada
trinquete monstruo
0

Así que quiero refactorizarlo para que se vea así:

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

Esto me parece significativamente más legible y seguro que el primer método.

Es [más legible]. También es una buena idea.

¿Pero tiene sentido crear las clases PixelCoordinate y PixelSize?

Si representan conceptos diferentes, tiene sentido (es decir, "sí").

¿O sería mejor simplemente usando std :: pair para cada uno?

Podrías comenzar haciendo typedef std::pair<int,int> PixelCoordinate, PixelSize;. De esta manera, cubre la semántica (está trabajando con coordenadas de píxeles y tamaños de píxeles, no con std :: pares en su código de cliente) y si necesita ampliar, simplemente reemplace typedef con una clase y su código de cliente debería requieren cambios mínimos.

¿Y tiene sentido tener una clase ZoomLevel, o debería usar una doble?

También podría escribirlo, pero usaría un doublehasta que necesite una funcionalidad asociada que no exista para un doble (por ejemplo, tener un ZoomLevel::defaultvalor requeriría que defina una clase, pero hasta que tenga algo como eso, mantenlo doble).

Mi intuición detrás del uso de clases para todo se basa en estos supuestos [omitidos por brevedad]

Son argumentos sólidos (siempre debe esforzarse por hacer que sus interfaces sean fáciles de usar correctamente y difíciles de usar incorrectamente).

Sin embargo, la mayoría del código de C ++ que he visto usa muchos tipos primitivos, e imagino que debe haber una buena razón para eso.

Hay razones; en muchos casos, sin embargo, esas razones no son buenas. Una razón válida para hacer esto puede ser el rendimiento, pero eso significa que debería ver este tipo de código como una excepción, no como una regla.

¿Es una buena idea usar objetos para algo, o tiene inconvenientes que desconozco?

En general, es una buena idea asegurarse de que si sus valores siempre aparecen juntos (y no tienen sentido aparte), los debe agrupar (por las mismas razones que mencionó en su publicación).

Es decir, si tiene coordenadas que siempre tienen x, y y z, entonces es mucho mejor tener una coordinateclase que transmitir tres parámetros a todas partes.

utnapistim
fuente