Podczas ostatniego projektu, nad którym pracowałem, musiałem użyć wielu funkcji, które wyglądają tak:
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);
Chcę go zmienić tak, aby wyglądał mniej więcej tak:
static GPSCoordinate getGPS(GPSCoordinate plane, Angle3D planeAngle, Angle3D gimbalAngle,
PixelCoordinate target, ZoomLevel zoom, PixelSize imageSize)
Wydaje mi się, że jest znacznie bardziej czytelny i bezpieczny niż pierwsza metoda. Ale czy tworzenie PixelCoordinate
i PixelSize
klasy mają sens ? A może lepiej byłoby po prostu użyć std::pair<int,int>
dla każdego. I czy warto mieć ZoomLevel
klasę, czy powinienem po prostu użyć double
?
Moja intuicja dotycząca używania klas do wszystkiego opiera się na następujących założeniach:
- Jeśli istnieją klasy dla wszystkiego, nie byłoby możliwe podanie
ZoomLevel
w miejscu, w którymWeight
oczekiwany był obiekt, więc trudniej byłoby podać niewłaściwe argumenty funkcji - Podobnie, niektóre nielegalne operacje spowodowałyby błędy kompilacji, takie jak dodanie a
GPSCoordinate
doZoomLevel
innegoGPSCoordinate
- Czynności prawne będą łatwe do reprezentowania i bezpieczne. tzn. odjęcie dwóch
GPSCoordinate
s dałoby aGPSDisplacement
Jednak większość kodu C ++, który widziałem, używa wielu prymitywnych typów i myślę, że musi to mieć dobry powód. Czy warto używać obiektów do czegokolwiek, czy ma wady, których nie jestem świadomy?
Odpowiedzi:
Tak, zdecydowanie. Funkcje / metody, które pobierają zbyt wiele argumentów, mają zapach kodu i wskazują co najmniej jedną z następujących czynności:
Jeśli tak jest w przypadku ostatniego (a twój przykład na pewno to sugeruje), najwyższy czas zrobić trochę tej abstrakcyjnej fantazji, o której wspominali fajne dzieciaki kilka dekad temu. W rzeczywistości poszedłbym dalej niż twój przykład i zrobiłbym coś takiego:
(Nie znam GPS, więc prawdopodobnie nie jest to poprawna abstrakcja; na przykład nie rozumiem, co zoom ma wspólnego z wywoływaną funkcją
getGPS
).Proszę wolą zajęcia jak
PixelCoordinate
powyżejstd::pair<T, T>
, nawet jeśli oba mają te same dane. Dodaje wartość semantyczną oraz odrobinę dodatkowego bezpieczeństwa typu (kompilator powstrzyma cię od przekazaniaScreenCoordinate
a,PixelCoordinate
nawet jeśli oba sąpair
s.źródło
Uwaga: Wolę C niż C ++
Zamiast klas używałbym struktur. Ta kwestia jest co najwyżej pedantyczna, ponieważ struktury i klasy są prawie identyczne (zobacz tutaj prosty wykres lub to pytanie SO ), ale myślę, że różnicowanie ma sens.
Programiści C znają struktury jako bloki danych, które mają większe znaczenie po zgrupowaniu, natomiast gdy widzę klasy, zakładam, że istnieje pewien stan wewnętrzny i metody manipulowania tym stanem. Współrzędna GPS nie ma stanu, tylko dane.
Z twojego pytania wynika, że jest to dokładnie to, czego szukasz, ogólny kontener, a nie struktura stanowa. Jak wspomnieli inni, nie zawracałbym sobie głowy umieszczeniem Zoomu we własnej klasie; Ja osobiście nienawidzę klas zbudowanych na typ danych (moi profesorowie uwielbiają tworzyć
Height
iId
klasy).Nie sądzę, że to jest problem. Jeśli zmniejszysz liczbę parametrów poprzez pogrupowanie oczywistych rzeczy, które zrobiłeś, nagłówki powinny wystarczyć, aby zapobiec tym problemom. Jeśli naprawdę się martwisz, oddziel prymitywy strukturą, która powinna dać ci błędy kompilacji dla typowych błędów. Łatwo jest zamienić dwa parametry obok siebie, ale trudniej, jeśli są bardziej oddalone od siebie.
Dobre wykorzystanie przeciążeń operatora.
Również dobre wykorzystanie przeciążeń operatora.
Myślę, że jesteś na dobrej drodze. Inną rzeczą do rozważenia jest to, jak pasuje to do reszty programu.
źródło
Korzystanie z typów dedykowanych ma tę zaletę, że znacznie trudniej jest sprecyzować kolejność argumentów. Na przykład pierwsza wersja przykładowej funkcji zaczyna się łańcuchem 9 podwójnych. Gdy ktoś korzysta z tej funkcji, bardzo łatwo jest zepsuć kolejność i podać kąty gimbala zamiast współrzędnych GPS i odwrotnie. Może to prowadzić do bardzo dziwnych i trudnych do wyśledzenia błędów. Jeśli zamiast tego przekażesz tylko trzy argumenty różnych typów, ten błąd może zostać przechwycony przez kompilator.
W rzeczywistości rozważę pójście o krok dalej i wprowadzenie typów, które są technicznie identyczne, ale mają inne znaczenie semantyczne. Na przykład posiadając klasę
PlaneAngle3d
i oddzielną klasęGimbalAngle3d
, mimo że obie są zbiorem trzech podwójnych. Uniemożliwiłoby to użycie jednego jako drugiego, chyba że jest to celowe.Poleciłbym użycie typedefs, które są tylko aliasami dla typów pierwotnych (
typedef double ZoomLevel
) nie tylko z tego powodu, ale także z innego powodu: Pozwala to łatwo zmienić typ później. Kiedy pewnego dnia pojawi się pomysł, że używaniefloat
może być równie dobre,double
ale może być bardziej wydajne pod względem pamięci i procesora, wystarczy zmienić to w jednym miejscu, a nie w kilkudziesięciu.źródło
Świetne odpowiedzi już tu są, ale chciałbym dodać jedną rzecz:
Używanie
PixelCoordinate
iPixelSize
klasa czyni rzeczy bardzo specyficznymi. GenerałCoordinate
lubPoint2D
klasa może lepiej pasować do twoich potrzeb. APoint2D
powinna być klasą realizującą najczęstsze operacje punkt / wektor. Można realizować taką klasę nawet rodzajowo (Point2D<T>
gdzie T może byćint
lubdouble
czy coś innego).Operacje punkt / wektor 2D są tak bardzo powszechne w przypadku wielu zadań programowania geometrii 2D, że według mojego doświadczenia taka decyzja znacznie zwiększa szansę na ponowne wykorzystanie istniejących operacji 2D. Będzie można uniknąć konieczności ponownego wdrażania ich do siebie
PixelCoordinate
,GPSCoordinate
„Brak jakichkolwiek zastrzeżeń” -Coordinate klasa znowu i znowu.źródło
struct PixelCoordinate:Point2D<int>
jeśli chcesz odpowiedniego bezpieczeństwa typuJest [bardziej czytelny]. To także dobry pomysł.
Ale czy warto tworzyć klasy PixelCoordinate i PixelSize?
Jeśli reprezentują różne koncepcje, ma to sens (to znaczy „tak”).
Możesz zacząć od zrobienia
typedef std::pair<int,int> PixelCoordinate, PixelSize;
. W ten sposób pokrywasz semantykę (pracujesz ze współrzędnymi i rozmiarami pikseli, a nie ze std :: parami w kodzie klienta) i jeśli potrzebujesz rozszerzyć, po prostu zamieniasz typedef na klasę, a kod klienta powinien wymagają minimalnych zmian.Można go również wpisać, ale używałbym,
double
dopóki nie potrzebowałbym powiązanej z nim funkcji, która nie istnieje dla podwójnego (na przykład posiadanieZoomLevel::default
wartości wymagałoby zdefiniowania klasy, ale dopóki nie będzie czegoś podobnego to podwójnie).Są to mocne argumenty (zawsze powinieneś dążyć do tego, aby twoje interfejsy były łatwe w użyciu i trudne w użyciu).
Są powody; jednak w wielu przypadkach powody te nie są dobre. Jednym z ważnych powodów tego może być wydajność, ale to oznacza, że powinieneś widzieć ten rodzaj kodu jako wyjątek, a nie z reguły.
Zasadniczo dobrym pomysłem jest upewnienie się, że jeśli twoje wartości zawsze pojawiają się razem (i nie mają żadnego sensu osobno), powinieneś je pogrupować (z tych samych powodów, o których wspomniałeś w swoim poście).
Oznacza to, że jeśli masz współrzędne, które zawsze mają x, yiz, to znacznie lepiej jest mieć
coordinate
klasę niż przesyłać wszędzie trzy parametry.źródło