S.O.L.I.D.ne programowanie – część 3, czyli podkładamy świnię
S.O.L.I.D.ne programowanie – część 0, czyli wstęp
S.O.L.I.D.ne programowanie – część 1, czyli monogamia
S.O.L.I.D.ne programowanie – część 2, czyli spoufalamy się
Witam w trzeciej części cyklu S.O.L.I.D.ne Programowanie. Dziś na warsztat bierzemy Liskov Substitution Principle (LSP).
Nie kijem go to pałą
Efekt powinien być ten sam. Zasada Podstawienia Liskowa jest prosta, ale może przysporzyć problemów.
Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T.
Względnie znacznie szerzej znana definicja matematyczna tego problemu:
Dla każdego X i Y należącego do zbioru R, takich że X = Y, jeżeli F(X) odwzorowuje X w zbiorze R to F(Y) też odwzorowuje Y w zbiorze R tak, że F(X) = F(Y)
Jednym zdaniem wymiana implementacji nie powinna wpływać na kod klienta.
Kij
LSP jest, jak już pisałem, dość prosta. Jednocześnie należy zwrócić uwagę na fakt, że czasami realizacja tej zasady jest niemożliwa. Co ciekawe są to dość częste przypadki i należy pamiętać o nich.
Do rzeczy. Po pierwsze co mów nam ta zasada to to, że jeżeli klasę A zamienimy klasą B, która dziedziczy z A to klient nie powinien otrzymać innych wyników. Ważna rzecz, o której wszyscy zapominamy LSP dotyczy tylko klas dziedziczących po sobie, a nie implementacji interfejsów. Szczególnie interfejsów uogólnionych, czyli takich które pozwalają na realizację wielu funkcjonalności za pomocą jednego spójnego API.
Przykład 1. Filtr tekstu
Przykładem kodu, który nie podpada pod LSP jest kod filtrujący. Załóżmy, że użytkownik wywołuje zdalnie metodę procced(String):String. Ciężko jest wymagać, by wszystkie filtry działały w ten sam sposób i wykonywały takie same operacje. Dlatego też wybór konkretnej implementacji spada na użytkownika. To on wykonuje wymianę implementacji i ponosi wszelkie konsekwencje tej zmiany.
Listing 1. Kod niepodlegający LSP
package pl.koziolekweb.solid.lsp;
public class App {
public static void main(String[] args) {
// kod użytkownika
String abc = "abc";
TextFilter filter = new AFilter();
System.out.println(filter.procced(abc));
filter = new BFilter();
System.out.println(filter.procced(abc));
}
}
// nasz kod
interface TextFilter {
public String procced(String text);
}
class AFilter implements TextFilter {
public String procced(String text) {
return text.replaceAll("a", "");
}
}
class BFilter implements TextFilter {
public String procced(String text) {
return text.replaceAll("b", "");
}
}
W kodzie klienta nastąpiła wymiana implementacji i pociągnęła ona za sobą zmianę zachowania. Klient może mieć pretensje tylko do siebie ponieważ z pełną świadomością wymienił implementację na inną.
Pała
Skoro istnieje kod, który po d LSP nie podpada to muszą istnieć też miejsca dla których LSP ma zastosowanie. Powróćmy do naszego przykładu i załóżmy, że zmieniamy implementację AFilter.
Listing 2. LSP w praktyce – poprawnie
package pl.koziolekweb.solid.lsp;
public class App {
public static void main(String[] args) {
// kod użytkownika
String abc = "abc";
TextFilter filter = new AFilter();
System.out.println(filter.procced(abc));
filter = new BFilter();
System.out.println(filter.procced(abc));
}
}
// nasz kod
interface TextFilter {
public String procced(String text);
}
class AFilter implements TextFilter {
public String procced(String text) {
try{
//coś ekstra liczymy
}catch (Exception e) {
}
return text.replaceAll("a", "");
}
}
class BFilter implements TextFilter {
public String procced(String text) {
return text.replaceAll("b", "");
}
}
Listing 3. LSP w praktyce – niepoprawnie
package pl.koziolekweb.solid.lsp;
public class App {
public static void main(String[] args) {
// kod użytkownika
String abc = "abc";
TextFilter filter = new AFilter();
System.out.println(filter.procced(abc));
filter = new BFilter();
System.out.println(filter.procced(abc));
}
}
interface TextFilter {
public String procced(String text) throws Exception;
}
class AFilter implements TextFilter {
public String procced(String text) throws Exception{
//coś ekstra liczymy
return text.replaceAll("a", "");
}
}
class BFilter implements TextFilter {
public String procced(String text) throws Exception{
return text.replaceAll("b", "");
}
}
tu leży pies pogrzebany. Zmieniliśmy implementację kodu. Na listing 2. zrobiliśmy to w prawidłowy sposób zamykając nowe ciekawe możliwości wewnątrz metody. Na listingu 3. zmiany nie dość, że spowodowały zmianę API to dodatkowo kod klienta nie kompiluje się. Czyli jest źle.
W javie mamy @Deprecated, która służy od oznaczania kodu przestarzałego. Jedyną możliwością wycofania metody lub klasy z API oznaczenie jej jako przestarzałej. LSP chroni użytkownika przed niepodziewanymi zmianami w działaniu kodu jak i zmianami API.
Listing 4. LSP w praktyce – poprawnie
package pl.koziolekweb.solid.lsp;
public class App {
public static void main(String[] args) {
// kod użytkownika
String abc = "abc";
TextFilter filter = new AFilter();
System.out.println(filter.procced(abc));
filter = new BFilter();
System.out.println(filter.procced(abc));
}
}
// nasz kod
interface TextFilter {
@Deprecated
public String procced(String text);
public String proccedAndThrow(String text) throws Exception;
}
class AFilter implements TextFilter {
@Deprecated
public String procced(String text) {
return text.replaceAll("a", "");
}
public String proccedAndThrow(String text) throws Exception {
// coś ekstra liczymy
return text.replaceAll("a", "");
}
}
class BFilter implements TextFilter {
@Deprecated
public String procced(String text) {
return text.replaceAll("b", "");
}
public String proccedAndThrow(String text) throws Exception {
return procced(text);
}
}
Prawidłowe rozwiązanie, które zachowując LSP pozwala na wprowadzenie nowego inaczej zachowującego się kodu.
Podsumowanie
Choć LSP jest bardzo prosta w zrozumieniu i użyciu to należy pamiętać, że istnieje kod, który tej zasadzie nie podlega. LSP należy używać mądrze, a nie zawsze.









November 25th, 2009 at 23:24
Znalazłem kiedyś takie oto skrócone wyjaśnienie LSP: używaj dziedziczenia tylko wtedy, gdy będziesz korzystał z polimorfizmu. A nie w celu “wyciągania przed nawias” wspólnych części – do tego służy agregacja. Chodzi o to, że wyciągając przez dziedziczenie pojawia się problem Kruchej Klasy Bazowej – zmiany w niej niszczą resztę. Zwykle przez to zmian po prostu nie ma.
November 26th, 2009 at 09:37
Z tą agregacją i dziedziczeniem to zależy. Java udostępnia mechanizm genericsów dzięki któremu można tworzyć kod szablonowy w klasie abstrakcyjnej, a klasy konkretne udostępniają tylko szczegóły. W takim przypadku dziedziczenie jest dobre. Agregacja sprawdza się gdy chcemy stworzyć obiekt, który będzie udostępniał kilka funkcjonalności w łatwy sposób. Musi on być jednak na poziomie ponad tymi funkcjonalnościami inaczej nie ma SRP i można sobie go wsadzić.
November 26th, 2009 at 10:49
Tak, kod szablonowy jest idealnym przykładem dobrego dziedziczenia – zawsze będziesz wołał abstrakcję polimorficzne.