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 PixelCoordinate
y PixelSize
clases? ¿O sería mejor usar solo std::pair<int,int>
para cada uno? ¿Y tiene sentido tener una ZoomLevel
clase, o debería usar una double
?
Mi intuición detrás del uso de clases para todo se basa en estos supuestos:
- Si hay clases para todo, sería imposible pasar un
ZoomLevel
lugar dondeWeight
se esperaba un objeto, por lo que sería más difícil proporcionar argumentos incorrectos para una función - Del mismo modo, algunas operaciones ilegales causarían errores de compilación, como agregar una
GPSCoordinate
aZoomLevel
otraGPSCoordinate
- Las operaciones legales serán fáciles de representar y seguras. es decir, restar dos
GPSCoordinate
s 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?
Respuestas:
Sí definitivamente. Las funciones / métodos que toman demasiados argumentos son un olor a código e indican al menos uno de los siguientes:
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:
(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
PixelCoordinate
overstd::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 unScreenCoordinate
aPixelCoordinate
aunque ambos seanpair
s.fuente
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
Height
y lasId
clases).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.
Buen uso de sobrecargas del operador.
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.
fuente
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 clase
PlaneAngle3d
y una clase separadaGimbalAngle3d
, 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 usofloat
podría ser tan bueno comodouble
podría ser más eficiente en cuanto a memoria y CPU, solo tiene que cambiar esto en un solo lugar, y no en docenas.fuente
Buenas respuestas aquí ya, pero hay una cosa que me gustaría agregar:
Usar
PixelCoordinate
yPixelSize
clase hace las cosas muy específicas. Un generalCoordinate
oPoint2D
clase probablemente se adapte mejor a sus necesidades. APoint2D
deberí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 serint
odouble
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.fuente
struct PixelCoordinate:Point2D<int>
si desea que la seguridad de tipos adecuadaEs [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í").
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.También podría escribirlo, pero usaría un
double
hasta que necesite una funcionalidad asociada que no exista para un doble (por ejemplo, tener unZoomLevel::default
valor requeriría que defina una clase, pero hasta que tenga algo como eso, mantenlo doble).Son argumentos sólidos (siempre debe esforzarse por hacer que sus interfaces sean fáciles de usar correctamente y difíciles de usar incorrectamente).
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.
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
coordinate
clase que transmitir tres parámetros a todas partes.fuente