24 kwietnia 2008

SCJP – Hermetyzacja, zależność i spójność

Dziś parę słów o terminach, w których wyraża się jakość programu obiektowego – kontynuacja akcji zapowiedzianej w artykule "Przygotowania do SCJP czas zacząć". Nie było łatwo dobrać tłumaczenia do anglojęzycznych zwrotów, ale myślę, że ostatecznie się udało. Będzie więc o hermetyzacji (ang. encapsulation), zależności (ang. coupling), która im prostsza tym lepsza (ang. loose coupling) i spójności (ang. cohesion) która z kolei powinna być możliwie duża (ang. high cohesion).

Na początku zadajmy sobie pytanie – czemu te pojęcia są ważne? Ludzkość nie bez przyczyny przestawiła się na programowanie obiektowe, tą przyczyną nie była też li tylko moda. Programowanie obiektowe ma ułatwiać tworzenie oprogramowania i rzeczywiście ułatwia, ale tylko pod warunkiem, że programy obiektowe są napisane – nie wymagajmy zbyt wiele – chociaż przyzwoicie. I właśnie temu służą omawiane dziś pojęcia – określeniu, jak należy programować, żeby uzyskać z obiektowości maksymalne korzyści. Program obiektowy składa się z klas a pojęcia te określają, jakie te klasy powinny być.

Klasy powinny być hermetyczne. Oznacza to, że klasy powinny ukrywać swoje wewnętrzne struktury i operacje a udostępniać tylko te, dla których udostępnienia zostały powołane, tj. swoje dobrze określone interfejsy. Dla przykładu, jeśli dla implementacji wymaganych mechanizmów posłużyliśmy się pewną strukturą danych, albo kilkoma strukturami to nie powinny być one "widoczne z zewnątrz". Jest to bowiem wewnętrzna sprawa klasy jak jej mechanizmy zostały zaimplementowane i poprzez nie ujawnianie tych szczegółów chcemy zagwarantować, że sposób implementacji będzie można w przyszłości zmienić bez obawy o konieczność modyfikacji innych klas. Tutaj uwaga! Niska hermetyczność klas to tylko potencjalne ryzyko pojawienia się nieprzewidzianych i niepożądanych zależności, natomiast stan faktyczny istnienia takowych to zupełnie inna sprawa. W języku Java hermetyzację osiągamy poprzez odpowiednie stosowanie modyfikatorów dostępu – w miarę możliwości jak najbardziej restrykcyjne. Przejawem dążenia do wysokiej hermetyzacji są też właściwości (ang. properties) znane ze specyfikacji JavaBeans – wszystkie zmienne instancyjne powinny być prywatne i "z zewnątrz" dostępne poprzez metody typu get i set. Klasy powinny być hermetyczne także dlatego, aby nie było możliwe tworzenie instancji reprezentujących niespójny, niepożądany stan. Rozważmy poniższy przykład, klasę reprezentującą wniosek kredytowy.

class LoanApplication {
public float profitMargin;

public long amount;

public LoanApplication(long amount) {
if(amount < Config.AMOUNT_LOW) {
profitMargin = Config.PROFIT_HIGH;
} else {
profitMargin = Config.PROFIT_LOW;
}
}
}

Intencją powyższego kodu jest, aby wnioski kredytowe opiewające na małą sumę skutkowały wysoką marżą a dopiero powyżej pewnej kwoty marża byłaby odpowiednio niższa. Jednak, ponieważ zmienna ‘profitMargin’ jest publiczna, możliwa jest dowolna zmiana jej wartości zaraz po utworzeniu obiektu. Wbrew intencjom legalny jest więc poniższy kod.

class GoodTeller {
private LoanRegistry loanRegistry;

public LoanID submitApplication(long amount, Customer cust) {
LoanApplication loanApplication = new LoanApplication(amount);

loanApplication.profitMargin = 0; // Ups!

if(cust.isTrustworthy()) {
return loanRegistry.grantLoan(loanApplication, cust);
} else {
return null;
}
}
}
Klasy nie powinny być we wzajemnych skomplikowanych zależnościach. Kod jednej klasy nie powinien wykorzystywać cech innej klasy, które wynikają z jej konkretnej implementacji a nie są elementem zaprojektowanego interfejsu. Zagadnienie to jest ściśle powiązane z hermetyzacją w tym sensie, że skomplikowane zależności mogą wystąpić tylko wtedy, kiedy tej hermetyzacji brakuje. W kontekście egzaminu SCJP, ale nie tylko ważne jest, aby zrozumieć różnicę – różnicę między stanem faktycznym istnienia ścisłych zależności a brakiem hermetyzacji, który jedynie umożliwia istnienie tych zależności, ale niczego nie implikuje.

Wymóg spójności oznacza, że klasy powinny implementować dobrze określony, wąski zakres funkcjonalności. Oznacza to, że tam gdzie tylko ma to sens należy używać delegacji odpowiedzialności. Wspominałem już o tym w artykule "SCJP - Związki typu IS-A oraz HAS-A" i tam też znajduje się dobry przykład. Dla ułatwienia lektury powtórzę go tutaj. Zerknijmy na poniższy kod.

class Company {
private Boss boss;

int requestPayRise(Employee emp, int amount) {
return boss.requestPayRise(emp, amount);
}
}

class Boss extends Employee {
int requestPayRise(Employee emp, int amount) {
if(emp instanceof Boss) {
return amount;
} else {
return 0;
}
}
}

class Employee {
private Company company;

private int salary;

void requestPayRise(int amount) {
salary += company.requestPayRise(this, amount);
}
}
Zwróćmy uwagę na klasę Employee. Jej metoda requestPayRise() deleguje wykonanie właściwych operacji do klasy Company a ta z kolei dalej do klasy Boss. I jest to słuszne, to w końcu nasz szef decyduje, czy dostaniemy podwyżkę czy nie.

4 komentarze:

Koziołek pisze...

Hermetyzacja klas to nie tylko ukrycie zasad działania, ale też zabezpieczenie przed "rozjazdem" danych gdy używamy wątków. Te zagadnienia pojawiają się trochę później w spisie zagadnień egzaminacyjnych.

Mariusz Lipiński pisze...

No tak, jeśli chcemy mieć synchronizacje to metody będą wielce pomocne, aczkolwiek można też użyć synchronizowanych struktur danych. Aczkolwiek zgadza się, że nie chodzi tylko o ukrycie zasad działania - tak jak zresztą napisałem "klasy powinny być hermetyczne także dlatego, aby nie było możliwe tworzenie instancji reprezentujących niespójny, niepożądany stan".

Anonimowy pisze...

Moze czas rozpoczac akademicka dyskusje na temat dlaczego enkapsulacja to nie to samo co hermetyzacja?

Mariusz Lipiński pisze...

Oczywiście. Słucham.