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.

7 myśli na temat “S.O.L.I.D.ne programowanie – część 3, czyli podkładamy świnię

  1. 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.

  2. 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ć.

  3. Tak, kod szablonowy jest idealnym przykładem dobrego dziedziczenia – zawsze będziesz wołał abstrakcję polimorficzne.

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax