2 mar 2009

SOLID - Liskov Substitution Principle

Pisanie naprawdę obiektowego kodu nie jest popularne. Tak naprawdę nie widziałem zbyt wielu projektów pisanych obiektowo. A w javie piszę już 10 lat. W tzw. aplikacjach enterprise nie ma prawie dziedziczenia, nie ma hierarchii. Aplikacje są totalnie płaskie, proceduralne, z niejasną strukturą, z czasem bardzo trudne do utrzymania.

Wartość dziedziczenia przy strukturyzowaniu kodu pokazywałem już w poprzednich postach, w szczególności przy okazji zasady zamknięcia-otwartości. Natomiast zasada o której teraz trochę napiszę dotyczy nie tego jak ma wyglądać struktura, ale co ma być w klasach dziedziczących. Zasada podstawiania Liskov brzmi mniej-więcej tak:
Jeśli twój kod oczekuje jakiejś klasy, to zamiast niej powinieneś móc podstawić dowolną klasę z niej dziedziczącą bez zmieniania żadnych oczekiwanych zachowań.
Oznacza to tyle, że jeśli przeciążamy jakąś metodę w podklasie, to musi ona zachować semantykę odpowiedniej metody nadklasy.

Ta zasada mówi jedną bardzo ważną rzecz: jeśli przeciążasz metodę, napisz ją tak, by użyta polimorficznie działała poprawnie. Wyobraźmy sobie taką sytuację: mamy aplikację składającą się z modułów. Są tam zwykli użytkownicy oraz administratorzy modułów. Mamy użytkownika:


public class User {
protected String login;
protected String password;
protected List modules;

public void addAccessToModule(Module module) {
modules.add(module);
}

public boolean canAccess(Module module) {
if (modules.contains(module))
return true;
return false;
}
}

Mamy też administratora:

public class Admin extends User {
private List administeredModules;

public boolean canAccess(Module module) {
if (administeredModules.contains(module))
return true;
return false;
}
}


Ja widzimy metoda canAccess jest przeciążona w klasie Admin i sprawdza czy dany użytkownik jest administratorem podanego modułu (programista zaimplementował w ten sposób wymaganie, że administrator ma dostęp do wszystkich modułów jako zwykły użytkownik, a jako administrator do pewnego ich podzbioru). Wyobraźmy też sobie, że moduły emitują zdarzenia, na których otrzymywanie użytkownicy mogą się zapisać:

public void subscribeToEvents(User user, Module module) {
if (! user.canAccess(module)) {
user.addAccessToModule(module);
}
module.sendEventsTo(user);
}

Na pierwszy rzut oka nie widać żadnego błędu - jeśli ktoś jeszcze nie ma dostępu do jakiegoś modułu, to jest on mu przyznawany i dopisywany jest on do listy użytkowników notyfikowanych o zdarzeniach. Ale niestety przez to, że programista nie zastosował się do zasady podstawiania, kod ten jest poprawny tylko dla zwykłych użytkowników. Wielokrotne wywoływanie tej metody dla obiektów klasy Admin spowoduje ciągłe dodawanie tych samych modułów do do kolekcji modules klasy User, choć metoda canAccess w klasie Admin w ogóle jej nie sprawdza.

Nie zachowywanie zasady podstawiania Liskov powoduje powstawanie błędów często bardzo trudnych do wykrycia. Błąd jest prawie zawsze w klasie, której w kodzie wołającym w ogóle nie widać - w końcu są to wywołania polimorficzne. Przy bogatszych hierarchiach i duplikacji w kodzie szukanie błędów może się zmienić w wielogodzinną walkę. Co więcej, jeśli nie dbamy o jakość naszego kodu (właściwe hierarchie, duplikacje, właściwe umiejscowienie metod), nawet szczegółowe testy jednostkowe ze 100% pokryciem kodu może nic nie pomóc (w powyższym przykładzie wystarczy, by był jeszcze jakiś inny sposób na dodanie modułu do kolekcji modules, by testy jednostkowe nie wychwyciły tego problemu).

1 komentarz:

Anonimowy pisze...

Zdaje się że nie mamy tutaj doczynienia z przeciążeniem lecz przesłonięciem metody canAccess, gdyż definicja tej metody w podklasie ma identyczną sygnaturę jak w klasie bazowej.
Przeciążona metoda cechuje się tym, iż w stosunku do pierwowzoru ma innę ilość parametró, ich typ lub spełnia obie z tych cech jednocześnie