Czym w ogóle jest enkapsulacja (zwana również hermetyzacją)? Tak z grubsza:
Jest to ukrywanie. Nasze obiekty powinny być skryte i nieśmiałe 😉 i nie uzewnętrzniać się… to znaczy swoich bebeszków. Co ukrywamy? Wszystko co się da, przede wszystkim pola klasy i w C# właściwości (properties). W Javie zamiast właściwości są metody getX setX, te same gettery i settery tylko w innej formie. Najlepiej je ukryć, a raczej ich nie używać… ale życie często jest inne i musimy je udostępnić (nie, nie musimy, świadomie wtedy łamiemy enkapsulację bo jest łatwiej – co nie znaczy zawsze lepiej). Wtedy chociaż settery powinny być ukryte lub nie istnieć w ogóle.
A jak ukrywamy? Uzyskujemy to między innymi przez „słówko” private, bądź jeśli trzeba protected (np ze względu na użycie NHibernate’a). W Javie do tego jest chyba jeszcze opcja pośrednia – brak modyfikatora dostępu (niech mnie proszę ktoś poprawi jeśli się mylę). Jak wiecie, private oznacza widoczność danej zmiennej tylko w obrębie naszej klasy, a protected w obrębie samej klasy oraz klas z niej dziedziczących.
Więc jak należy ustawiać wartości takim zmiennym? Idealnie byłoby robić to tylko w:
– konstruktorze obiektu
– poprzez odpowiednią metodę realizującą daną potrzebę biznesową, np public void RecalculateWith(int someParam);
Przejdźmy teraz do tego – po co w ogóle stosować enkapsulację?
0. Bo tak w szkole uczą.
1. Dzięki temu uzyskamy spójność działania naszej aplikacji.
2. Promujemy pisanie kodu obiektowego.
ad 0. Argument mówi sam za siebie 😉
ad 1.Krótki naiwny i trywialny przykład kodu, jak nie tworzyć obiektów:
1 2 3 4 5 6 7 8 9 10 11 |
public class MyClass { public bool foo; public int bar; public MyClass(bool foo, int bar) { this.foo = foo; this.bar = bar; } } |
Zauważ, że nie ma mowy tutaj o enkapsulacji, mimo tego, że nie ma domyślnego pustego konstruktora więc gdy tworzę obiekt to będzie on zawsze „poprawny”.
Załóżmy, że gdy zmieniam foo, bar powinien być zawsze też zmieniony, ze względu na pewne wymaganie biznesowe.
Mogę więc zrobić to tak:
1 2 3 4 5 6 7 8 |
myClassInstance = new MyClass(true, 54); // A później w programie: myClassInstance.foo = false; myClassInstance.bar = 55; //A jeszcze później: myClassInstance.foo = false; myClassInstance.bar = 40; //I w tysiącu innych miejsc to samo. |
Ale powoduje to kilka problemów:
– przede wszystkim mogę zapomnieć albo nawet nie wiedzieć, że powinienem to zrobić „na raz”.
Kiedyś, spędziłem kilka dni grzebiąc w spagetti code, szukając właśnie tego typu buga – wtedy jeszcze nie wiedziałem, co jest jego przyczyną. Gdy rozmawiałem o tym później z leadem, powiedział „Dziwne, przecież każdy wie, że należy ustawiać foo razem z bar”. No cóż, gość który ustawił jedno bez drugiego najwyraźniej zapomniał albo nie wiedział o tym. Dziwne prawda? 😉 Ja wcześniej też nie wiedziałem. I za pół roku pewnie i tak zapomniałbym.
– drugi problem w tym przykładzie z flagą jest taki, że jest tam potrzebna dodatkowa wiedza co należy zmienić i kiedy. Np: czy zmieniam bar tylko kiedy ustawiam foo, czy kiedy rzeczywiście zmieniam wartość foo na inną niż była wcześniej? Na jaką wartość i kiedy?
Oczywiście o tym też możemy nie pamiętać, ale możemy to „obsłużyć” w kodzie:
1 2 3 4 5 6 7 8 9 10 11 12 |
bool temp = myClassInstance.foo; myClassInstance.foo = false; if (temp == true) myClassInstance.bar = 55; //i w innym miejscu: bool temp = myClassInstance.foo; myClassInstance.foo = false; if (temp == false) myClassInstance.bar = 40; //I w tysiącu innych miejsc to samo. |
Jak widać… nie widać o co chodzi i dlaczego. Nie dość że bezpieczniej bo spójnie to jeszcze czytelniej byłoby zrobić to na przykład tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
private bool foo; private int bar; public MyClass(bool foo, int bar) { this.foo = foo; this.bar = bar; } public void UpdateMyClassBecauseOfSomeBusinessReason() { foo = !foo; bar++; } //lub jeśli potrzebujemy to tak public void UpdateMyClassBecauseOfSomeBusinessReason(bool param) { if (foo == param) return; foo = param; bar++; } |
W zależności od potrzeb, czyli wymagań, ten kod mógłby oczywiście wyglądać inaczej i inne reguły decydować co i kiedy ma być zmieniane. Dzięki zastosowaniu enkapsulacji, logika zmian będzie tylko w jednym miejscu, nie wycieknie nigdzie poza sam obiekt gdzie jej miejsce. Nikt się też nie pomyli i nie zrobi czegoś „inaczej”. Wystarczy że wywoła metodę. Łatwiej to też będzie testować – w jednym miejscu a nie wielu w aplikacji.
Ukrywając wnętrzności obiektu przed korzystającymi z niego innymi obiektami, a tak naprawdę przed programistami piszącymi kod :), nie dasz im i sobie możliwości napisania niepoprawnego kodu. Chcąc nie chcąc będą/będziesz musieli/musiał skorzystać z przygotowanej wcześniej metody.
ad2. Argument, który się rozumie samo przez się. Kod zaprezentowany jako ten „zły i niedobry, a do tego mroczny” nie jest kodem obiektowym zgodnym z OOP, bo nie ma enkapsulacji. Enkapsulacja to jedno z głównych założeń OOP.
Mam nadzieję, że po przeczytaniu tego zrozumiesz nie tylko podstawy tego jak, ale przede wszystkim (moim zdaniem to nawet ważniejsze) dlaczego warto stosować enkapsulację.
W Javie modyfikator domyślny czyli (bez modyfikatora) to inaczej w .NET-cie internal. w Javie w ramach pakietu, w .NET w ramach biblioteki więc to jest takie pośrednie podejście do chowania obiektów.
Fajnie chyba rozumiem ale mam z tym czasem problem. Co zrobić jak tych obiektów, które chce ukryć jest więcej. Mam taką zasadę, że konstruktor lub metoda nie może mieć więcej niż 3 parametry(żeby było czytelnie) – więc co zrobić jak się okazuje, że nie ma wyboru i trzeba dołożyć więcej. Wtedy należy wstawić te parametry do kolejnego obiektu?
Dzięki za wyjaśnienie, jak wygląda to w Javie.
Co do Twojego problemu ilości ukrywanych zmiennych: Jeśli jest ich więcej, to więcej będzie ukrytych (badum tss). Na poważnie, będzie ich tyle, ile trzeba. Jeśli jest ich za dużo, może to znak, że klasa jest za duża i ma za wiele odpowiedzialności?
Co do kwestii ilości parametrów konstruktora czy metody:
To zależy. W głowie mam teraz kilka przypadków, ale żaden może nie pasować do Twojego. Ok to kilka możliwości:
Może to być oznaka tego, że metoda bądź klasa ma za dużą odpowiedzialność i należy ją podzielić.
Można opakować te parametry w jedno DTO zwłaszcza jeśli potem jest przesyłane dalej. Ale jeśli miało by to być tylko DTO tworzone tylko w tym celu, tuż przed wrzuceniem do konstruktora, a w nim po prostu brane z niego wartości – to takie podejście nie ma sensu.
Co by miało sens, to podzielenie głównego obiektu na mniejsze i tworzenie ich na przykład wcześniej i wrzucanie powiedzmy dwóch gotowych obiektów jako parametry do konstruktora.
Na przykład najpierw tworzymy obiekty Engine i Brake, a dopiero potem te dwa wrzucamy do Car.
Ale z drugiej strony, równie dobrze możemy to zrobić w konstruktorze, mając tę logikę w jednym miejscu. Zależy ile tych parametrów jest i czy mają sens jako osobny obiekt. A jeśli nie konstruktor to co?
Można się wtedy zastanowić nad jakąś fabryką, chociażby w postaci prostej metody statycznej. Czy to ma sens i czy skórka jest warta wyprawki zależy jak bardzo skomplikowana jest logika tworzenia danego obiektu, kiedy i gdzie oraz jak często go tworzymy.
Mam nadzieję, że trochę Ci rozjaśniłem.
Dziwne. Do tej pory myślałem, że properties to jest właśnie sposób na enkapsulację memberów. Po to mamy w getgerach setterach blok kodu aby zadbać o wszelkie operację niezbędne przed i po ustawieniem membera. Nie bardzo rozumiem dlaczego przekonujesz, że gettery i settery trzeba ukryć i najlepiej ich nie używać. Jasne czasem jest jakiś BusinessReason alby utworzyć taką konstrukcje jak pokazałeś w przykładzie, ale bywa i tak że samo properties mogą zadbać o enkapsulację i spójność danych.
Bardzo dobre pytanie. Myślę, że tej odpowiedzi brakuje w tym wpisie… bądź właśnie następnym który przygotuje na ten temat. 🙂
Tak w skrócie: masz rację gettery i settery, czy to poprzez properties w .Net czy zwykłe metody w Javie, są sposobem na zwiększenie enkapsulacji i spójności danych (możemy je wykorzystać do umieszczenia bardzo prostej logiki pomagającej w zachowaniu spójności, ale ważne, żeby była prosta).
Rozbicie dostępu do pola klasy na odczyt i zapis pola pomaga właśnie enkapsulować jeden z tych elementów – najczęściej właśnie zapis, jednocześnie pozwalając na swobodny odczyt pola. I to jest świetne, bo nie musimy wybierać jednego z dwojga.
Jednak samo stosowanie getterów i setterów nie sprawia że „mamy” enkapsulacje. To tylko jedno z narzędzi i wcale nie jest wymagane do tego, ani samo jego użycie niczego nie daje, jeśli zrobimy to źle. Np gdy getter i setter są publiczne, to jest to równoznaczne z publicznym polem, jeśli chodzi o enkapsulowanie dostępu. Czyli równoznaczne z brakiem enkapsulacji. Upublicznianie pola do odczyty również łamie enkapsulacje. Co jak pisałem często jest łatwiejsze i nie znaczy ze złe tak ogólnie, ale warto być tego świadomym.
Napisze o tym więcej w osobnym wpisie i podam bardziej rozbudowane przykłady. Dziękuję raz jeszcze za motywację do tego 🙂