Pamiętam, jak miałem podobny spór z wykładowcą, gdy uczyłem się C ++ na uniwersytecie. Po prostu nie widziałem sensu używania metod pobierających i ustawiających, kiedy mogłem upublicznić zmienną. Teraz rozumiem lepiej dzięki wieloletniemu doświadczeniu i nauczyłem się lepszego powodu niż po prostu mówienie „zachować kapsułkowanie”.
Definiując programy pobierające i ustawiające, zapewnisz spójny interfejs, więc jeśli chcesz zmienić swoją implementację, mniej prawdopodobne jest uszkodzenie kodu zależnego. Jest to szczególnie ważne, gdy zajęcia są udostępniane za pośrednictwem interfejsu API i używane w innych aplikacjach lub przez strony trzecie. A co z rzeczami, które trafiają do gettera lub setera?
Lepiej jest, jeśli Getters jest zaimplementowany jako zwykłe, tępe przejście dla dostępu do wartości, ponieważ dzięki temu ich zachowanie jest przewidywalne. Mówię ogólnie, ponieważ widziałem przypadki, w których do uzyskiwania dostępu do wartości zmanipulowanych przez obliczenia lub nawet kod warunkowy wykorzystano metody pobierające. Zasadniczo nie jest tak dobry, jeśli tworzysz komponenty wizualne do użycia w czasie projektowania, ale wydaje się przydatny w czasie wykonywania. Nie ma jednak żadnej rzeczywistej różnicy między tym a użyciem prostej metody, z tym wyjątkiem, że kiedy używasz metody, zwykle bardziej prawdopodobne jest, że nazwa metody będzie bardziej odpowiednia, dzięki czemu funkcjonalność „gettera” będzie bardziej widoczna podczas czytania kodu.
Porównaj następujące:
int aValue = MyClass.Value;
i
int aValue = MyClass.CalculateValue();
Druga opcja wyjaśnia, że wartość jest obliczana, podczas gdy pierwszy przykład mówi, że zwracasz wartość, nie wiedząc nic o samej wartości.
Być może możesz argumentować, że następujące wyjaśnienia byłyby jaśniejsze:
int aValue = MyClass.CalculatedValue;
Problem jednak polega na tym, że zakładasz, że wartość została już zmanipulowana w innym miejscu. W przypadku elementu pobierającego, chociaż możesz założyć, że coś innego może się dziać, gdy zwracasz wartość, trudno jest wyjaśnić takie rzeczy w kontekście właściwości, a nazwy właściwości nigdy nie powinny zawierać czasowników w przeciwnym razie trudno jest na pierwszy rzut oka zrozumieć, czy używane nazwisko powinno być ozdobione nawiasami, gdy jest dostępne.
Setery to jednak nieco inna sprawa. Całkowicie właściwe jest, aby ustawiający zapewnił dodatkowe przetwarzanie w celu sprawdzenia poprawności danych przesyłanych do właściwości, zgłaszając wyjątek, jeśli ustawienie wartości naruszałoby zdefiniowane granice właściwości. Problem, jaki niektórzy programiści mają przy dodawaniu przetwarzania do setterów, polega jednak na tym, że zawsze istnieje pokusa, aby setter zrobił trochę więcej, na przykład wykonując obliczenia lub manipulację danymi w jakiś sposób. W tym miejscu można uzyskać działania niepożądane, które w niektórych przypadkach mogą być nieprzewidywalne lub niepożądane.
W przypadku seterów zawsze stosuję prostą zasadę, która polega na jak najmniejszym wykorzystaniu danych. Na przykład zwykle zezwalam na testowanie granic i zaokrąglanie, aby w obu przypadkach móc zgłaszać wyjątki lub unikać niepotrzebnych wyjątków, w których można rozsądnie ich uniknąć. Właściwości zmiennoprzecinkowe są dobrym przykładem, w którym można zaokrąglić zbyt wiele miejsc po przecinku, aby uniknąć podniesienia wyjątku, jednocześnie pozwalając na wprowadzenie wartości zakresu z kilkoma dodatkowymi miejscami po przecinku.
Jeśli zastosujesz jakąś manipulację wejściem settera, będziesz miał taki sam problem jak w przypadku gettera, że trudno jest pozwolić innym wiedzieć, co robi seter, po prostu nazywając go. Na przykład:
MyClass.Value = 12345;
Czy to mówi ci coś o tym, co stanie się z wartością, gdy zostanie przekazana setterowi?
Co powiesz na:
MyClass.RoundValueToNearestThousand(12345);
Drugi przykład mówi dokładnie, co stanie się z Twoimi danymi, a pierwszy nie poinformuje Cię, czy twoja wartość zostanie arbitralnie zmodyfikowana. Podczas czytania kodu drugi przykład będzie znacznie jaśniejszy pod względem celu i funkcji.
Czy mam rację, że całkowicie zniweczyłoby to cel posiadania getterów i seterów, a walidacja i inna logika (bez dziwnych efektów ubocznych) powinny być dozwolone?
Posiadanie programów pobierających i ustawiających nie polega na enkapsulacji ze względu na „czystość”, ale na enkapsulacji w celu umożliwienia łatwej refaktoryzacji kodu bez ryzyka zmiany interfejsu klasy, która w innym przypadku zakłóciłaby kompatybilność klasy z kodem wywołującym. Sprawdzanie poprawności jest w pełni odpowiednie dla setera, jednak istnieje niewielkie ryzyko, że zmiana sprawdzania poprawności może przerwać kompatybilność z kodem wywołującym, jeśli kod wywołujący opiera się na sprawdzeniu poprawności występującym w określony sposób. Jest to na ogół rzadka i stosunkowo niskiego ryzyka sytuacja, ale należy ją odnotować ze względu na kompletność.
Kiedy powinna się odbyć walidacja?
Walidacja powinna nastąpić w kontekście ustawiającego przed faktycznym ustawieniem wartości. Zapewnia to, że jeśli zostanie zgłoszony wyjątek, stan obiektu nie zmieni się i potencjalnie unieważni jego dane. Zasadniczo uważam, że lepiej delegować sprawdzanie poprawności na osobną metodę, która byłaby pierwszą rzeczą wywoływaną w ramach ustawiającego, aby zachować względnie uporządkowany kod ustawiającego.
Czy ustawiający może zmienić wartość (może przekonwertować prawidłową wartość na jakąś kanoniczną reprezentację wewnętrzną)?
Może w bardzo rzadkich przypadkach. Ogólnie rzecz biorąc, prawdopodobnie lepiej tego nie robić. Jest to coś, co najlepiej pozostawić innej metodzie.