Wciągnąłem interesujący internetowy spór o metody pobierające i ustawiające oraz enkapsulację. Ktoś powiedział, że wszystko, co powinni zrobić, to przypisanie (ustawiające) lub dostęp zmienny (pobierające), aby utrzymać je w czystości i zapewnić enkapsulację.
- 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?
- Kiedy powinna się odbyć walidacja?
- Podczas ustawiania wartości wewnątrz setera (aby uchronić obiekt przed wejściem w nieprawidłowy stan - moja opinia)
- Przed ustawieniem wartości poza ustawiaczem
- Wewnątrz obiektu, przed każdym użyciem wartości
- Czy ustawiający może zmienić wartość (może przekonwertować prawidłową wartość na jakąś kanoniczną reprezentację wewnętrzną)?
object-oriented
language-agnostic
encapsulation
Botond Balázs
źródło
źródło
Odpowiedzi:
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:
i
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:
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:
Czy to mówi ci coś o tym, co stanie się z wartością, gdy zostanie przekazana setterowi?
Co powiesz na:
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.
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ść.
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.
Może w bardzo rzadkich przypadkach. Ogólnie rzecz biorąc, prawdopodobnie lepiej tego nie robić. Jest to coś, co najlepiej pozostawić innej metodzie.
źródło
Jeśli getter / setter po prostu odzwierciedla wartość, to nie ma sensu ich utrzymywać ani uczynić wartością prywatną. Nie ma nic złego w upublicznieniu niektórych zmiennych członkowskich, jeśli masz dobry powód. Jeśli piszesz klasę punktową 3d, publiczne użycie .x, .y, .z ma sens.
Jak powiedział Ralph Waldo Emerson: „Głupia konsekwencja to hobgoblin małych umysłów, uwielbiany przez małych mężów stanu, filozofów i projektantów Javy”.
Funkcje pobierające / ustawiające są przydatne tam, gdzie mogą wystąpić skutki uboczne, gdy trzeba zaktualizować inne zmienne wewnętrzne, ponownie obliczyć buforowane wartości i chronić klasę przed nieprawidłowymi danymi wejściowymi.
Zwykłe uzasadnienie, że ukrywają wewnętrzną strukturę, jest na ogół najmniej przydatne. na przykład. Mam te punkty przechowywane jako 3 zmiennoprzecinkowe, mógłbym zdecydować się na zapisanie ich jako ciągów w zdalnej bazie danych, więc zrobię getters / setters, aby je ukryć, tak jakbyś mógł to zrobić bez żadnego innego wpływu na kod dzwoniącego.
źródło
IEnumerable<T>
niż wymuszając ją na coś podobnegoList<T>
. Twój przykład dostępu do bazy danych, powiedziałbym, narusza pojedynczą odpowiedzialność - mieszanie reprezentacji modelu z jego utrzymywaniem.Zasada jednolitego dostępu Meyera: „Wszystkie usługi oferowane przez moduł powinny być dostępne za pomocą jednolitej notacji, która nie zdradza, czy są one wdrażane poprzez pamięć czy obliczenia”. jest głównym powodem pobierających / ustawiających, czyli Właściwości.
Jeśli zdecydujesz się na buforowanie lub leniwe obliczenie jednego pola klasy, możesz to zmienić w dowolnym momencie, jeśli masz dostęp tylko do właściwości, a nie konkretnych danych.
Wartościowe obiekty, proste struktury nie potrzebują tej abstrakcji, ale moim zdaniem w pełni rozwinięta klasa.
źródło
Powszechną strategią projektowania klas, wprowadzoną za pomocą języka Eiffla, jest rozdzielanie poleceń i zapytań . Chodzi o to, że metoda powinna albo powiedzieć ci coś o obiekcie, albo powiedzieć obiektowi, aby coś zrobił, ale nie robić obu.
Dotyczy to tylko publicznego interfejsu klasy, a nie wewnętrznej reprezentacji. Rozważ obiekt modelu danych, który jest wspierany przez wiersz w bazie danych. Możesz stworzyć obiekt bez wczytywania danych, a wtedy przy pierwszym wywołaniu gettera faktycznie robi to
SELECT
. W porządku, możesz zmieniać niektóre wewnętrzne szczegóły dotyczące tego, jak obiekt jest reprezentowany, ale nie zmieniasz, jak wygląda dla klientów tego obiektu. Powinieneś być w stanie dzwonić do osób pobierających wiele razy i nadal uzyskiwać te same wyniki, nawet jeśli wykonują inną pracę, aby je zwrócić.Podobnie ustawiający wygląda umownie, jakby po prostu zmieniał stan obiektu. Może to zrobić w jakiś zawiły sposób - zapisując
UPDATE
do bazy danych lub przekazując parametr do jakiegoś obiektu wewnętrznego. W porządku, ale zrobienie czegoś niezwiązanego z ustaleniem stanu byłoby zaskakujące.Meyer (twórca Eiffla) również miał coś do powiedzenia na temat walidacji. Zasadniczo, ilekroć obiekt jest w stanie spoczynku, powinien być w poprawnym stanie. Zatem tuż po zakończeniu konstruktora, przed i po (ale niekoniecznie podczas) każdego zewnętrznego wywołania metody, stan obiektu powinien być spójny.
Warto zauważyć, że w tym języku zarówno składnia wywoływania procedury, jak i odczytu odsłoniętego atrybutu wygląda tak samo. Innymi słowy, osoba dzwoniąca nie może stwierdzić, czy używa jakiejś metody, czy też pracuje bezpośrednio ze zmienną instancji. Tylko w językach, które nie ukrywają tego szczegółu implementacji, pojawia się nawet pytanie - jeśli dzwoniący nie mogą powiedzieć, możesz przełączać się między publicznym ivar i akcesoriami bez wycieku tej zmiany w kodzie klienta.
źródło
Inną akceptowalną rzeczą jest klonowanie. W niektórych przypadkach musisz upewnić się, że po tym, jak ktoś da twoją klasę, np. lista czegoś, nie może tego zmienić w twojej klasie (ani zmienić obiektu na tej liście). Dlatego robisz głęboką kopię parametru w seterze i zwracasz głęboką kopię w getterze. (Używanie niezmiennych typów jako parametrów jest kolejną opcją, ale powyżej zakłada, że nie jest to możliwe). Ale nie klonuj akcesoriów, jeśli nie są potrzebne. Łatwo jest myśleć (ale nie odpowiednio) o poprawkach / getterach i setterach jako operacjach o stałych kosztach, więc jest to mina wydajności czekająca na użytkownika api.
źródło
Nie zawsze istnieje odwzorowanie 1-1 między akcesoriami do nieruchomości a ivarami, które przechowują dane.
Na przykład klasa widoku może zapewniać
center
właściwość, nawet jeśli nie ma ivara, który przechowywałby środek widoku; ustawieniecenter
powoduje zmiany w innych ivarach, podobnychorigin
lub innychtransform
, ale klienci klasy nie wiedzą ani nie dbają ocenter
to, jak są przechowywane, pod warunkiem, że działają poprawnie. Jednak nie powinno się zdarzyć, że ustawieniecenter
powoduje, że rzeczy dzieją się poza tym, co jest konieczne do zapisania nowej wartości, jednak tak się dzieje.źródło
Najlepsze w seterach i getterach jest to, że ułatwiają one modyfikowanie reguł API bez zmiany API. Jeśli wykryjesz błąd, bardziej prawdopodobne jest, że możesz naprawić błąd w bibliotece i nie każe każdemu konsumentowi aktualizować swojej bazy kodu.
źródło
Zwykle wierzę, że setery i gettery są złe i powinny być używane tylko w klasach zarządzanych przez framework / kontener. Właściwy projekt klasy nie powinien dawać pobierających i ustawiających.
Edycja: dobrze napisany artykuł na ten temat .
Edycja2: pola publiczne to nonsens w podejściu OOP; mówiąc, że pobożni i rozgrywający są źli, nie mam na myśli, że należy je zastąpić polami publicznymi.
źródło