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.
"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?