Co powinno być dozwolone w modułach pobierających i ustawiających?

45

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ą)?
Botond Balázs
źródło
18
Najlepszą rzeczą w modułach pobierających i ustawiających jest to, że nie można ich mieć , tj. Opuszczają narzędzie ustawiające, a użytkownik ma właściwość tylko do odczytu, pozostawia narzędzie pobierające i ma opcję konfiguracji, której bieżąca wartość nie jest sprawą niczyją.
Michael Borgwardt,
7
@MichaelBorgwardt Pozostaw jedno i drugie, aby mieć czysty interfejs „mów, nie pytaj”. Właściwości = potencjalny zapach kodu.
Konrad Rudolph
2
Setery mogą być również użyte do podniesienia Wydarzenia .
John Isaiah Carmona
@KonradRudolph Zgadzam się z twoim stwierdzeniem, chociaż chciałbym naprawdę podkreślić słowo „potencjał”.
Phil

Odpowiedzi:

37

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.

S.Robins
źródło
re: zmień wartość, uzasadnione może być ustawienie jej na -1 lub jakiś token z flagą NULL, jeśli ustawiający otrzyma niedozwoloną wartość
Martin Beckett,
1
Jest z tym kilka problemów. Arbitralne ustawienie wartości powoduje zamierzony i niejasny efekt uboczny. Ponadto nie pozwala kodowi wywołującemu otrzymywać informacji zwrotnych, które mogłyby zostać wykorzystane do lepszego radzenia sobie z nielegalnymi danymi. Jest to szczególnie ważne w przypadku wartości na poziomie interfejsu użytkownika. Szczerze mówiąc, jednym z wyjątków, o których właśnie pomyślałem, może być dopuszczenie wielu formatów danych jako danych wejściowych, przy jednoczesnym zapisaniu daty w standardowym formacie daty / godziny. Można w pewnym momencie argumentować, że ten konkretny „efekt uboczny” to normalizacja danych w trakcie walidacji, pod warunkiem, że dane wejściowe są legalne.
S.Robins
Tak, jednym z uzasadnień setera jest to, że powinien on zwracać wartość prawda / fałsz, jeśli można ustawić wartość.
Martin Beckett,
1
„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”. W jakich okolicznościach zbyt duża liczba miejsc po przecinku powoduje wyjątek?
Mark Byers
2
Widzę zmianę wartości na reprezentację kanoniczną jako formę sprawdzania poprawności. Domyślne brakujące wartości (np. Bieżąca data, jeśli znacznik czasu zawiera tylko czas) lub skalowanie wartości (.93275 -> 93,28%) w celu zapewnienia wewnętrznej spójności powinno być OK, ale wszelkie takie manipulacje powinny być jawnie przywołane w dokumentacji API , szczególnie jeśli są rzadkim idiomem w interfejsie API.
TMN
20

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.

Martin Beckett
źródło
1
Wow, projektanci Java są boscy? </jk>
@delnan - oczywiście nie - inaczej rymują się lepiej ;-)
Martin Beckett,
Czy poprawne byłoby utworzenie punktu 3d, w którym wszystkie x, y, z są Float.NAN?
Andrew T Finnell,
4
Nie zgadzam się z twoim stwierdzeniem, że „zwykłe uzasadnienie” (ukrywanie wewnętrznej struktury) jest najmniej użyteczną właściwością pobierających i ustawiających. W języku C # zdecydowanie warto uczynić właściwość interfejsem, którego podstawową strukturę można zmienić - na przykład, jeśli chcesz tylko właściwość dostępną do wyliczenia, o wiele lepiej jest powiedzieć, IEnumerable<T>niż wymuszając ją na coś podobnego List<T>. Twój przykład dostępu do bazy danych, powiedziałbym, narusza pojedynczą odpowiedzialność - mieszanie reprezentacji modelu z jego utrzymywaniem.
Brandon Linton
@AndrewFinnell - może to być dobry sposób na oznaczenie punktów jako niemożliwe / nieważne / usunięte itp.
Martin Beckett,
8

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.

Karl
źródło
2

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 UPDATEdo 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
1

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.

użytkownik470365
źródło
1

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ć centerwłaściwość, nawet jeśli nie ma ivara, który przechowywałby środek widoku; ustawienie centerpowoduje zmiany w innych ivarach, podobnych originlub innych transform, ale klienci klasy nie wiedzą ani nie dbają o center to, jak są przechowywane, pod warunkiem, że działają poprawnie. Jednak nie powinno się zdarzyć, że ustawienie centerpowoduje, że rzeczy dzieją się poza tym, co jest konieczne do zapisania nowej wartości, jednak tak się dzieje.

Caleb
źródło
0

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.

AlexanderBrevig
źródło
-4

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.

m3th0dman
źródło
1
Myślę, że brakuje Ci punktu tego artykułu, który sugeruje oszczędne korzystanie z metod pobierających / ustawiających i unikanie ujawniania danych klasy, jeśli nie jest to konieczne. To IMHO jest rozsądną zasadą projektową, którą deweloper powinien stosować celowo. W zakresie tworzenia obiektów na klasy, mógłby po prostu wystawiać zmienną, ale to sprawia, że coraz trudniej jest zapewnić walidację gdy zmienna jest ustawiona, lub zwrócić wartość z alternatywnego źródła, gdy „got”, w istocie naruszenie hermetyzacji i potencjalnie blokując cię w trudnym do utrzymania projekcie interfejsu.
S.Robins
@ S.Robins Moim zdaniem publiczne podmioty pobierające i ustawiające są w błędzie; pola publiczne są bardziej błędne (jeśli jest to porównywalne).
m3th0dman
@methodman Tak, zgadzam się, że pole publiczne jest błędne, jednak własność publiczna może być przydatna. Tę właściwość można wykorzystać do zapewnienia miejsca do sprawdzania poprawności lub zdarzeń związanych z ustawianiem lub zwracaniem danych, w zależności od konkretnych wymagań w danym czasie. Getters i setery same w sobie nie są błędne. Z drugiej strony sposób i czas ich użycia lub nadużycia mogą być postrzegane jako kiepskie pod względem projektu i konserwacji w zależności od okoliczności. :)
S.Robins
@ S.Robins Pomyśl o podstawach OOP i modelowania; obiekty mają kopiować prawdziwe byty. Czy istnieją rzeczywiste byty, które mają tego rodzaju operacje / właściwości, takie jak pobierające / ustawiające?
m3th0dman
Aby uniknąć zbyt wielu komentarzy tutaj, wezmę tę rozmowę na ten czat i tam skomentuję twój komentarz.
S.Robins