Dlaczego metody getter i setter są złe

Nie zamierzałem rozpoczynać serii „jest złem”, ale kilku czytelników poprosiło mnie o wyjaśnienie, dlaczego wspomniałem, że należy unikać metod get / set w zeszłomiesięcznym artykule „Why extends Is Evil”.

Chociaż metody pobierające / ustawiające są powszechne w Javie, nie są one szczególnie zorientowane obiektowo (OO). W rzeczywistości mogą uszkodzić łatwość utrzymania kodu. Co więcej, obecność wielu metod pobierających i ustawiających jest sygnałem ostrzegawczym, że program niekoniecznie jest dobrze zaprojektowany z punktu widzenia obiektowego.

W tym artykule wyjaśniono, dlaczego nie powinieneś używać metod pobierających i ustawiających (i kiedy możesz ich używać) oraz sugerujemy metodologię projektowania, która pomoże Ci wyrwać się z mentalności pobierającej / ustawiającej.

O naturze projektu

Zanim przejdę do kolejnej kolumny dotyczącej projektowania (z prowokacyjnym tytułem), chcę wyjaśnić kilka rzeczy.

Zaskoczyły mnie komentarze niektórych czytelników, które pojawiły się w zeszłomiesięcznym artykule „Dlaczego rozszerza się jest zło” (zobacz Talkback na ostatniej stronie artykułu). Niektórzy ludzie uważali, że argumentowałem, że orientacja obiektowa jest zła po prostu dlatego, że extendsma problemy, tak jakby te dwie koncepcje były równoważne. To z pewnością nie to, co myślałem, że powiedziałem, więc pozwól mi wyjaśnić kilka metaproblemów.

Ta kolumna i artykuł z zeszłego miesiąca dotyczą projektowania. Design z natury to seria kompromisów. Każdy wybór ma dobre i złe strony, a dokonujesz wyboru w kontekście ogólnych kryteriów określonych przez konieczność. Jednak dobro i zło nie są absolutne. Dobra decyzja w jednym kontekście może być zła w innym.

Jeśli nie rozumiesz obu stron problemu, nie możesz dokonać inteligentnego wyboru; w rzeczywistości, jeśli nie rozumiesz wszystkich konsekwencji swoich działań, w ogóle nie projektujesz. Potykasz się w ciemności. To nie przypadek, że każdy rozdział w książce Gang of Four's Design Patterns zawiera sekcję „Konsekwencje” opisującą, kiedy i dlaczego używanie wzorca jest niewłaściwe.

Stwierdzenie, że jakaś funkcja języka lub powszechny idiom programowania (np. Akcesory) ma problemy, to nie to samo, co stwierdzenie, że w żadnym wypadku nie należy ich używać. I tylko dlatego, że funkcja lub idiom jest powszechnie używany, nie oznacza, że powinieneś go używać. Niedoinformowani programiści piszą wiele programów, a zatrudnienie ich przez Sun Microsystems lub Microsoft w magiczny sposób nie poprawia czyjegoś umiejętności programowania lub projektowania. Pakiety Java zawierają dużo świetnego kodu. Ale są też części tego kodu, jestem pewien, że autorzy są zawstydzeni, przyznając się, że napisali.

Z tego samego powodu zachęty marketingowe lub polityczne często wypychają idiomy projektowe. Czasami programiści podejmują złe decyzje, ale firmy chcą promować to, co potrafi technologia, więc pomijają fakt, że sposób, w jaki to robisz, jest mniej niż idealny. Najlepiej wykorzystują złą sytuację. W związku z tym zachowujesz się nieodpowiedzialnie, przyjmując jakąkolwiek praktykę programistyczną, po prostu dlatego, że „tak właśnie powinno się robić”. Wiele nieudanych projektów Enterprise JavaBeans (EJB) potwierdza tę zasadę. Technologia oparta na EJB jest świetną technologią, jeśli jest właściwie używana, ale może dosłownie doprowadzić firmę do upadku, jeśli jest niewłaściwie używana.

Chodzi mi o to, że nie powinieneś programować na ślepo. Musisz zrozumieć spustoszenie, jakie może spowodować funkcja lub idiom. Robiąc to, jesteś w znacznie lepszej pozycji, aby zdecydować, czy powinieneś użyć tej funkcji lub idiomu. Twoje wybory powinny być przemyślane i pragmatyczne. Celem tych artykułów jest pomoc w podejściu do programowania z otwartymi oczami.

Abstrakcja danych

Podstawową zasadą systemów OO jest to, że obiekt nie powinien ujawniać żadnych szczegółów implementacji. W ten sposób możesz zmienić implementację bez zmiany kodu używającego obiektu. Wynika z tego, że w systemach OO należy unikać funkcji pobierających i ustawiających, ponieważ w większości zapewniają one dostęp do szczegółów implementacji.

Aby zobaczyć, dlaczego, weź pod uwagę, że może istnieć 1000 wywołań getX()metody w programie, a każde wywołanie zakłada, że ​​zwracana wartość jest określonego typu. Możesz na przykład przechowywać getX()zwracaną wartość w zmiennej lokalnej, a ten typ zmiennej musi pasować do typu wartości zwracanej. Jeśli potrzebujesz zmienić sposób implementacji obiektu w taki sposób, że zmienia się typ X, masz poważne kłopoty.

Jeśli X był int, ale teraz musi być a long, otrzymasz 1000 błędów kompilacji. Jeśli nieprawidłowo rozwiążesz problem, rzutując zwracaną wartość na int, kod skompiluje się bezproblemowo, ale nie zadziała. (Wartość zwracana może zostać obcięta.) Aby skompensować zmianę, należy zmodyfikować kod otaczający każde z tych 1000 wywołań. Z pewnością nie chcę wykonywać takiej pracy.

Jedną z podstawowych zasad systemów OO jest abstrakcja danych . Należy całkowicie ukryć sposób, w jaki obiekt implementuje procedurę obsługi komunikatów, przed resztą programu. To jeden z powodów, dla których wszystkie zmienne instancji (niestałe pola klasy) powinny być private.

Jeśli utworzysz zmienną instancji public, nie możesz zmienić pola, ponieważ klasa ewoluuje w czasie, ponieważ złamałbyś zewnętrzny kod, który używa tego pola. Nie chcesz przeszukiwać 1000 zastosowań klasy tylko dlatego, że zmienisz tę klasę.

Ta zasada ukrywania implementacji prowadzi do dobrego testu jakości systemu obiektowego: czy możesz dokonać ogromnych zmian w definicji klasy - a nawet wyrzucić całość i zastąpić ją zupełnie inną implementacją - bez wpływu na kod, który z niej korzysta przedmioty klasy? Ten rodzaj modularyzacji jest głównym założeniem orientacji obiektu i znacznie ułatwia konserwację. Bez ukrywania implementacji nie ma sensu używać innych funkcji obiektowych.

Metody pobierające i ustawiające (znane również jako akcesory) są niebezpieczne z tego samego powodu, co publicpola są niebezpieczne: zapewniają zewnętrzny dostęp do szczegółów implementacji. Co się stanie, jeśli musisz zmienić typ dostępnego pola? Musisz także zmienić typ zwracania metody dostępu. Używasz tej wartości zwracanej w wielu miejscach, więc musisz także zmienić cały ten kod. Chcę ograniczyć skutki zmiany do jednej definicji klasy. Nie chcę, żeby weszły do ​​całego programu.

Ponieważ akcesory naruszają zasadę hermetyzacji, można rozsądnie argumentować, że system, który intensywnie lub nieprawidłowo używa akcesorów, po prostu nie jest zorientowany obiektowo. Jeśli przejdziesz przez proces projektowania, a nie tylko kodowanie, nie znajdziesz prawie żadnych akcesoriów w swoim programie. Proces jest ważny. Więcej na ten temat mam na końcu artykułu.

Brak metod pobierających / ustawiających nie oznacza, że ​​niektóre dane nie przepływają przez system. Niemniej jednak najlepiej jest jak najbardziej zminimalizować przenoszenie danych. Z mojego doświadczenia wynika, że ​​łatwość konserwacji jest odwrotnie proporcjonalna do ilości danych przemieszczanych między obiektami. Chociaż możesz jeszcze nie wiedzieć, jak to zrobić, w rzeczywistości możesz wyeliminować większość tego przenoszenia danych.

Starannie projektując i koncentrując się na tym, co musisz zrobić, a nie na tym, jak to zrobisz, eliminujesz w programie większość metod pobierających / ustawiających. Nie pytaj o informacje potrzebne do wykonania pracy; zapytaj obiekt, który ma informacje, aby wykonać pracę za Ciebie.Większość akcesorów trafia do kodu, ponieważ projektanci nie myśleli o modelu dynamicznym: obiektach środowiska wykonawczego i komunikatach, które wysyłają do siebie nawzajem w celu wykonania pracy. Zaczynają (niepoprawnie) od zaprojektowania hierarchii klas, a następnie próbują wprowadzić te klasy do modelu dynamicznego. To podejście nigdy nie działa. Aby zbudować model statyczny, musisz odkryć relacje między klasami, a te relacje dokładnie odpowiadają przepływowi komunikatów. Skojarzenie istnieje między dwiema klasami tylko wtedy, gdy obiekty jednej klasy wysyłają komunikaty do obiektów drugiej. Głównym celem modelu statycznego jest uchwycenie tych informacji o asocjacjach podczas dynamicznego modelowania.

Bez jasno zdefiniowanego modelu dynamicznego zgadujesz tylko, jak użyjesz obiektów klasy. W związku z tym metody pomocnicze często kończą się w modelu, ponieważ musisz zapewnić jak największy dostęp, ponieważ nie możesz przewidzieć, czy będziesz go potrzebować. Tego rodzaju strategia polegająca na projektowaniu przez zgadywanie jest w najlepszym przypadku nieefektywna. Tracisz czas na pisanie bezużytecznych metod (lub dodawanie niepotrzebnych możliwości do klas).

Akcesoria również trafiają do projektów z przyzwyczajenia. Kiedy programiści proceduralni przyjmują Javę, zwykle zaczynają od zbudowania znanego kodu. Języki proceduralne nie mają klas, ale mają C struct(pomyśl: klasa bez metod). Wydaje się zatem naturalne naśladowanie a structprzez budowanie definicji klas praktycznie bez metod i wyłącznie z publicpolami. Ci programiści proceduralni czytają gdzieś, że pola powinny być private, więc tworzą pola privatei dostarczają publicmetody dostępu. Ale tylko skomplikowali publiczny dostęp. Z pewnością nie stworzyli systemu zorientowanego obiektowo.

Narysuj się

Jedną konsekwencją enkapsulacji pełnego pola jest konstrukcja interfejsu użytkownika (UI). Jeśli nie możesz używać akcesorów, nie możesz wywołać getAttribute()metody klasy konstruktora interfejsu użytkownika . Zamiast tego klasy mają elementy takie jak drawYourself(...)metody.

getIdentity()Sposób może również pracować, oczywiście pod warunkiem, że powrót do obiektu, który implementuje Identityinterfejsu. Ten interfejs musi zawierać metodę drawYourself()(lub daj-mi-która JComponent-reprezentuje-twoją-tożsamość). Chociaż getIdentityzaczyna się od „get”, nie jest to akcesor, ponieważ nie zwraca tylko pola. Zwraca złożony obiekt, który zachowuje się rozsądnie. Nawet gdy mam Identityprzedmiot, nadal nie mam pojęcia, jak wewnętrznie reprezentowana jest tożsamość.

Oczywiście drawYourself()strategia oznacza, że ​​umieszczam kod interfejsu użytkownika w logice biznesowej. Zastanów się, co się stanie, gdy zmienią się wymagania interfejsu użytkownika. Powiedzmy, że chcę przedstawić atrybut w zupełnie inny sposób. Dzisiaj „tożsamość” to nazwa; jutro to nazwisko i numer identyfikacyjny; następnego dnia to nazwisko, numer identyfikacyjny i zdjęcie. Ograniczam zakres tych zmian do jednego miejsca w kodzie. Jeśli mam JComponentklasę daj-mi-która -reprezentuje-twoją-tożsamość, to wyodrębniłem sposób reprezentacji tożsamości od reszty systemu.

Pamiętaj, że tak naprawdę nie umieściłem żadnego kodu interfejsu użytkownika w logice biznesowej. Napisałem warstwę interfejsu użytkownika w kategoriach AWT (Abstract Window Toolkit) lub Swing, które są warstwami abstrakcji. Rzeczywisty kod interfejsu użytkownika znajduje się w implementacji AWT / Swing. O to właśnie chodzi w warstwie abstrakcji - aby oddzielić logikę biznesową od mechaniki podsystemu. Mogę łatwo przenieść się do innego środowiska graficznego bez zmiany kodu, więc jedynym problemem jest trochę bałaganu. Możesz łatwo wyeliminować ten bałagan, przenosząc cały kod interfejsu użytkownika do klasy wewnętrznej (lub używając wzorca projektowego Fasada).

JavaBeans

Możesz sprzeciwić się, mówiąc: „Ale co z JavaBeans?” Co z nimi? Z pewnością możesz zbudować JavaBeans bez metod pobierających i ustawiających. BeanCustomizer, BeanInfoOraz BeanDescriptorzajęcia wszystko istnieją dokładnie ten cel. Projektanci specyfikacji JavaBean wrzucili do obrazu idiom getter / setter, ponieważ myśleli, że będzie to łatwy sposób na szybkie zrobienie fasoli - coś, co możesz zrobić, ucząc się, jak to zrobić dobrze. Niestety nikt tego nie zrobił.

Akcesory zostały stworzone wyłącznie w celu oznaczenia pewnych właściwości, aby program do tworzenia interfejsu użytkownika lub jego odpowiednik mógł je zidentyfikować. Nie powinieneś sam nazywać tych metod. Istnieją dla zautomatyzowanego narzędzia do użycia. To narzędzie wykorzystuje interfejsy API introspekcji w Classklasie, aby znaleźć metody i ekstrapolować istnienie pewnych właściwości na podstawie nazw metod. W praktyce ten idiom oparty na introspekcji nie sprawdził się. To sprawiło, że kod stał się zbyt skomplikowany i proceduralny. Programiści, którzy nie rozumieją abstrakcji danych, w rzeczywistości wywołują metody dostępu, w wyniku czego kod jest trudniejszy w utrzymaniu. Z tego powodu funkcja metadanych zostanie włączona do języka Java 1.5 (zapowiedź w połowie 2004 r.). Więc zamiast:

własność prywatna int; public int getProperty () {return property; } public void setProperty (int wartość} {właściwość = wartość;}

Będziesz mógł użyć czegoś takiego:

private @property int property; 

Narzędzie do tworzenia interfejsu użytkownika lub równoważne będzie używać interfejsów API introspekcji do znajdowania właściwości, zamiast sprawdzać nazwy metod i wywnioskować istnienie właściwości na podstawie nazwy. Dlatego żaden akcesor środowiska uruchomieniowego nie uszkadza kodu.

Kiedy akcesorium jest w porządku?

Po pierwsze, jak wspomniałem wcześniej, metoda zwraca obiekt w postaci interfejsu, który obiekt implementuje, ponieważ ten interfejs izoluje cię od zmian w klasie implementującej. Ten rodzaj metody (która zwraca odwołanie do interfejsu) nie jest tak naprawdę „pobieraczem” w sensie metody, która po prostu zapewnia dostęp do pola. Jeśli zmienisz wewnętrzną implementację dostawcy, po prostu zmienisz definicję zwracanego obiektu, aby uwzględnić zmiany. Nadal chronisz kod zewnętrzny, który używa obiektu za pośrednictwem jego interfejsu.