Enkapsulacja każe mi uczynić wszystkie lub prawie wszystkie pola prywatnymi i ujawnić je autorom / ustawiającym. Ale teraz pojawiają się biblioteki takie jak Lombok, które pozwalają nam ujawnić wszystkie prywatne pola za pomocą jednej krótkiej adnotacji @Data
. Będzie tworzyć gettery, settery i konstruktory ustawień dla wszystkich prywatnych pól.
Czy ktoś mógłby mi wyjaśnić, jakie jest sens ukrywania wszystkich pól jako prywatnych, a następnie ujawnienia ich za pomocą dodatkowej technologii? Dlaczego więc po prostu nie używamy tylko pól publicznych? Wydaje mi się, że przeszliśmy długą i trudną drogę, aby wrócić do punktu wyjścia.
Tak, istnieją inne technologie, które działają poprzez metody pobierające i ustawiające. I nie możemy ich używać poprzez proste pola publiczne. Ale te technologie pojawiły się tylko dlatego, że mieliśmy te liczne właściwości - prywatne pola za publicznymi obiektami pobierającymi / ustawiającymi. Gdybyśmy nie mieli tych właściwości, technologie te opracowałyby inny sposób i wspierałyby obszary publiczne. I wszystko byłoby proste i nie potrzebowalibyśmy teraz Lombok.
Jaki jest sens całego tego cyklu? I czy enkapsulacja naprawdę ma sens teraz w programowaniu w prawdziwym życiu?
źródło
"It will create getters, setters and setting constructors for all private fields."
- Sposób, w jaki można opisać tego narzędzia, to brzmi jak to jest zachowaniu enkapsulacji. (Przynajmniej w luźnym, zautomatyzowanym, nieco anemicznym modelu). Więc na czym dokładnie polega problem?Odpowiedzi:
Jeśli ujawnisz wszystkie swoje atrybuty za pomocą metod pobierających / ustawiających, zyskujesz tylko strukturę danych, która jest nadal używana w języku C lub innym języku proceduralnym. To nie jest enkapsulacja, a Lombok sprawia, że praca z kodem proceduralnym jest mniej bolesna. Getters / setery tak złe jak zwykłe pola publiczne. Naprawdę nie ma różnicy.
A struktura danych nie jest przedmiotem. Jeśli zaczniesz tworzyć obiekt od pisania interfejsu, nigdy nie dodasz getterów / ustawiaczy do interfejsu. Ujawnienie atrybutów prowadzi do spaghetti kodu proceduralnego, w którym manipulacja danymi jest poza obiektem i rozprzestrzenia się po całej bazie kodu. Teraz masz do czynienia z danymi i manipulacjami danymi zamiast rozmawiać z obiektami. Dzięki programom pobierającym / ustawiającym będziesz mieć programowane procedury oparte na danych, w których manipulowanie odbywa się w sposób absolutnie konieczny. Zdobądź dane - zrób coś - ustaw dane.
W OOP enkapsulacja jest słoniem, jeśli jest wykonana we właściwy sposób. Powinieneś obudować szczegóły stanu i implementacji, aby obiekt miał nad tym pełną kontrolę. Logika będzie skupiona w obiekcie i nie będzie rozłożona na całą bazę kodu. I tak - enkapsulacja jest nadal niezbędna w programowaniu, ponieważ kod będzie łatwiejszy w utrzymaniu.
EDYCJE
Po obejrzeniu trwających dyskusji chcę dodać kilka rzeczy:
Więc jeśli wrócimy do pytania, dlaczego powinniśmy to zrobić. Rozważ ten prosty przykład:
Tutaj mamy Dokument ujawniający szczegóły wewnętrzne przez getter i mamy zewnętrzny kod proceduralny w
printDocument
funkcji, który działa z tym poza obiektem. Dlaczego to takie złe? Ponieważ teraz masz tylko kod w stylu C. Tak, ma strukturę, ale jaka jest naprawdę różnica? Możesz uporządkować funkcje C w różnych plikach i nazwach. I te tak zwane warstwy właśnie to robią. Klasa usług to tylko kilka procedur, które działają z danymi. Ten kod jest trudniejszy w utrzymaniu i ma wiele wad.Porównaj z tym. Teraz mamy umowę, a szczegóły implementacji tej umowy są ukryte wewnątrz obiektu. Teraz możesz naprawdę przetestować tę klasę, a ta klasa hermetyzuje niektóre dane. Przedmiotem jest to, jak działa z tymi danymi. Aby teraz porozmawiać z przedmiotem, musisz poprosić go o wydrukowanie się. To jest enkapsulacja i to jest obiekt. Z OOP zyskasz pełną moc zastrzyku zależności, drwiny, testowania, pojedynczych obowiązków i wiele korzyści.
źródło
Chodzi o to, że nie powinieneś tego robić .
Hermetyzacja oznacza, że ujawniasz tylko te pola, do których faktycznie potrzebujesz innych klas, i że jesteś bardzo selektywny i ostrożny.
Czy nie wystarczy dać wszystkich dziedzinach pobierające i ustawiające domyślnie!
Jest to całkowicie sprzeczne z duchem specyfikacji JavaBeans, która, jak na ironię, wywodzi się z koncepcji publicznych programów pobierających i ustawiających.
Ale jeśli spojrzysz na specyfikację, zobaczysz, że zamierzała ona, aby tworzenie programów pobierających i ustawiających było bardzo selektywne, i że mówi o właściwościach „tylko do odczytu” (bez settera) i „tylko do zapisu” (nie rębacz).
Innym czynnikiem jest to, że osoby pobierające i ustawiające niekoniecznie są prostym dostępem do prywatnego pola . Moduł pobierający może obliczyć zwracaną wartość w dowolnie złożony sposób, być może buforuje go. Seter może sprawdzić wartość lub powiadomić nasłuchujących.
A więc jesteś: enkapsulacja oznacza, że udostępniasz tylko te funkcje, których faktycznie potrzebujesz. Ale jeśli nie pomyślisz o tym, co musisz ujawnić, i po prostu nie chcąc ujawnić wszystkiego, dokonując jakiejś transformacji syntaktycznej, to oczywiście nie jest to naprawdę kapsułkowanie.
źródło
getFieldName()
staje się dla nas automatyczną umową. Nie spodziewamy się po tym skomplikowanych zachowań. W większości przypadków jest to po prostu przełamanie enkapsulacji.Myślę, że sedno sprawy wyjaśniono w twoim komentarzu:
Problemem jest mieszanie modelu danych trwałości z aktywnym modelem danych.
Aplikacja zazwyczaj będzie miała wiele modeli danych:
na górze modelu danych, którego faktycznie używa do wykonywania swoich obliczeń.
Zasadniczo modele danych używane do komunikacji z otoczeniem powinny być izolowane i niezależne od wewnętrznego modelu danych (model obiektu biznesowego, BOM), na którym wykonywane są obliczenia:
W tym scenariuszu idealnie nadaje się obiektom używanym w warstwach komunikacyjnych, aby wszystkie elementy były publiczne lub widoczne dla osób pobierających / ustawiających. Są to zwykłe przedmioty, które nie są niezmienne.
Z drugiej strony, BOM powinien mieć niezmienniki, co na ogół wyklucza posiadanie dużej liczby ustawiaczy (gettery nie wpływają na niezmienniki, chociaż zmniejszają do pewnego stopnia enkapsulację).
źródło
Rozważ następujące..
Masz
User
klasę z właściwościąint age
:Chcesz to zwiększyć, aby
User
mieć datę urodzenia, a nie tylko wiek. Korzystanie z gettera:Możemy zamienić
int age
pole na bardziej złożoneLocalDate dateOfBirth
:Żadnych naruszeń umów, żadnego łamania kodu. Nic więcej niż skalowanie wewnętrznej reprezentacji w ramach przygotowań do bardziej złożonych zachowań.
Samo pole jest hermetyzowane.
Teraz, aby wyjaśnić obawy ...
Lombok
@Data
adnotacja jest podobna do Kotlin w klasach danych .Nie wszystkie klasy reprezentują obiekty behawioralne. Jeśli chodzi o przełamywanie enkapsulacji, zależy to od korzystania z tej funkcji. Nie powinieneś wystawiać wszystkich swoich pól za pośrednictwem programów pobierających.
W bardziej ogólnym sensie hermetyzacja to ukrywanie informacji. Jeśli nadużywasz
@Data
, łatwo założyć, że prawdopodobnie łamiesz enkapsulację. Ale to nie znaczy, że to nie ma celu. Na przykład JavaBeans są przez niektórych niezadowoleni. Jednak jest szeroko stosowany w rozwoju przedsiębiorstw.Czy doszedłbyś do wniosku, że rozwój przedsiębiorstwa jest zły z powodu stosowania fasoli? Oczywiście nie! Wymagania różnią się od wymagań standardowych. Czy fasola może być nadużywana? Oczywiście! Cały czas są wykorzystywani!
Lombok obsługuje również
@Getter
i@Setter
niezależnie - wykorzystuj to, czego wymagają twoje wymagania.źródło
@Data
adnotacji do typu, który z założenia dekapsułkuje wszystkie polasetAge(xyz)
Nie tak definiuje się enkapsulację w programowaniu obiektowym. Hermetyzacja oznacza, że każdy obiekt powinien być jak kapsuła, której zewnętrzna powłoka (publiczne API) chroni i reguluje dostęp do swojego wnętrza (prywatne metody i pola) i ukrywa go przed widokiem. Ukrywając elementy wewnętrzne, osoby dzwoniące nie będą zależeć od elementów wewnętrznych, co pozwala na zmianę elementów wewnętrznych bez zmiany (lub nawet ponownej kompilacji) elementów wywołujących. Ponadto hermetyzacja pozwala każdemu obiektowi wymusić własne niezmienniki, udostępniając tylko bezpieczne operacje dzwoniącym.
Hermetyzacja jest zatem szczególnym przypadkiem ukrywania informacji, w którym każdy obiekt ukrywa swoje wnętrze i wymusza niezmienniki.
Generowanie obiektów pobierających i ustawiających dla wszystkich pól jest dość słabą formą enkapsulacji, ponieważ struktura danych wewnętrznych nie jest ukryta, a niezmienników nie można egzekwować. Ma tę zaletę, że można zmienić sposób, w jaki dane są przechowywane wewnętrznie (o ile można konwertować do i ze starej struktury) bez konieczności zmiany (lub nawet ponownej kompilacji) dzwoniących.
Częściowo wynika to z historycznego wypadku. Pierwszym z nich jest to, że w Javie wyrażenia wywołania metody i wyrażenia dostępu do pola różnią się składniowo w witrynie wywoływania, tj. Zastąpienie dostępu do pola wywołaniem funkcji pobierającej lub ustawiającej psuje interfejs API klasy. Dlatego jeśli potrzebujesz akcesorium, musisz go teraz napisać lub przerwać API. Ten brak obsługi właściwości na poziomie języka jest w wyraźnym kontraście do innych współczesnych języków, w szczególności C # i EcmaScript .
Uderzenie 2 jest to, że JavaBeans opis określonych właściwościach jak pobierające / ustawiające, pól nie były właściwości. W rezultacie większość wczesnych struktur korporacyjnych obsługiwała metody pobierające / ustawiające, ale nie pola. To już dawno już przeszłość ( Java Persistence API (JPA) , Bean Validation , Java Architecture for XML Binding (JAXB) , Jacksonwszystkie pola wsparcia są w tej chwili w porządku), ale stare samouczki i książki nadal się utrzymują i nie wszyscy wiedzą, że wszystko się zmieniło. Brak obsługi właściwości na poziomie języka może być w niektórych przypadkach nadal uciążliwy (na przykład dlatego, że leniwe ładowanie pojedynczych encji przez JPA nie uruchamia się po odczytaniu pól publicznych), ale w większości pola publiczne działają dobrze. To znaczy, moja firma pisze wszystkie DTO dla swoich interfejsów API REST z polami publicznymi (w końcu nie robi się więcej publicznych niż te przesyłane przez Internet :-).
To powiedziawszy, Lombok na
@Data
nie więcej niż wygenerowania pobierające / ustawiające: Generuje równieżtoString()
,hashCode()
iequals(Object)
, co może być bardzo cenne.Hermetyzacja może być bezcenna lub całkowicie bezużyteczna, zależy to od obiektu, który jest enkapsulowany. Ogólnie rzecz biorąc, im bardziej złożona logika w klasie, tym większa korzyść z enkapsulacji.
Automatycznie generowane programy pobierające i ustawiające dla każdego pola są na ogół nadużywane, ale mogą być przydatne do pracy ze starszymi strukturami lub do korzystania z funkcji okazjonalnej struktury nieobsługiwanej dla pól.
Hermetyzację można uzyskać za pomocą metod pobierających i poleceń. Setery zwykle nie są odpowiednie, ponieważ oczekuje się, że zmienią tylko jedno pole, natomiast utrzymanie niezmienników może wymagać zmiany kilku pól jednocześnie.
Podsumowanie
gettery / settery oferują raczej słabą enkapsulację.
Rozpowszechnienie programów pobierających / ustawiających w Javie wynika z braku obsługi właściwości językowych na poziomie językowym oraz wątpliwych wyborów projektowych w jego historycznym modelu komponentów, które zostały zapisane w wielu materiałach dydaktycznych i nauczanych przez nich programistach.
Inne języki obiektowe, takie jak EcmaScript, obsługują właściwości na poziomie języka, dzięki czemu można wprowadzać moduły pobierające bez przerywania interfejsu API. W takich językach programy pobierające mogą być wprowadzane wtedy, gdy są naprawdę potrzebne, a nie z wyprzedzeniem, na wypadek, gdybyś potrzebował tego jednego dnia, co sprawia, że programowanie jest znacznie przyjemniejsze.
źródło
Rzeczywiście zadałem sobie to pytanie.
To jednak nie do końca prawda. Częstotliwość pobierania / ustawiania IMO jest spowodowana specyfikacją Java Bean, która tego wymaga; więc jest to raczej cecha programowania nie zorientowanego obiektowo, ale programowania zorientowanego na Bean, jeśli wolisz. Różnica między nimi polega na tym, że istnieją one w warstwie abstrakcji; Fasola jest bardziej interfejsem systemowym, tj. Na wyższej warstwie. Oderwają się od podstaw OO, a przynajmniej mają na celu - jak zawsze, sprawy są prowadzone zbyt często.
Powiedziałbym, że nieco niefortunne jest to, że tej fasoli, która jest wszechobecna w programowaniu Java, nie towarzyszy odpowiednia funkcja języka Java - myślę o czymś takim jak koncepcja właściwości w C #. Dla tych, którzy nie są tego świadomi, jest to konstrukcja językowa, która wygląda następująco:
W każdym razie drobiazgowość rzeczywistej implementacji wciąż bardzo zyskuje na enkapsulacji.
źródło
Prosta odpowiedź brzmi: masz absolutną rację. Gettery i settery eliminują większość (ale nie wszystkie) wartości enkapsulacji. Nie oznacza to, że za każdym razem, gdy masz metodę get i / lub set, przełamujesz enkapsulację, ale jeśli ślepo dodajesz akcesoria do wszystkich prywatnych członków klasy, robisz to źle.
Gettery to ustawiacze, które są wszechobecne w programowaniu w Javie, ponieważ koncepcja JavaBean została wypchnięta jako sposób na dynamiczne powiązanie funkcjonalności z gotowym kodem. Na przykład możesz mieć formularz w aplecie (ktoś pamięta?), Który sprawdzałby twój obiekt, znalazł wszystkie właściwości i wyświetlał jako pola. Następnie interfejs użytkownika może modyfikować te właściwości na podstawie danych wprowadzonych przez użytkownika. Jako deweloper martwisz się więc napisaniem klasy i umieszczeniem tam jakiejkolwiek walidacji lub logiki biznesowej itp.
Korzystanie z przykładowych ziaren
Nie jest to sam w sobie okropny pomysł, ale nigdy nie byłem wielkim fanem tego podejścia w Javie. Po prostu idzie wbrew pozorom. Używaj Pythona, Groovy itp. Coś, co wspiera takie podejście w bardziej naturalny sposób.
JavaBean wymknął się spod kontroli, ponieważ stworzył JOBOL, tj. Programistów napisanych w Javie, którzy nie rozumieją OO. Zasadniczo obiekty stały się niczym więcej niż workami danych, a cała logika została napisana na zewnątrz długimi metodami. Ponieważ było to postrzegane jako normalne, ludzie tacy jak ty i ja, którzy kwestionują to, byli uważani za kooków. Ostatnio widziałem zmianę, a to nie jest tak duża pozycja z zewnątrz
Wiązanie XML to trudny orzech. Prawdopodobnie nie jest to dobre pole bitwy do przeciwstawienia się JavaBeans. Jeśli musisz zbudować te JavaBeans, postaraj się trzymać je z dala od prawdziwego kodu. Traktuj je jako część warstwy serializacji.
źródło
[Serializable]
atrybut i jego krewnych. Po co pisać 101 konkretnych metod / klas serializacji, skoro można po prostu napisać jedną z nich, wykorzystując refleksję?Ile możemy zrobić bez getterów? Czy można je całkowicie usunąć? Jakie to stwarza problemy? Czy moglibyśmy nawet zablokować
return
słowo kluczowe?Okazuje się, że możesz wiele zrobić, jeśli chcesz wykonać pracę. W jaki więc sposób informacja może kiedykolwiek wydostać się z tego w pełni zamkniętego obiektu? Za pośrednictwem współpracownika.
Zamiast pozwalać, by kod zadawał ci pytania, które podpowiadasz. Jeśli te rzeczy również nie zwracają rzeczy, nie musisz nic robić z tym, co zwracają. Więc jeśli myślisz o sięgnięciu po,
return
spróbuj sięgnąć po jakiegoś współpracownika portu wyjściowego, który zrobi wszystko, co pozostało do zrobienia.Robienie rzeczy w ten sposób ma zalety i konsekwencje. Musisz pomyśleć o czymś więcej niż tylko o tym, co wrócisz. Musisz pomyśleć o tym, jak wyślesz to jako wiadomość do obiektu, który o to nie prosił. Możliwe, że podasz ten sam obiekt, który zwróciłeś, lub może wystarczy zwykłe wywołanie metody. Takie myślenie wiąże się z pewnymi kosztami.
Korzyścią jest to, że teraz rozmawiasz z interfejsem. Oznacza to, że w pełni czerpiesz korzyści z abstrakcji.
Daje to również wysyłkę polimorficzną, ponieważ chociaż wiesz, co mówisz, nie musisz wiedzieć, do czego dokładnie to mówisz.
Możesz myśleć, że oznacza to, że musisz przejść tylko jedną drogę przez stos warstw, ale okazuje się, że możesz użyć polimorfizmu, aby cofnąć się bez tworzenia szalonych zależności cyklicznych.
Może to wyglądać tak:
Jeśli potrafisz tak kodować, wybór getters jest wyborem. Nie konieczne zło.
źródło
Jest to kwestia kontrowersyjna (jak widać), ponieważ wiązka dogmatów i nieporozumień miesza się z uzasadnionymi obawami dotyczącymi spraw pobierających i ustawiających. Krótko mówiąc, nie ma w tym nic złego
@Data
i nie przerywa enkapsulacji.Dlaczego warto korzystać z funkcji pobierających i ustawiających zamiast pól publicznych?
Ponieważ gettery / settery zapewniają enkapsulację. Jeśli ujawnisz wartość jako pole publiczne, a następnie przejdziesz do obliczania wartości w locie, musisz zmodyfikować wszystkich klientów uzyskujących dostęp do tego pola. Oczywiście jest to złe. Jest to szczegół implementacji, czy jakaś właściwość obiektu jest przechowywana w polu, jest generowana w locie, czy jest pobierana z innego miejsca, więc różnica nie powinna być ujawniana klientom. Setter Getter / Setters rozwiązuje ten problem, ponieważ ukrywa implementację.
Ale jeśli getter / setter po prostu odzwierciedlają leżące u podstaw prywatne pole, czy nie jest tak źle?
Nie! Chodzi o to, że enkapsulacja pozwala na zmianę implementacji bez wpływu na klientów. Pole może nadal być doskonałym sposobem na przechowywanie wartości, o ile klienci nie muszą o tym wiedzieć ani się tym przejmować.
Ale czy autogenerowanie getterów / setterów z pól łamie enkapsulację?
Nie ma jeszcze enkapsulacji!
@Data
adnotacje to po prostu wygodny sposób na pisanie par getter / setter, które wykorzystują podstawowe pole. Z punktu widzenia klienta jest to jak zwykła para getter / setter. Jeśli zdecydujesz się przepisać implementację, możesz to zrobić bez wpływu na klienta. Otrzymujesz więc to, co najlepsze z obu światów: enkapsulację i zwięzłą składnię.Ale niektórzy twierdzą, że getter / setters są zawsze źli!
Istnieje osobna kontrowersja, w której niektórzy uważają, że wzorzec getter / setter jest zawsze zły, niezależnie od podstawowej implementacji. Chodzi o to, że nie należy ustawiać ani pobierać wartości z obiektów, a raczej wszelkie interakcje między obiektami powinny być modelowane jako komunikaty, w których jeden obiekt prosi inny obiekt o zrobienie czegoś. Jest to głównie fragment dogmatu z początków myślenia obiektowego. Obecnie myślimy, że dla niektórych wzorców (np. Obiektów wartości, obiektu transferu danych) gettery / settery mogą być całkowicie odpowiednie.
źródło
Kapsułkowanie ma jakiś cel, ale może być również niewłaściwie wykorzystywane lub nadużywane.
Rozważmy coś takiego jak interfejs API Androida, który ma klasy z dziesiątkami (jeśli nie setkami) pól. Ujawnienie tych pól konsumentowi interfejsu API utrudnia nawigację i korzystanie, a także daje użytkownikowi fałszywe wyobrażenie, że może robić, co chce, z polami, które mogą kolidować z tym, w jaki sposób powinny być używane. Zatem enkapsulacja jest świetna w tym sensie dla łatwości konserwacji, użyteczności, czytelności i unikania szalonych błędów.
Z drugiej strony POD lub zwykłe stare typy danych, takie jak struct z C / C ++, w którym wszystkie pola są publiczne, mogą być również przydatne. Posiadanie bezużytecznych programów pobierających / ustawiających, takich jak te generowane przez adnotację @data w Lombok, jest tylko sposobem na zachowanie „wzorca enkapsulacji”. Jednym z niewielu powodów, dla których robimy „bezużyteczne” programy pobierające / ustawiające w Javie, jest to, że metody zapewniają kontrakt .
W Javie nie można mieć pól w interfejsie, dlatego do pobierania wspólnych i ustawiających parametrów używa się wspólnej właściwości, którą mają wszyscy implementatorzy tego interfejsu. W nowszych językach, takich jak Kotlin lub C #, pojęcie własności postrzegamy jako pola, dla których można zadeklarować settera i gettera. Ostatecznie bezużyteczne programy pobierające / ustawiające są bardziej dziedzictwem, z którym Java musi żyć, chyba że Oracle doda do niej właściwości. Na przykład Kotlin, który jest innym językiem JVM opracowanym przez JetBrains, ma klasy danych, które w zasadzie robią to, co adnotacja @data robi w Lombok.
Oto kilka przykładów:
To zły przypadek kapsułkowania. Getter i setter są skutecznie bezużyteczne. Hermetyzacja jest najczęściej stosowana, ponieważ jest to standard w językach takich jak Java. W rzeczywistości nie pomaga, oprócz zachowania spójności w całej bazie kodu.
To dobry przykład enkapsulacji. Hermetyzacja służy do egzekwowania umowy, w tym przypadku IDataInterface. Celem enkapsulacji w tym przykładzie jest skłonienie konsumenta tej klasy do korzystania z metod udostępnianych przez interfejs. Mimo że getter i setter nie robią nic szczególnego, zdefiniowaliśmy wspólną cechę między DataClass i innymi implementatorami IDataInterface. Dlatego mogę mieć taką metodę:
Mówiąc o enkapsulacji, myślę, że ważne jest również rozwiązanie problemu składni. Często widzę, jak ludzie narzekają na składnię, która jest konieczna do wymuszenia enkapsulacji, a nie samej enkapsulacji. Jednym z przykładów, które przychodzi do głowy to od Casey Muratori (można zobaczyć jego rant tutaj ).
Załóżmy, że masz klasę graczy korzystającą z hermetyzacji i chcesz przesunąć swoją pozycję o 1 jednostkę. Kod wyglądałby tak:
Bez enkapsulacji wyglądałoby to tak:
Tutaj twierdzi, że enkapsulacja prowadzi do znacznie więcej pisania bez żadnych dodatkowych korzyści i może to w wielu przypadkach być prawdą, ale zauważ coś. Argument jest przeciwko składni, a nie samej enkapsulacji. Nawet w językach takich jak C, w których brak koncepcji enkapsulacji, często zobaczysz zmienne w strukturach z prefiksem lub sufiksem z „_” lub „my” lub cokolwiek innego, co oznacza, że nie powinny być używane przez konsumenta interfejsu API, tak jakby były prywatny.
Faktem jest, że enkapsulacja może sprawić, że kod będzie łatwiejszy w utrzymaniu i łatwy w użyciu. Rozważ tę klasę:
Gdyby zmienne były publiczne w tym przykładzie, konsument tego interfejsu API byłby zdezorientowany, kiedy używać posX i posY, a kiedy setPosition (). Ukrywanie tych szczegółów pomaga konsumentowi lepiej korzystać z interfejsu API w intuicyjny sposób.
Składnia jest jednak ograniczeniem w wielu językach. Jednak nowsze języki oferują właściwości, które zapewniają nam ładną składnię członków publice i zalety enkapsulacji. Znajdziesz właściwości w C #, Kotlin, nawet w C ++, jeśli używasz MSVC. oto przykład w Kotlinie.
klasa VerticalList: ... {var posX: Int set (x) {field = x; ...} var posY: Int set (y) {field = y; ...}}
Tutaj osiągnęliśmy to samo, co w przykładzie Java, ale możemy używać posX i posY tak, jakby były zmiennymi publicznymi. Kiedy jednak spróbuję zmienić ich wartość, zostanie wykonane ciało setera set ().
Na przykład w Kotlin byłby to odpowiednik Java Bean z zaimplementowanymi modułami pobierającymi, ustawiającymi, hashcode, equals i toString:
Zauważ, jak ta składnia pozwala nam wykonać Java Bean w jednym wierszu. Prawidłowo zauważyłeś problem, jaki ma język, taki jak Java, przy implementacji enkapsulacji, ale jest to wina Javy, że nie sama enkapsulacja.
Powiedziałeś, że używasz @Data Lomboka do generowania pobierających i ustawiających. Zwróć uwagę na nazwę @Data. Jest to głównie przeznaczone do stosowania w klasach danych, które przechowują tylko dane i są przeznaczone do serializacji i deserializacji. Pomyśl o czymś w rodzaju pliku zapisu z gry. Ale w innych scenariuszach, na przykład z elementem interfejsu użytkownika, na pewno chcesz setterów, ponieważ sama zmiana wartości zmiennej może nie wystarczyć do uzyskania oczekiwanego zachowania.
źródło
Kapsułkowanie zapewnia elastyczność . Oddzielając strukturę i interfejs, umożliwia zmianę struktury bez zmiany interfejsu.
Np. Jeśli uznasz, że musisz obliczyć właściwość na podstawie innych pól zamiast inicjowania pola leżącego u podstaw konstrukcji, możesz po prostu zmienić getter. Jeśli odsłoniłeś to pole bezpośrednio, musisz zmienić interfejs i wprowadzić zmiany w każdej witrynie użytkowania.
źródło
Spróbuję zilustrować problematyczną przestrzeń enkapsulacji i projektowania klas, a na końcu odpowiem na twoje pytanie.
Jak wspomniano w innych odpowiedziach, celem enkapsulacji jest ukrycie wewnętrznych szczegółów obiektu za publicznym interfejsem API, który służy jako umowa. Obiekt może bezpiecznie zmienić swoje elementy wewnętrzne, ponieważ wie, że jest wywoływany tylko przez publiczny interfejs API.
To, czy sensowne jest posiadanie pól publicznych, metod pobierających / ustawiających, czy metod transakcyjnych wyższego poziomu lub przekazywania wiadomości, zależy od natury modelowanej domeny. W książce Akka Concurrency (którą mogę polecić, nawet jeśli jest nieco nieaktualna) znajdziesz przykład, który to ilustruje, który skrócę tutaj.
Rozważ klasę użytkownika:
Działa to dobrze w kontekście jednowątkowym. Modelowana domena jest imieniem osoby, a mechanizmy przechowywania tej nazwy mogą być doskonale zawarte w ustawieniach.
Wyobraź sobie jednak, że musi to być zapewnione w kontekście wielowątkowym. Załóżmy, że jeden wątek okresowo odczytuje nazwę:
Dwa inne wątki toczą wojnę, ustawiając ją z kolei na Hillary Clinton i Donalda Trumpa . Każdy z nich musi wywołać dwie metody. Zwykle działa to dobrze, ale co jakiś czas zobaczysz Hillary Trump lub Donalda Clintona .
Nie można rozwiązać tego problemu, dodając blokadę wewnątrz ustawiaczy, ponieważ blokada jest utrzymywana tylko przez czas ustawiania imienia lub nazwiska. Jedynym rozwiązaniem poprzez blokowanie jest dodanie blokady wokół całego obiektu, ale przerywa to enkapsulację, ponieważ kod wywołujący musi zarządzać blokadą (i może powodować zakleszczenia).
Jak się okazuje, nie ma czystego rozwiązania poprzez blokowanie. Czystym rozwiązaniem jest ponowne zamknięcie elementów wewnętrznych poprzez zwiększenie ich szorstkości:
Sama nazwa stała się niezmienna i widać, że jej członkowie mogą być publiczni, ponieważ jest to teraz czysty obiekt danych, bez możliwości modyfikacji po utworzeniu. Z kolei publiczny interfejs API klasy User stał się bardziej gruboziarnisty, został tylko jeden setter, dzięki czemu nazwę można zmienić tylko jako całość. Hermetyzuje więcej swojego wewnętrznego stanu za API.
W tym cyklu widać próby zbyt szerokiego zastosowania rozwiązań dobrych w określonych okolicznościach. Odpowiedni poziom enkapsulacji wymaga zrozumienia modelowanej domeny i zastosowania odpowiedniego poziomu enkapsulacji. Czasami oznacza to, że wszystkie pola są publiczne, a czasem (jak w aplikacjach Akka) oznacza, że w ogóle nie masz publicznego interfejsu API, z wyjątkiem jednej metody odbierania wiadomości. Jednak sama koncepcja enkapsulacji, oznaczająca ukrywanie elementów wewnętrznych za stabilnym API, jest kluczem do programowania oprogramowania na dużą skalę, szczególnie w systemach wielowątkowych.
źródło
Mogę wymyślić jeden przypadek użycia, w którym ma to sens. Możesz mieć klasę, do której początkowo uzyskujesz dostęp za pomocą prostego interfejsu API getter / setter. Później rozszerzasz lub modyfikujesz, aby nie korzystał już z tych samych pól, ale nadal obsługiwał ten sam interfejs API .
Nieco wymyślony przykład: Punkt, który zaczyna się jako para kartezjańska z
p.x()
ip.y()
. Później tworzysz nową implementację lub podklasę, która używa współrzędnych biegunowych, dzięki czemu możesz również wywoływaćp.r()
ip.theta()
, ale kod klienta, który wywołujep.x()
ip.y()
pozostaje ważny. Sama klasa w przejrzysty sposób przekształca się z wewnętrznej postaci polarnej, czyliy()
terazreturn r * sin(theta);
. (W tym przykładzie ustawienie tylkox()
luby()
nie ma tak dużego sensu, ale nadal jest możliwe).W takim przypadku może się zdarzyć, że powiesz: „Cieszę się, że zadałem sobie trud automatycznego zadeklarowania programów pobierających i ustawiających zamiast upubliczniać pola, albo musiałbym tam złamać mój interfejs API”.
źródło
Nie ma absolutnie żadnego sensu. Jednak fakt, że zadajesz to pytanie, pokazuje, że nie rozumiesz, co robi Lombok i że nie rozumiesz, jak pisać kod OO z enkapsulacją. Cofnijmy się trochę ...
Niektóre dane dla instancji klasy zawsze będą wewnętrzne i nigdy nie powinny być ujawniane. Niektóre dane dla instancji klasy będą musiały zostać ustawione zewnętrznie, a niektóre dane mogą wymagać przekazania z powrotem z instancji klasy. Możemy jednak chcieć zmienić sposób, w jaki klasa robi rzeczy pod powierzchnią, więc używamy funkcji, które pozwalają nam pobierać i ustawiać dane.
Niektóre programy chcą zapisać stan dla instancji klas, więc mogą one mieć interfejs szeregowania. Dodajemy więcej funkcji, które pozwalają instancji klasy przechowywać jej stan w pamięci i pobierać jej stan z pamięci. Zachowuje to enkapsulację, ponieważ instancja klasy nadal kontroluje własne dane. Być może szeregujemy prywatne dane, ale reszta programu nie ma do nich dostępu (a dokładniej utrzymujemy Mur Chiński , wybierając opcję nieświadomego uszkodzenia tych prywatnych danych), a instancja klasy może (i powinna) przeprowadzić kontrole integralności po dezrializacji, aby upewnić się, że dane wróciły OK
Czasami dane wymagają kontroli zakresu, kontroli integralności lub podobnych czynności. Samodzielne pisanie tych funkcji pozwala nam to zrobić. W tym przypadku nie chcemy ani nie potrzebujemy Lombok, ponieważ robimy to wszystko sami.
Często jednak zdarza się, że parametr ustawiony zewnętrznie jest przechowywany w jednej zmiennej. W takim przypadku potrzebne byłyby cztery funkcje do pobrania / ustawienia / serializacji / deserializacji zawartości tej zmiennej. Samo pisanie tych czterech funkcji spowalnia Cię i jest podatne na błędy. Automatyzacja procesu za pomocą Lombok przyspiesza rozwój i eliminuje możliwość wystąpienia błędów.
Tak, można by upublicznić tę zmienną. W tej konkretnej wersji kodu byłby funkcjonalnie identyczny. Wróćmy jednak do tego, dlaczego używamy funkcji: „Możemy chcieć zmienić sposób, w jaki klasa robi rzeczy pod powierzchnią ...” Jeśli upublicznisz zmienną, ograniczysz swój kod teraz i na zawsze, aby ta zmienna publiczna była Interfejs. Jeśli jednak korzystasz z funkcji lub używasz Lombok do automatycznego generowania tych funkcji, możesz w dowolnym momencie zmienić dane bazowe i implementację bazową.
Czy to czyni to jaśniejszym?
źródło
W rzeczywistości nie jestem programistą Java; ale poniższe są dość niezależne od platformy.
Prawie wszystko, co piszemy, używa publicznych pobierających i ustawiających, które uzyskują dostęp do zmiennych prywatnych. Większość pobierających i ustawiających jest trywialna. Ale kiedy zdecydujemy, że setter musi coś ponownie przeliczyć, lub setter dokonuje pewnej weryfikacji lub musimy przekazać właściwość do właściwości zmiennej składowej tej klasy, jest to całkowicie niezniszczalne dla całego kodu i jest kompatybilne binarnie, więc może zamienić jeden moduł.
Kiedy zdecydujemy, że ta właściwość powinna być obliczana na bieżąco, cały kod, który na nią patrzy, nie musi się zmieniać, a tylko kod, który do niej pisze, musi się zmienić, a IDE może ją dla nas znaleźć. Kiedy zdecydujemy, że jest to pole komputerowe z możliwością zapisu (tylko kilka razy musiało to zrobić), możemy to zrobić. Zaletą jest to, że kilka z tych zmian jest kompatybilnych binarnie (przejście na pole obliczeniowe tylko do odczytu nie jest teoretycznie, ale i tak może być w praktyce).
Skończyło się na wielu trywialnych getterach ze skomplikowanymi setterami. Skończyło się również na kilku buforach do pobierania. W rezultacie możesz założyć, że osoby pobierające są dość tanie, ale osoby ustawiające mogą nie być. Z drugiej strony jesteśmy wystarczająco mądrzy, aby zdecydować, że setery nie upierają się przy dysku.
Ale musiałem wyśledzić faceta, który ślepo zmieniał wszystkie zmienne składowe na właściwości. Nie wiedział, co to jest atomowy dodatek, więc zmienił rzecz, która naprawdę musiała być zmienną publiczną na właściwość, i złamał kod w subtelny sposób.
źródło
Gettery i setery są środkiem „na wszelki wypadek” dodanym w celu uniknięcia refaktoryzacji w przyszłości, jeśli struktura wewnętrzna lub wymagania dotyczące dostępu zmienią się podczas procesu programowania.
Powiedzmy, że kilka miesięcy po wydaniu twój klient informuje cię, że jedno z pól klasy jest czasem ustawione na wartości ujemne, nawet jeśli w takim przypadku powinno ono osiągnąć zero. Korzystając z pól publicznych, musisz wyszukać każde przypisanie tego pola w całej bazie kodu, aby zastosować funkcję blokowania do wartości, którą zamierzasz ustawić, i pamiętaj, że zawsze będziesz musiał to zrobić podczas modyfikowania tego pola, ale to jest do bani. Zamiast tego, jeśli zdarzyło Ci się już używać getterów i setterów, byłbyś w stanie po prostu zmodyfikować metodę setField (), aby mieć pewność, że to zaciskanie jest zawsze stosowane.
Problem z „przestarzałymi” językami, takimi jak Java, polega na tym, że zachęcają do korzystania z metod do tego celu, co sprawia, że Twój kod jest nieskończenie bardziej szczegółowy. Pisanie jest bolesne i trudne do odczytania, dlatego używamy IDE, które w ten czy inny sposób łagodzą ten problem. Większość IDE automatycznie generuje dla ciebie gettery i settery, a także je ukrywa, chyba że podano inaczej. Lombok idzie o krok dalej i po prostu generuje je proceduralnie w czasie kompilacji, aby Twój kod był wyjątkowo elegancki. Jednak inne bardziej nowoczesne języki po prostu rozwiązały ten problem w taki czy inny sposób. Na przykład języki Scala lub .NET
Na przykład w VB .NET lub C # można po prostu ustawić wszystkie pola, które mają mieć zwykłe efekty uboczne i ustawiające tylko pola publiczne, a następnie ustawić je jako prywatne, zmienić ich nazwę i ujawnić właściwość o poprzedniej nazwie pole, w którym można dokładnie dostosować zachowanie dostępu do pola, jeśli jest to potrzebne. Dzięki Lombok, jeśli chcesz dostroić zachowanie narzędzia pobierającego lub ustawiającego, możesz po prostu usunąć te tagi w razie potrzeby i kodować własne według nowych wymagań, wiedząc na pewno, że nie będziesz musiał niczego refaktoryzować w innych plikach.
Zasadniczo sposób, w jaki twoja metoda uzyskuje dostęp do pola, powinien być przejrzysty i jednolity. Współczesne języki pozwalają definiować „metody” przy użyciu tej samej składni dostępu / wywołania pola, więc te modyfikacje można wprowadzać na żądanie, nie myśląc o tym dużo na wczesnym etapie programowania, ale Java zmusza cię do wykonania tej pracy z wyprzedzeniem, ponieważ nie ma tej funkcji. Wszystko, co robi Lombok, to oszczędność czasu, ponieważ używany język nie pozwalał oszczędzać czasu na metodę „na wszelki wypadek”.
źródło
foo.bar
ale może być obsługiwany przez wywołanie metody. Twierdzisz, że jest to lepsze niż „przestarzały” sposób, aby interfejs API wyglądał jak wywołania metodfoo.getBar()
. Wydaje się, że zgadzamy się, że pola publiczne są problematyczne, ale twierdzę, że „przestarzała” alternatywa jest lepsza od „nowoczesnej”, ponieważ cały nasz interfejs API jest symetryczny (wszystkie wywołania metod). W podejściu „nowoczesnym” musimy zdecydować, które rzeczy powinny być właściwościami, a które metodami, co nadmiernie komplikuje wszystko (zwłaszcza jeśli użyjemy refleksji!).Tak, to jest sprzeczne. Najpierw natrafiłem na właściwości w Visual Basic. Do tego czasu, w innych językach, z mojego doświadczenia, wokół pól nie było opakowań nieruchomości. Tylko pola publiczne, prywatne i chronione.
Właściwości były rodzajem enkapsulacji. Zrozumiałem właściwości Visual Basic jako sposób kontrolowania i manipulowania danymi wyjściowymi jednego lub więcej pól, jednocześnie ukrywając pole jawne, a nawet jego typ danych, na przykład emitując datę jako ciąg znaków w określonym formacie. Ale nawet wtedy nie można „ukrywać stanu i ujawniać funkcjonalności” z perspektywy większego obiektu.
Ale właściwości usprawiedliwiają się z powodu oddzielnych obiektów pobierających i ustawiających właściwości. Odsłonięcie pola bez właściwości było wszystkim albo niczym - jeśli potrafisz je odczytać, możesz je zmienić. Teraz można uzasadnić słabą klasę projektową za pomocą chronionych ustawień.
Dlaczego więc nie używamy / oni tylko rzeczywistych metod? Ponieważ był to Visual Basic (i VBScript ) (oooh! Aaah!), Kodujący dla mas (!), I to była wściekłość. I w ten sposób ostatecznie zdominowała idiokracja.
źródło