Zwracanie obiektu w akcesorze a hermetyzacja

0

W jaki sposób mam skonstruować metodę akcesora getTrzymaneKarty(), żeby nie złamać zasady hermetyzacji?
Mam klasę Uzytkownik z polem: private ArrayList<Karta> trzymaneKarty = new ArrayList<Karta>(13); Mam też oczywiście klasę Karta. Jeśli w klasie Uzytkownik zrobię metodę

public ArrayList<Karta> getTrzymaneKarty() {
		return trzymaneKarty;
	}

to wystarczy w dowolnej klasie napisać nazwaUzytkownika.getTrzymaneKarty().add(new Karta(3,3)); aby zmienić prywatne pole w klasie Uzytkownik. Pomyślałem, że trzeba zwrócić kopię, ale oczywiście

public ArrayList<Karta> getTrzymaneKarty() {
		kopia = trzymaneKarty;
		return kopia;
	}

taki zapis nic nie da, bo kopia odwoła się do tego samego obiektu i zmieniając kopię jego też zmienimy. Wobec tego pomyślałem, żeby przepisać całą listę do nowej listy i zwrocić tą nową.

ArrayList<Karta> kopia = new ArrayList<Karta>(13);
kopia.addAll(trzymaneKarty);
return kopia;

Jednak nie wiem czemu nagle pojawiać się zaczał błąd przy linii if (temp.get(j).getLiczbaNaKarcie() == temp.get(k).getLiczbaNaKarcie())
"Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 10, Size: 10
at java.util.ArrayList.rangeCheck(Unknown Source)
at java.util.ArrayList.get(Unknown Source)
at Glowna.wyrzucPary(Glowna.java:85)
at Glowna.main(Glowna.java:22)"

w zasadzie o to też chciałbym zapytać dlaczego przy normalnym 'return trzymaneKarty' błędu nie było, a w tej wersji z kopią się pojawił. Mogę przekopiować cały kod, tylko nie wiem czy to konieczne.

Wracając do głównego problemu. Gdy zacząłem się zastanawiać nad powyższym błędem z kopią, dotarło do mnie, że w ten sposób zwrocę rzeczywiście inną listę i zmiana jej nie zmieni tej prywatnej listy z klasy, ale zwróce w niej te same obiekty klasy Karta co w oryginalnej liście! Wtedy nawet gdyby poprawnie działała wersja z przepisaniem listy do kopii, zabezpieczyłbym się chyba przed
nazwaUzytkownika.getTrzymaneKarty().add(new Karta(3,3));
bo nie dodałoby to już tej karty do trzymanych kart użytkownika. Wystarczy jednak napisać
nazwaUzytkownika.getTrzymaneKarty().get(1).setId(4);
i zmienimy obiekt Karta posiadany przez danego użytkownika w swojej liście trzymaneKarty.

Nie wiem czy zacząłem przesadzać, bo wygląda to dość śmiesznie, kolejno get.get.set na jednym obiekcie. Można zostawiać taką furtkę? Według mnie nie powinno tak być. Jak to rozwiązać, żeby rzeczywiście mieć hermetyczny akcesor? Ewentualnie poproszę o inne rozwiązanie. Widzę, że jest metoda clone(), ale tu sprawa wygląda jak z przepisywaniem listy. Klonuje listę, ale w niej będą referencje do tych samych obiektów klasy Karta. Znalazłem jakieś przykłady z przesłanianiem metody clone(), żeby uzyskać klonowanie głębokie, ale nie do końca się w tym łapie. Do tego w głowie rodzi mi się "a co jeśli Karta też miałaby w sobie referencje do innych obiektów". Przecież potem się w tym nie da połapać.
Kolejna rzecz, którą napotkałem to refleksje. To już dla mnie coś chyba nie do ogarnięcia na ten moment. W głowie już mi się miesza. Doradźcie proszę jak do tego podejść. Napisałbym więcej moich wątpliwości i pomysłów, ale na razie poczekam na odpowiedź, bo nikomu nie będzie się chciało takiego długiego posta czytać.

0
  1. Przesadziłeś i tyle. Jesteśmy dorosłymi ludźmi. Jak ktoś będzie chciał namieszać w bebechach danej klasy to i tak sobie poradzi. Powinieneś zwracać normalnie tą kolekcje co masz. Enkapsulacja i hermetyzacja służą między innymi do tego że udostępniasz użytkownikowi tylko interfejs, a nie informacje o wewnętrznej budowie obiektów. Zauważ że dzięki takiemu akcesorowi getTrzymaneKarty() user wcale nie wie co to za kolekcja którą dostaje! Przecież ty ją możesz tam w locie konstruować i mu zwracać na bazie jakichś innych obiektów. Albo możesz robić głęboką kopię właśnie. Użytkownik tego nie wie, dlatego normalny użytkownik nie będzie polegał na efektach ubocznych ;]
  2. A co jest dziwnego w refleksji? Ot jest to po prostu mechanizm pozwalający manipulować metadanymi obiektów (np. zmieniać modyfikatory dostępu, pobierać listę metod itd) i dobierać sie do bebechów obiektów. W praktyce refleksja jest stosowana tylko w bardzo szczególnych przypadkach (np. tworzenie w runtime obiektów które nie były znane w chwili pisania kodu). Jeśli musisz gdzies jej użyć to na 99% bardzo źle coś zaprojektowałeś.
0

W Javie nie ma głębokiego consta takiego jak w C/ C++, ale nawet ten const z C/ C++ nie da w 100% zabezpieczenia bez modyfikacji co bardziej skomplikowanych struktur danych (tzn dokładnie wskaźniki muszą być oconstowane, bo nie 'dziedziczą' consta).

Jest funkcja java.util.Collections.unmodifiableList, która działa jak płytki const, tzn zabezpiecza przed dodawaniem, podmienianiem, usuwaniem, etc referencji bezpośrednio w liście, no ale sekwencje typu:

lista.get(x).setCośtam()

nadal będą działać, bo ta unmodifiableList nie modyfikuje zwracanych obiektów, w szczególności nie zabezpiecza ich przed modyfikacją.

A co do kodu:

ArrayList<Karta> kopia = new ArrayList<Karta>(13);
kopia.addAll(trzymaneKarty);
return kopia;

Możesz go skrócić do:

return new ArrayList<Karta>(trzymaneKarty);

o ile trzymaneKarty jest kolekcją.

0
Shalom napisał(a):

W praktyce refleksja jest stosowana tylko w bardzo szczególnych przypadkach (np. tworzenie w runtime obiektów które nie były znane w chwili pisania kodu). Jeśli musisz gdzies jej użyć to na 99% bardzo źle coś zaprojektowałeś.
Raczej nie muszę, to nie jest mocno skomplikowany projekt. Po prostu gdzieś znalazłem, że może to być rozwiązanie podobnego problemu.

Wibowit napisał(a):

o ile trzymaneKarty jest kolekcją.
tak, to jest ten sam typ ArrayList<Karta>

Rozumiem, że wywołania nazwaUzytkownika.getTrzymaneKarty().get(1).setId(4); są nienaturalne i można tak nie robić, ale wystarczy, że ja sam się zapomnę, albo wrócę do tego za jakiś czas, ewentualnie ktoś inny będzie z tego korzystał i już się robi nieciekawie, bo można dać gdzieś w programie

ArrayList<Karta> karty = jakisUzytkownik.getTrzymaneKarty();
//jakieś operacje wykonywane na karty
innyUzytkownik.setTrzymaneKarty(karty); 

Mając zamiar przekopiowania kart od użytkownika i (po pewnych modyfikacjach) wstawieniu ich nowemu, nie będziemy oczekiwać zmiany kart tego pierwszego użytkownika. Wobec tego chyba nie mogę tego tak zostawić.

Nie ma jakiegoś prostego sposobu na to? Znalazłem jakiś przykład z głębokim kopiowaniem za pomocą przesłaniania metody clone() i chyba go rozumiem, ale czy to najlepszy sposób? Czy każdą klasę trzeba w ten sposób zabezpieczać z myślą, że może jakaś inna będzie miała kolekcję z ich obiektami i dane wypłyną?

Jeszcze ten drugi problem, dlaczego w metodzie getTrzymaneKarty() to return new ArrayList<Karta>(trzymaneKarty);oraz to return trzymaneKarty nie wyrzuca mi żadnych błędów, a ta moja wersja

ArrayList<Karta> kopia = new ArrayList<Karta>(13);
kopia.addAll(trzymaneKarty);
return kopia; 

wyrzuca IndexOutOfBoundsException w innej metodzie? Jak to w ogóle możliwe? Skoro lista.get(index) nie wykracza poza zakres listy, to dlaczego nagle na kopii zaczął wykraczać?

0

A nie robisz przypadkiem w kodzie czegoś typu:

for (int i = 0; i < trzymaneKarty.size(); i++) {
  kopia.get(i);
}

?

0

Mając zamiar przekopiowania kart od użytkownika i (po pewnych modyfikacjach) wstawieniu ich nowemu, nie będziemy oczekiwać zmiany kart tego pierwszego użytkownika. Wobec tego chyba nie mogę tego tak zostawić.

No dobra, ale ty tam nic nie kopiujesz. Ty po prostu albo zabierasz karty jednemu i dajesz drugiemu, albo mówisz im "to są wasze wspólne karty". Jeśli każdy ma mieć swoje karty to MUSISZ stworzyć nowe karty (na przykład klonując).

Czy każdą klasę trzeba w ten sposób zabezpieczać z myślą, że może jakaś inna będzie miała kolekcję z ich obiektami i dane wypłyną?

Nie i nikt normalny tak nie robi bo nie ma potrzeby. Tak jak mówiłem, bądźmy dorośli i poważni.

A jak już tak bardzo chcesz te swoje karty zabezpieczyć to jest prosta recepta: napisz klasę TaliaKart która przechowuje karty (np. w List) i udostępnia tylko taki interfejs jaki chcesz. W efekcie user nawet jeśli zrobi getTrzymaneKarty() to nie dostanie List<T> tylko TaliaKart. Ogólnie to jest dobry pomysł i nazywa się Zasadą Separacji Interfejsów. Każdy obiekt powinien posiadać minimalny interfejs potrzebny do swojego działania.

1 użytkowników online, w tym zalogowanych: 0, gości: 1