Kod jak:
public Thing[] getThings(){
return things;
}
To nie ma większego sensu, ponieważ twoja metoda dostępu nie robi nic, tylko bezpośrednio zwraca wewnętrzną strukturę danych. Równie dobrze można po prostu zadeklarować Thing[] things
się public
. Ideą metody dostępu jest stworzenie interfejsu, który izoluje klientów od wewnętrznych zmian i uniemożliwia im manipulowanie rzeczywistą strukturą danych, z wyjątkiem dyskretnych sposobów, na jakie pozwala interfejs. Jak odkryłeś, kiedy cały kod klienta się zepsuł, twoja metoda dostępu tego nie zrobiła - to po prostu zmarnowany kod. Myślę, że wielu programistów ma tendencję do pisania takiego kodu, ponieważ dowiedzieli się gdzieś, że wszystko musi być otoczone metodami dostępu - ale to z powodów, które wyjaśniłem. Robienie tego tylko po to, by „podążać za formą”, gdy metoda dostępu nie służy żadnemu celowi, to po prostu hałas.
Zdecydowanie poleciłbym zaproponowane rozwiązanie, które realizuje niektóre z najważniejszych celów enkapsulacji: Zapewnienie klientom solidnego, dyskretnego interfejsu, który izoluje ich od wewnętrznych szczegółów implementacyjnych twojej klasy i nie pozwala im dotknąć wewnętrznej struktury danych oczekuj w sposób, który uznasz za odpowiedni - „prawo najmniej koniecznego przywileju”. Jeśli spojrzysz na duże popularne frameworki OOP, takie jak CLR, STL, VCL, zaproponowany przez ciebie wzorzec jest powszechny, właśnie z tego powodu.
Czy zawsze powinieneś to robić? Niekoniecznie. Na przykład, jeśli masz klasy pomocnika lub przyjaciela, które są zasadniczo składnikiem twojej głównej klasy robotniczej i nie są skierowane do przodu, nie jest to konieczne - to przesada, która doda wiele niepotrzebnego kodu. W takim przypadku w ogóle nie użyłbym metody dostępu - jak wyjaśniono - jest bez sensu. Po prostu zadeklaruj strukturę danych w sposób, który ogranicza się tylko do głównej klasy, która z niej korzysta - większość języków obsługuje takie sposoby - friend
lub deklarując ją w tym samym pliku, co główna klasa robotnicza itp.
Jedynym minusem, jaki widzę w twojej propozycji, jest to, że kodowanie wymaga więcej pracy (a teraz będziesz musiał ponownie kodować swoje klasy konsumenckie - ale i tak musiałeś / musiałeś to zrobić). Ale to nie jest tak naprawdę wada - musisz to zrobić dobrze, a czasem wymaga to więcej pracy.
Jedną z rzeczy, które sprawiają, że dobry programista jest dobry, jest to, że wiedzą, kiedy dodatkowa praca jest tego warta, a kiedy nie. W dłuższej perspektywie wprowadzenie dodatkowej kwoty w przyszłości przyniesie duże dywidendy - jeśli nie na tym projekcie, to na innych. Naucz się kodować we właściwy sposób i używaj do tego swojej głowy, a nie tylko automatycznie wykonuj określone formularze.
Należy pamiętać, że kolekcje bardziej złożone niż tablice mogą wymagać, aby klasa otaczająca zaimplementowała więcej niż trzy metody tylko w celu uzyskania dostępu do wewnętrznej struktury danych.
Jeśli ujawniasz całą strukturę danych za pośrednictwem zawierającej klasy, IMO musisz pomyśleć, dlaczego ta klasa jest w ogóle enkapsulowana, jeśli nie chodzi tylko o zapewnienie bezpieczniejszego interfejsu - „klasy opakowania”. Mówisz, że klasa zawierająca nie istnieje w tym celu - więc być może coś jest nie tak z twoim projektem. Zastanów się nad podziałem swoich klas na bardziej dyskretne moduły i warstwowaniem ich.
Klasa powinna mieć jeden jasny i dyskretny cel oraz zapewniać interfejs do obsługi tej funkcjonalności - nie więcej. Być może próbujesz połączyć ze sobą rzeczy, które do siebie nie pasują. Gdy to zrobisz, wszystko się zepsuje za każdym razem, gdy będziesz musiał wprowadzić zmianę. Im mniejsze i bardziej dyskretne są twoje zajęcia, tym łatwiej jest to zmieniać: Pomyśl LEGO.
get(index)
,add()
,size()
,remove(index)
, iremove(Object)
. Korzystając z zaproponowanej techniki, klasa zawierająca tę ArrayList musi mieć pięć publicznych metod tylko do delegowania do wewnętrznej kolekcji. A celem tej klasy w programie najprawdopodobniej nie jest hermetyzacja ArrayList, ale raczej zrobienie czegoś innego. ArrayList to tylko szczegół. [...]remove
nie jest tylko do odczytu. Rozumiem, że OP chce upublicznić wszystko - tak jak w oryginalnym kodzie przed proponowaną zmianąpublic Thing[] getThings(){return things;}
To mi się nie podoba.Pytanie: Czy zawsze powinienem całkowicie zawierać wewnętrzną strukturę danych?
Krótka odpowiedź: Tak, przez większość czasu, ale nie zawsze .
Długa odpowiedź: Myślę, że klasy dzielą się na następujące kategorie:
Klasy, które zawierają proste dane. Przykład: punkt 2D. Łatwo jest tworzyć funkcje publiczne, które umożliwiają uzyskanie / ustawienie współrzędnych X i Y, ale można łatwo ukryć dane wewnętrzne bez większych problemów. W przypadku takich klas ujawnianie szczegółów wewnętrznej struktury danych jest nieuzasadnione.
Klasy kontenerów, które hermetyzują kolekcje. STL ma klasyczne klasy kontenerów. Rozważam
std::string
istd::wstring
wśród nich też. Zapewniają bogaty interfejs do czynienia z abstrakcjami, alestd::vector
,std::string
istd::wstring
również dają możliwość, aby uzyskać dostęp do surowych danych. Nie spieszyłbym się z nazwaniem ich źle zaprojektowanymi zajęciami. Nie znam uzasadnienia dla tych klas, które ujawniają swoje surowe dane. Jednak w mojej pracy uznałem za konieczne ujawnienie surowych danych w kontaktach z milionami węzłów siatki i danych w tych węzłach siatki ze względu na wydajność.Ważną rzeczą w ujawnieniu wewnętrznej struktury klasy jest to, że musisz długo i intensywnie myśleć, zanim nadasz jej zielony sygnał. Jeśli interfejs jest wewnętrzny projektu, jego zmiana w przyszłości będzie kosztowna, ale nie niemożliwa. Jeśli interfejs jest zewnętrzny dla projektu (na przykład podczas opracowywania biblioteki, która będzie używana przez innych programistów aplikacji), zmiana interfejsu bez utraty klientów może być niemożliwa.
Klasy, które mają głównie charakter funkcjonalny. Przykłady:
std::istream
,std::ostream
, iteratory pojemników STL. Głupotą jest ujawnianie wewnętrznych szczegółów tych klas.Klasy hybrydowe. Są to klasy, które zawierają pewną strukturę danych, ale także zapewniają funkcjonalność algorytmiczną. Osobiście uważam, że są one wynikiem źle przemyślanego projektu. Jednak jeśli je znajdziesz, musisz zdecydować, czy sensowne jest ujawnianie ich wewnętrznych danych w poszczególnych przypadkach.
Podsumowując: Jedyny raz, kiedy uznałem za konieczne ujawnienie wewnętrznej struktury danych klasy, jest moment, w którym stała się ona wąskim gardłem wydajności.
źródło
Zamiast zwracać bezpośrednio surowe dane, spróbuj czegoś takiego
Zasadniczo zapewniasz niestandardową kolekcję, która przedstawia pożądaną twarz świata. W nowej implementacji
Teraz masz odpowiednią enkapsulację, ukrywasz szczegóły implementacji i zapewniasz wsteczną kompatybilność (za opłatą).
źródło
List
. Metody dostępu, które po prostu zwracają elementy danych, nawet z obsadą, aby uczynić coś bardziej niezawodnym, nie jest tak naprawdę dobrym enkapsulacją IMO. Klasa robotnicza powinna zajmować się tym wszystkim, a nie klientem. „Głupszy” musi być klient, tym bardziej będzie solidny. Nawiasem mówiąc, nie jestem pewien, czy odpowiedziałeś na pytanie ...Powinieneś używać interfejsów do tych rzeczy. Nie pomogłoby to w twoim przypadku, ponieważ tablica Java nie implementuje tych interfejsów, ale powinieneś to zrobić od teraz:
W ten sposób możesz zmienić
ArrayList
naLinkedList
lub cokolwiek innego, i nie złamiesz żadnego kodu, ponieważ wszystkie kolekcje Java (oprócz tablic), które mają (pseudo?) Losowy dostęp, prawdopodobnie zostaną zaimplementowaneList
.Możesz również użyć
Collection
, które oferują mniej metod niż,List
ale mogą obsługiwać kolekcje bez losowego dostępu, lubIterable
które mogą nawet obsługiwać strumienie, ale nie oferują wiele pod względem metod dostępu ...źródło
List
jako klasy pochodne, miałoby to większy sens, ale po prostu „nadzieja na najlepsze” nie jest dobrym pomysłem. „Dobry programista patrzy w obie strony na ulicę jednokierunkową”.List
(lubCollection
przynajmniejIterable
). To jest sedno tych interfejsów i szkoda, że tablice Java ich nie implementują, ale są one oficjalnym interfejsem dla kolekcji w Javie, więc nie jest zbyt daleko idące przypuszczenie, że zaimplementuje je dowolna kolekcja Java - chyba że ta kolekcja jest starsza niżList
, i w takim przypadku bardzo łatwo jest owinąć go AbstractList .List
doArrayList
, ale nie jest tak, że implementacja może być w 100% chroniona - zawsze możesz użyć refleksji, aby uzyskać dostęp do prywatnych pól. Takie postępowanie jest marszczone, ale rzucanie czuje się również (choć nie tak bardzo). Istotą enkapsulacji nie jest zapobieganie złośliwemu hakowaniu - raczej uniemożliwia użytkownikom uzależnienie się od szczegółów implementacji, które możesz chcieć zmienić. Korzystanie zList
interfejsu robi dokładnie to - użytkownicy klasy mogą polegać naList
interfejsie zamiast konkretnejArrayList
klasy, która może się zmienić.Jest to dość częste ukrywanie wewnętrznej struktury danych przed światem zewnętrznym. Czasami jest to przesada, szczególnie w DTO. Polecam to dla modelu domeny. Jeśli w ogóle wymagane jest ujawnienie, zwróć niezmienną kopię. Wraz z tym sugeruję utworzenie interfejsu zawierającego takie metody jak get, set, remove itp.
źródło