Czy enkapsulacja jest nadal jednym ze słoni, na którym stoi OOP?

97

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?

Gangnus
źródło
19
"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?
David
78
Hermetyzacja ukrywa wewnętrzne implementacje obiektu za jego zamówieniem publicznym (zwykle interfejs). Gettery i settery robią dokładnie odwrotnie - odsłaniają wewnętrzne elementy obiektu, więc problemem są gettery / settery, a nie hermetyzacja.
22
Klasy danych @VinceEmigh nie mają enkapsulacji .
Ich
10
@VinceEmigh korzystający z JavaBeans nie jest OO , jest proceduralny . To, że literatura nazywa je „Przedmiotami”, jest błędem historii.
Caleth,
28
Przez lata dużo się nad tym zastanawiałem; Myślę, że jest to przypadek, w którym zamiar OOP różnił się od wdrożenia. Po przestudiowaniu SmallTalk stało się jasne, jakie było przeznaczenie enkapsulacji OOP (tj. Każda klasa jest jak niezależny komputer, z metodami jako wspólnym protokołem), choć z nieznanych mi powodów zdecydowanie zyskała popularność obiekty getter / setter, które nie oferują enkapsulacji pojęciowej (niczego nie ukrywają, niczego nie zarządzają i nie ponoszą żadnej odpowiedzialności poza danymi), a jednak nadal korzystają z właściwości.
jrh

Odpowiedzi:

82

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:

  • Nie ma znaczenia, ile atrybutów ujawniasz za pomocą programów pobierających / ustawiających i jak ostrożnie to robisz. Większa selektywność nie spowoduje, że kod zostanie zamknięty w trybie enkapsulacji. Każdy ujawniony atrybut spowoduje, że procedura będzie działać z tymi nagimi danymi w bezwzględny sposób proceduralny. Po prostu będziesz rozprowadzał swój kod wolniej, będąc bardziej selektywnym. To nie zmienia rdzenia.
  • Tak, w granicach systemu masz nagie dane z innych systemów lub bazy danych. Ale te dane to tylko kolejny punkt enkapsulacji.
  • Obiekty powinny być niezawodne . Cała idea przedmiotów polega na byciu odpowiedzialnym, abyś nie musiał wydawać rozkazów prostych i bezwzględnych. Zamiast tego pytasz obiekt o to, co robi dobrze dzięki umowie. Bezpiecznie delegujesz działającą część do obiektu. Obiekty hermetyzują szczegóły stanu i implementacji.

Więc jeśli wrócimy do pytania, dlaczego powinniśmy to zrobić. Rozważ ten prosty przykład:

public class Document {
    private String title;

    public String getTitle() {
        return title;
    }
}

public class SomeDocumentServiceOrHandler {

    public void printDocument(Document document) {
        System.out.println("Title is " + document.getTitle());
    }
}

Tutaj mamy Dokument ujawniający szczegóły wewnętrzne przez getter i mamy zewnętrzny kod proceduralny w printDocumentfunkcji, 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.

public interface Printable {
    void print();
}

public final class PrintableDocument implements Printable {
    private final String title;

    public PrintableDocument(String title) {
        this.title = title;
    }

    @Override
    public void print() {
        System.out.println("Title is " + title);
    }
}

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.

Izbassar Tolegen
źródło
Komentarze nie są przeznaczone do rozszerzonej dyskusji; ta rozmowa została przeniesiona do czatu .
wałek klonowy
1
„Pobierani / ustawiający tak źli jak zwykłe pola publiczne” - to nieprawda, przynajmniej w wielu językach. Zwykle nie można zastąpić dostępu do pola zwykłego, ale można zastąpić metody pobierające / ustawiające. To daje wszechstronność klasom podrzędnym. Ułatwia także zmianę zachowania klas. np. możesz zacząć od gettera / settera wspieranego przez prywatne pole, a później przejść do innego pola lub obliczyć wartość z innych wartości. Nie można tego zrobić za pomocą zwykłych pól. Niektóre języki zezwalają na to, aby pola miały w zasadzie automatyczne programy pobierające i ustawiające, ale Java nie jest takim językiem.
Kat.
Eh, wciąż nie jestem przekonany. Twój przykład jest w porządku, ale po prostu oznacz inny styl kodowania, a nie „prawdziwy właściwy” - który nie istnieje. Należy pamiętać, że większość języków jest obecnie wieloparadigmowych i rzadko są czystymi jednostkami organizacyjnymi. Trzymanie się tak mocno przy „czystych koncepcjach OO”. Na przykład - nie możesz użyć DI w swoim przykładzie, ponieważ logikę drukowania połączono z podklasą. Drukowanie nie powinno być częścią dokumentu - dokument do wydrukowania wystawiłby jego część do wydruku (przez moduł pobierający) na usługę PrinterService.
T. Sar
Właściwe podejście OO do tego, gdybyśmy mieli przejść do podejścia „Czystego OO”, użyłoby czegoś, co implementuje abstrakcyjną klasę PrinterService, prosząc za pośrednictwem wiadomości, co do diabła powinno być wydrukowane - używając pewnego rodzaju GetPrintablePart. Rzeczą, która implementuje PrinterService, może być dowolna drukarka - druk do pliku PDF, na ekran, do TXT ... Twoje rozwiązanie uniemożliwia zamianę logiki drukowania na coś innego, co kończy się bardziej sprzężoną i mniej konserwowalną niż twój „zły” przykład.
T. Sar
Podsumowując: Getters i Setters nie są źli ani nie łamią OOP - są ludzie, którzy nie rozumieją, jak z nich korzystać. Twój przykład jest przykładem podręcznika osób, które nie rozumieją, jak działa DI. Przykład, który „poprawiłeś” był już włączony w DI, odsprzęgnięty, można go łatwo wyśmiewać… Naprawdę - pamiętasz całą rzecz „Wolę kompozycję niż dziedziczenie”? Jeden z filarów OO? Po prostu rzuciłeś go przez okno, zmieniając kod. To nie poleciałoby w żadnej poważnej recenzji kodu.
T. Sar
76

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 nie używamy tylko pól publicznych?

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.

Michael Borgwardt
źródło
20
„[G] etery i setery niekoniecznie są prostym dostępem” - myślę, że to kluczowy punkt. Jeśli Twój kod uzyskuje dostęp do pola według nazwy, nie możesz później zmienić tego zachowania. Jeśli używasz obj.get (), możesz.
Dan Ambrogio
4
@jrh: Nigdy nie słyszałem, by nazywało się to złą praktyką w Javie i jest to dość powszechne.
Michael Borgwardt,
2
@MichaelBorgwardt Ciekawe; trochę nie na temat, ale zawsze zastanawiałem się, dlaczego Microsoft odradza to robić dla C #. Sądzę, że te wytyczne sugerują, że w języku C # jedyne, co można ustawić, to konwersja wartości podanej na jakąś inną wartość używaną wewnętrznie, w sposób, który nie może zawieść lub mieć niepoprawną wartość (np. Mając właściwość PositionInches i PositionCentimeters gdzie automatycznie konwertuje wewnętrznie na mm)? Nie jest to świetny przykład, ale jest to najlepszy, jaki mogę teraz wymyślić.
jrh
2
@jrh to źródło mówi, aby nie robić tego dla getterów, w przeciwieństwie do seterów.
Captain Man,
3
@Gangnus i naprawdę widzisz, że ktoś ukrywa logikę w getter / seter? Jesteśmy do tego tak przyzwyczajeni, że 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.
Izbassar Tolegen
17

Myślę, że sedno sprawy wyjaśniono w twoim komentarzu:

Całkowicie zgadzam się z twoją myślą. Ale gdzieś musimy załadować obiekty danymi. Na przykład z XML. Obecne platformy, które go obsługują, robią to za pośrednictwem programów pobierających / ustawiających, co obniża jakość naszego kodu i naszego sposobu myślenia. Lombok nie jest sam w sobie zły, ale jego istnienie pokazuje, że mamy coś złego.

Problemem jest mieszanie modelu danych trwałości z aktywnym modelem danych.

Aplikacja zazwyczaj będzie miała wiele modeli danych:

  • model danych do komunikacji z bazą danych,
  • model danych do odczytu pliku konfiguracyjnego,
  • model danych do rozmowy z inną aplikacją,
  • ...

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:

  • niezależny : abyś mógł dodawać / usuwać właściwości na BOM zgodnie z własnymi wymaganiami bez konieczności zmiany wszystkich klientów / serwerów, ...
  • izolowane : tak, aby wszystkie obliczenia były wykonywane na BOM, na którym żyją niezmienniki, i że przejście z jednej usługi do drugiej lub uaktualnienie jednej usługi nie powoduje falowania w bazie kodu.

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ę).

Matthieu M.
źródło
3
Nie tworzyłbym obiektów pobierających i ustawiających dla obiektów komunikacyjnych. Po prostu upublicznię pola. Po co tworzyć więcej pracy niż jest to przydatne?
immibis
3
@immibis: Zgadzam się ogólnie, jednak, jak zauważono w PO, niektóre frameworki wymagają getterów / seterów, w takim przypadku po prostu musisz się zastosować. Zauważ, że OP używa biblioteki, która tworzy je automatycznie przy zastosowaniu pojedynczego atrybutu, więc to dla niego niewiele pracy.
Matthieu M.,
1
@Gangnus: Chodzi tutaj o to, że pobierające / ustawiające są izolowane do warstwy granicznej (I / O), gdzie brud jest normalny, ale reszta aplikacji (czysty kod) nie jest zanieczyszczona i pozostaje elegancka.
Matthieu M.,
2
@kubanczyk: oficjalna definicja to Business Object Model, o ile wiem. Odpowiada tutaj temu, co nazwałem „wewnętrznym modelem danych”, modelem danych, na którym wykonujesz logikę w „czystym” rdzeniu aplikacji.
Matthieu M.,
1
To jest pragmatyczne podejście. Żyjemy w hybrydowym świecie. Gdyby biblioteki zewnętrzne mogły wiedzieć, jakie dane przekazać do konstruktorów BOM, mielibyśmy tylko BOM.
Adrian Iftode,
12

Rozważ następujące..

Masz Userklasę z właściwością int age:

class User {
    int age;
}

Chcesz to zwiększyć, aby Usermieć datę urodzenia, a nie tylko wiek. Korzystanie z gettera:

class User {
    private int age;

    public int getAge() {
        return age;
    }
}

Możemy zamienić int agepole na bardziej złożone LocalDate dateOfBirth:

class User {
    private LocalDate dateOfBirth;

    public int getAge() {
        LocalDate now = LocalDate.now();
        int year = ...; // calculate using dateOfBirth and now
        return year;
    }

    // other behaviors can now make use of 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 @Dataadnotacja 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.

Hermetyzacja służy do ukrywania wartości lub stanu obiektu danych strukturalnych w klasie

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ż @Getteri @Setterniezależnie - wykorzystuj to, czego wymagają twoje wymagania.

Vince Emigh
źródło
2
Nie ma to związku z dodawaniem @Dataadnotacji do typu, który z założenia dekapsułkuje wszystkie pola
Caleth
1
Nie, pola nieukryte . Ponieważ wszystko może nadejść isetAge(xyz)
Caleth
9
@Caleth Pola ukryte. Zachowujesz się tak, jakby setery nie mogły mieć warunków wstępnych i końcowych, co jest bardzo częstym przypadkiem użycia seterów. Zachowujesz się tak, jakby właściwość field ==, która nawet w językach takich jak C #, po prostu nie jest prawdą, ponieważ oba są zwykle obsługiwane (jak w C #). Pole można przenieść do innej klasy, można go zamienić na bardziej złożoną reprezentację ...
Vince Emigh
1
I mówię, że to nie jest ważne
Caleth,
2
@Caleth Jak to nie jest ważne? To nie ma sensu. Mówisz, że kapsułkowanie nie ma zastosowania, ponieważ uważasz, że nie jest to ważne w tej sytuacji, nawet jeśli ma zastosowanie z definicji i czy ma to zastosowanie?
Vince Emigh,
11

Enkapsulacja każe mi uczynić wszystkie lub prawie wszystkie pola prywatnymi i ujawnić je autorom / ustawiającym.

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.

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.

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 @Datanie więcej niż wygenerowania pobierające / ustawiające: Generuje również toString(), hashCode()i equals(Object), co może być bardzo cenne.

I czy enkapsulacja naprawdę ma sens teraz w programowaniu w prawdziwym życiu?

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.

meriton
źródło
1
wymusza niezmienniki? Czy to angielski Czy mógłbyś to wyjaśnić nie-Anglikowi? Nie bądź zbyt skomplikowany - niektórzy ludzie nie znają wystarczająco dobrze angielskiego.
Gangnus,
Lubię twoją podstawę, lubię twoje myśli (+1), ale nie praktyczny wynik. Wolę używać pola prywatnego i specjalnej biblioteki do ładowania danych za ich pomocą refleksji. Nie rozumiem tylko, dlaczego podajesz odpowiedź jako argument przeciwko mnie?
Gangnus,
@Gangnus Niezmiennik to warunek logiczny, który się nie zmienia (tzn. Nie, a zmienne znaczenie zmienia się / zmienia). Wiele funkcji ma reguły, które muszą być prawdziwe przed i po ich wykonaniu (nazywane warunkami wstępnymi i późniejszymi), w przeciwnym razie w kodzie występuje błąd. Na przykład funkcja może wymagać, aby jej argument nie był zerowy (warunek wstępny) i może zgłosić wyjątek, jeśli jej argument jest zerowy, ponieważ taki przypadek byłby błędem w kodzie (tj. W informatyce, kod złamał niezmienny).
Pharap
@Gangus: Nie jestem pewien, gdzie refleksja wchodzi w tę dyskusję; Lombok to procesor adnotacji, tj. Wtyczka do kompilatora Java, która emituje dodatkowy kod podczas kompilacji. Ale jasne, możesz użyć lomboka. Mówię tylko, że w wielu przypadkach pola publiczne działają równie dobrze i są łatwiejsze do skonfigurowania (nie wszystkie kompilatory automatycznie wykrywają procesory adnotacji ...).
meriton
1
@Gangnus: Niezmienniki są takie same w matematyce i CS, ale w CS istnieje dodatkowa perspektywa: Z perspektywy obiektu niezmienniki muszą być egzekwowane i ustalane. Z perspektywy osoby wywołującej ten obiekt niezmienniki są zawsze prawdziwe.
meriton
7

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:

class MyClass {
    string MyProperty { get; set; }
}

W każdym razie drobiazgowość rzeczywistej implementacji wciąż bardzo zyskuje na enkapsulacji.

daniu
źródło
Python ma podobną funkcję, w której właściwości mogą mieć przezroczyste obiekty pobierające / ustawiające. Jestem pewien, że jeszcze więcej języków ma podobne funkcje.
JAB
1
„Częstotliwość pobierania / ustawiania IMO jest spowodowana specyfikacją Java Bean, która tego wymaga” Tak, masz rację. A kto powiedział, że to musi być wymagane? Powód, proszę?
Gangnus
2
Sposób C # jest absolutnie taki sam jak ten Lombok. Nie widzę żadnej prawdziwej różnicy. Bardziej praktyczna wygoda, ale oczywiście zła koncepcja.
Gangnus,
1
@Gangnis Specyfikacja tego wymaga. Dlaczego? Sprawdź moją odpowiedź na konkretny przykład tego, dlaczego osoby pobierające nie są tym samym, co wystawianie pól - spróbuj osiągnąć to samo bez osoby pobierającej.
Vince Emigh,
@VinceEmigh gangnUs, proszę :-). (gang-walking, nus - nut). A co z użyciem get / set tylko tam, gdzie jest to szczególnie potrzebne, i masowym ładowaniem na prywatne pola przez odbicie w innych przypadkach?
Gangnus,
6

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 nie używamy tylko pól publicznych? Wydaje mi się, że przeszliśmy długą i trudną drogę, aby powrócić do początku.

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.

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 osobami 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?

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.

wprowadź opis zdjęcia tutaj

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.

JimmyJames
źródło
2
Najbardziej konkretne i mądre myśli. +1. Ale dlaczego nie napisać grupy klas, z których każda będzie masowo ładować / zapisywać prywatne pola do / z jakiejś struktury zewnętrznej? JSON, XML, inny obiekt. Może współpracować z POJO lub być może tylko z polami oznaczonymi do tego adnotacją. Teraz myślę o tej alternatywie.
Gangnus,
@Gangnus Zgaduję, ponieważ oznacza to dużo dodatkowego pisania kodu. Jeśli używane jest odbicie, należy zapisać tylko jedną bryłę kodu serializacji i może on serializować każdą klasę według określonego wzorca. C # obsługuje to poprzez [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ę?
Pharap,
@Pharap Bardzo mi przykro, ale twój komentarz jest dla mnie za krótki. „wykorzystanie efektu refleksji”? A jakie jest sens ukrywania tak różnych rzeczy w jednej klasie? Czy możesz wyjaśnić, co masz na myśli?
Gangnus
@ Gangnus Mam na myśli refleksję , zdolność kodu do sprawdzania własnej struktury. W Javie (i innych językach) możesz uzyskać listę wszystkich funkcji udostępnianych przez klasę, a następnie użyć ich jako sposobu na wyodrębnienie danych z klasy wymaganej do serializacji do np. XML / JSON. Korzystanie z refleksji byłoby jednorazowym zajęciem zamiast ciągłego opracowywania nowych metod zapisywania struktur dla poszczególnych klas. Oto prosty przykład Java .
Pharap
@Pharap Właśnie o tym mówiłem. Ale dlaczego nazywacie to „lewarowaniem”? Alternatywą, o której wspomniałem w pierwszym komentarzu, pozostaje - czy (nie) zapakowane pola powinny mieć specjalne adnotacje, czy nie?
Gangnus
5

Ile możemy zrobić bez getterów? Czy można je całkowicie usunąć? Jakie to stwarza problemy? Czy moglibyśmy nawet zablokować returnsł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, returnspró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:

Wpisz opis zdjęcia tutaj

class Interactor implements InputPort {
    OutputPort out;
    int y = 0;

    Interactor(OutputPort out){
        this.out = out;
    }

    void accumulate(int x) {
        y = y + x;
        out.showAsImage(y);
    }
}

Jeśli potrafisz tak kodować, wybór getters jest wyborem. Nie konieczne zło.

candied_orange
źródło
Tak, tacy pobierający / ustawiający mają wielki sens. Ale czy rozumiesz, że chodzi o coś innego? :-) +1 w każdym razie.
Gangnus,
1
@Gangnus Dziękujemy. Jest wiele rzeczy, o których możesz mówić. Jeśli chciałbyś chociaż wymienić ich nazwy, mógłbym do nich wejść.
candied_orange
5

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 @Datai 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! @Dataadnotacje 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.

JacquesB
źródło
Kapsułkowanie powinno pozwolić nam na polimorfizm i bezpieczeństwo. Pobiera / sety zezwala na pierwsze, ale zdecydowanie przeciwstawia się drugiemu.
Gangnus,
Jeśli powiesz, że używam gdzieś dogmatu, proszę powiedz, co z mojego tekstu to dogmat. I nie zapominaj, że niektórych myśli, których używam, nie lubię ani nie zgadzam się z nimi. Używam ich do wykazania sprzeczności.
Gangnus,
1
„GangNus” :-). Tydzień temu pracowałem nad projektem dosłownie pełnym wywołań pobierających i ustawiających na kilku warstwach. Jest to absolutnie niebezpieczne, a co gorsza, obsługuje ludzi w niebezpiecznym kodowaniu. Cieszę się, że zmieniłem pracę wystarczająco szybko, ponieważ ludzie przyzwyczaili się do tego. Więc nie sądzę, widzę to .
Gangnus,
2
@jrh: Nie wiem, czy istnieje formalne wyjaśnienie jako takie, ale C # ma wbudowane wsparcie dla właściwości (getters / setters z ładniejszą składnią) i dla typów anonimowych, które są typami tylko z właściwościami i bez zachowania. Dlatego projektanci C # celowo odeszli od metafory wiadomości i stosowali zasadę „mów, nie pytaj”. Rozwój C # od wersji 2.0 wydaje się bardziej inspirowany językami funkcjonalnymi niż tradycyjną czystością OO.
JacquesB
1
@jrh: Zgaduję teraz, ale myślę, że C # jest dość zainspirowany funkcjonalnymi językami, w których dane i operacje są bardziej oddzielne. W architekturach rozproszonych perspektywa również zmieniła się z zorientowanej na komunikaty (RPC, SOAP) na zorientowaną na dane (REST). I przeważały bazy danych, które ujawniają i działają na danych (nie obiektach „czarnej skrzynki”). Krótko mówiąc, myślę, że fokus został przeniesiony z wiadomości między czarnymi skrzynkami na ujawnione modele danych
JacquesB
3

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:

class DataClass 
{
    private int data;

    public int getData() { return data; }
    public void setData(int data) { this.data = data; } 
}

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.

class DataClass implements IDataInterface
{
    private int data;

    @Override public int getData() { return data; }
    @Override public void setData(int data) { this.data = data; }
}

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ę:

void doSomethingWithData(IDataInterface data) { data.setData(...); }

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:

player.setPosX(player.getPosX() + 1);

Bez enkapsulacji wyglądałoby to tak:

player.posX++;

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ę:

class VerticalList implements ...
{
    private int posX;
    private int posY;
    ... //other members

    public void setPosition(int posX, int posY)
    {
        //change position and move all the objects in the list as well
    }
}

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:

data class DataClass(var data: Int)

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.

BananyaDev
źródło
Czy mógłbyś podać tutaj przykład niewłaściwego użycia enkapsulacji? To może być interesujące. Twój przykład jest raczej przeciwko brakowi enkapsulacji.
Gangnus
1
@Gangnus Dodałem kilka przykładów. Bezużyteczne enkapsulacja jest na ogół niewłaściwe, a także z mojego doświadczenia, że ​​programiści API starają się zbyt mocno, aby zmusić cię do korzystania z API w określony sposób. Nie podałem na to przykładu, ponieważ nie miałem łatwego do przedstawienia. Jeśli znajdę taki, na pewno go dodam. Myślę, że większość komentarzy przeciwko enkapsulacji jest w rzeczywistości przeciwko składni enkapsulacji, a nie samej enkapsulacji.
BananyaDev,
Dzięki za edycję. Znalazłem jednak drobną literówkę: „publice” zamiast „public”.
jrh
2

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.

glennsl
źródło
Chociaż jest to dobry pomysł teoretycznie, pamiętaj, że spowodowanie wyjątku, że wersja 2 interfejsu API wyrzuci wyjątki, że wersja 1 nie złamałaby strasznie użytkownikom twojej biblioteki lub klasy (i być może po cichu!); Jestem nieco sceptycznie nastawiony do tego, że każdą poważną zmianę można wprowadzić „za kulisami” w większości tych klas danych.
jrh
Przepraszamy, to nie jest wyjaśnienie. Fakt, że używanie pól jest zabronione w interfejsach, wynika z idei enkapsulacji. A teraz rozmawiamy o tym. Opierasz się na omawianych faktach. (zauważ, że nie mówię za ani przeciw twoim myślom, tylko o tym, że nie mogą być tutaj użyte jako argumenty)
Gangnus,
@Gangnus Słowo „interfejs” ma znaczenie poza słowem kluczowym Java: dictionary.com/browse/interface
glennsl
@jrh To zupełnie inna sprawa. Możesz użyć go do argumentowania przeciwko enkapsulacji w pewnych kontekstach , ale w żaden sposób nie unieważnia to argumentów.
glennsl
1
Stara enkapsulacja bez masowego get / set dawała nam nie tylko polimorfizm, ale także bezpieczeństwo. A masy ustawiają / uczą nas złej praktyki.
Gangnus,
2

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:

public class User {
  private String first = "";
  private String last = "";

  public String getFirstName() {
    return this.first;
  }
  public void setFirstName(String s) {
    this.first = s;
  }

  public String getLastName() {
    return this.last;
  }
  public void setLastName(String s) {
    this.last = s;
  }
}

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ę:

System.out.println(user.getFirstName() + " " + user.getLastName());

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:

public class UserName {
   public final String first;
   public final String last;
   public UserName(String first, String last) { ... }
}

public class User
   private UserName name;
   public UserName getName() { return this.name; }
   public setName(UserName n) { this.name = n; }
}

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.

Jaki jest sens całego tego cyklu? I czy enkapsulacja naprawdę ma sens teraz w programowaniu w prawdziwym życiu?

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.

Joeri Sebrechts
źródło
Fantastyczny. Powiedziałbym, że podniosłeś dyskusję o poziom wyżej. Może dwa. Naprawdę myślałem, że rozumiem enkapsulację, a czasem lepszą niż standardowe książki, i naprawdę obawiałem się, że praktycznie ją tracimy z powodu pewnych przyjętych zwyczajów i technologii, i to był prawdziwy temat mojego pytania (może nie sformułowany w dobry sposób), ale pokazałeś mi naprawdę nowe strony enkapsulacji. Zdecydowanie nie jestem dobry w wielozadaniowości, a pokazałeś mi, jak szkodliwe może być zrozumienie innych podstawowych zasad.
Gangnus,
Ale nie mogę oznaczyć post jako na odpowiedź, bo nie chodzi o masowe ładowanie danych do / z obiektów, ze względu na problem, który pojawia się takie platformy jak fasola i Lombok. Może mógłbyś rozwinąć swoje myśli w tym kierunku, proszę, jeśli masz czas? Sam muszę jeszcze przemyśleć konsekwencje, jakie wasza myśl niesie z tym problemem. I nie jestem pewien, czy jestem do tego wystarczający (pamiętajcie, złe tło wielowątkowości: - [)
Gangnus 10.10.17
Nie korzystałem z lomboka, ale jak rozumiem, jest to sposób implementacji określonego poziomu enkapsulacji (pobierających / ustawiających na każdym polu) z mniejszym pisaniem. Nie zmienia idealnego kształtu interfejsu API dla Twojej problematycznej domeny, jest tylko narzędziem do szybszego pisania.
Joeri Sebrechts,
1

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()i p.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()i p.theta(), ale kod klienta, który wywołuje p.x()i p.y()pozostaje ważny. Sama klasa w przejrzysty sposób przekształca się z wewnętrznej postaci polarnej, czyli y()teraz return r * sin(theta);. (W tym przykładzie ustawienie tylko x()lub y()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”.

Davislor
źródło
2
To nie jest tak miłe, jak to widzisz. Zmiana wewnętrznej reprezentacji fizycznej naprawdę sprawia, że ​​obiekt jest inny. I źle , że wyglądają tak samo, ponieważ mają różne cechy. System Kartezjusza nie ma specjalnych punktów, system polarny ma jeden.
Gangnus,
1
Jeśli nie podoba ci się ten konkretny przykład, z pewnością możesz pomyśleć o innych, które naprawdę mają wiele równoważnych reprezentacji lub nowszą reprezentację, którą można przekonwertować na i ze starszej.
Davislor,
Tak, możesz bardzo produktywnie wykorzystać je do prezentacji. W interfejsie użytkownika może być szeroko stosowany. Ale czy nie zmuszasz mnie do odpowiedzi na moje pytanie? :-)
Gangnus,
Bardziej wydajny w ten sposób, prawda? :)
Davislor,
0

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?

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?

Graham
źródło
Mówisz o pytaniach studenta pierwszego roku SW. Jesteśmy już gdzie indziej. Nigdy bym tego nie powiedział, a nawet dałbym ci plus za sprytne odpowiedzi, gdybyś nie był tak niegrzeczny w formie swojej odpowiedzi.
Gangnus,
0

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.

Jozuego
źródło
Witamy w świecie Java. (C # też). *westchnienie*. Ale jeśli chodzi o używanie get / set do ukrywania wewnętrznej reprezentacji, bądź ostrożny. Chodzi tylko o format zewnętrzny, jest OK. Ale jeśli chodzi o matematykę lub, co gorsza, fizykę lub inną rzeczywistą rzecz, różne wewnętrzne przedstawienie ma różne cechy. mianowicie mój komentarz do softwareengineering.stackexchange.com/a/358662/44104
Gangnus
@Gangnus: Ten przykład jest najbardziej niefortunny. Mieliśmy ich sporo, ale obliczenia były zawsze dokładne.
Joshua
-1

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”.

HorriblePerson
źródło
W tych „nowoczesnych” językach interfejs API wygląda jak dostęp do pola, foo.barale 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 metod foo.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!).
Warbo,
1
1. Ukrywanie fizyki nie zawsze jest tak dobre. spójrz mój komentarz na softwareengineering.stackexchange.com/a/358662/44104 . 2. Nie mówię o tym, jak trudne lub proste jest tworzenie gettera / setera. C # ma absolutnie ten sam problem, o którym mówimy. Brak inkapsulacji z powodu złego rozwiązania ładowania informacji masowych. 3. Tak, rozwiązanie może opierać się na składni, ale proszę na kilka innych sposobów niż wspomniany. Te są złe, mamy tutaj dużą stronę wypełnioną wyjaśnieniami. 4. Rozwiązanie nie musi być oparte na składni. Można to zrobić w limitach Java.
Gangnus,
-1

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, 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.

radarbob
źródło
Tak, właściwości interfejsu użytkownika mają szczególny sens, są to publiczne i ładne reprezentacje pól. Słuszna uwaga.
Gangnus,
„Więc dlaczego my / oni po prostu nie zastosowaliśmy rzeczywistych metod?” Technicznie zrobili to w wersji .Net. Właściwości to cukier składniowy dla funkcji get i set.
Pharap,
„idiotokracja” ? Nie masz na myśli „ idiosynkrazji ”?
Peter Mortensen,
Miałem na myśli idiokrację
radarbob