Trzecia już zasada mnemoniku SOLID. Nazwa tej reguły pochodzi od nazwiska twórczyni, czyli Barbary Liskov. Według niej w miejscu, gdzie wykorzystywana jest klasa bazowa bez problemu można użyć klas pochodnych, które mogą rozszerzać bazowe funkcjonalności, ale zmiany w nich zawarte nie mogą mieć wpływu na działanie programu oraz jego wynik. W ten sposób dostarczana jest alternatywa dla istniejącego rozwiązania. Można spróbować uprościć ten opis do tego, że podmieniany jest tylko algorytm wykonania danego zadania. Warto zaznaczyć również, że podklasa nie może mieć silniejszych warunków wstępnych niż klasa bazowa.

Liskov substitution principle
Nie zawsze sytuacja rzeczywista da się odwzorować 1:1 w kodzie źródłowym

Poniżej przedstawiono klasyczny przykład, z kwadratem i prostokątem, naruszającym tę zasadę.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Rectangle {

  protected int width;
  protected int height;

  public void setWidth(int width) {
    this.width = width;
  }

  public void setHeight(int height) {
    this.height = height;
  }

  public int calculateArea() {
    return width * height;
  }
}

public class Square extends Rectangle {

  public void setWidth (int width) {
    this.width = width;
    this.height = width;
  }

  public void setHeight(int height) {
    this.width = height;
    this.height = height;
  }
}

public class Program {
  
  public static void Main(String[] args) {
    Rectangle rectangle = new Rectangle();
    rectangle.setWidth(5);
    rectangle.setHeight(10);
    rectangle.calculateArea(); // 50

    Rectangle rectangleSquare = new Square();
    rectangleSquare.setWidth(5);
    rectangleSquare.setHeight(10);
    rectangleSquare.calculateArea(); // 100!

    Square square = new Square();
    square.setWidth(5);
    square.setHeight(10);
    square.calculateArea(); // 100!
  }
}

Sytuacja przedstawia podstawową zasadę matematyczną, która mówi, że każdy kwadrat jest prostokątem. Jednak w projektowaniu oprogramowania taka sytuacja wprowadza w błąd. Ustawiając wysokość spodziewamy się, że zmienimy tylko tę właściwość. To samo ma miejsce w przypadku szerokości. Natomiast rozszerzając klasę Rectangle przez Square łamana jest taka logika. Zmieniając wysokość, a następnie szerokość, za drugim razem znowu wysokość zmienia swoją wartość co wprowadza w błąd 😵! Dla programisty bardziej intuicyjna byłaby metoda dla kwadratu setSide. Prowadzi to do wniosku, że nie każdą sytuację życiową da się przedstawić w kodzie w taki sam sposób.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Manager {

  public void organizeMeetings() {
    // organize meetings
  }

  public void motivateTeam() {
    // motivate team
  }
}

public class OrganizationManager extends Manager {

  public void motivateTeam() {
    throw new TypeOfWorkNotSupportedException();
  }
}

Powyższy przykład również odzwierciedla rzeczywistość. Manager zajmujący się organizacją spotkań jest także managerem (Captain Obvious!). Do jego obowiązków nie należy motywowanie zespołu, stąd zmiana implementacji metody motivateTeam. Z programistycznego punktu widzenia klasa OrganizationManager nie powinna w ogóle posiadać motivateTeam, jest to nadmiarowe i zbędne. Wręcz wprowadza w błąd! Potwierdza to jeszcze raz sytuację, żeby nie należy zawsze kierować się intuicją przy projektowaniu encji.

Podsumowanie

Reguła Liskov Substitution Principle jak dla mnie jest najcięższa do zrozumienia w porównaniu do pozostałych. Nie mówi jednoznacznie w jaki sposób można się do niej dostosować. Jej zadaniem nie jest tłumaczenie w jaki sposób używać wskaźników czy referencji do obiektów różnych typów (czy to klasy bazowej czy pochodnej). Jednak to programista powinien wiedzieć z jakiego powodu korzysta z dobrodziejstw danej zasady. Bądź też powinien umieć uzasadnić dlaczego łamie daną regułę.

Należy zawsze mieć jakiś punkt wyjścia podczas nauki, dlatego pozwolę sobie napisać taką formułkę:

Klasa rozszerzająca funkcjonalności klasy bazowej może je podmieniać, ale nie powinno mieć to wpływu na działanie aplikacji oraz także na jej wynik.

Jednym z przykładów złamania tej zasady w bibliotece Javy jest klasa UnmodifiableList, którą otrzymujemy korzystając z metody Collections.unmodifiableList();. Co prawda zwracana jest lista, której nie możemy w żadnej sposób modyfikować, jednak okupione jest to kosztem złamania reguły Liskov. Przy próbie np. dodania elementu rzucany jest wyjątek, którego nie spotkamy w żadnej, znanej implementacji List. Jest to niespodziewany efekt, który zmienia działanie programu.

Podziel się ze mną swoją opinią na temat tej zasady. Czy zgadzasz się z moją interpretacją? Być może masz inne postrzeganie? Kojarzysz jakieś ciekawe przypadki łamiące zasadę Liskov? Napisz o tym w komentarzu bądź też wyślij do mnie maila. Czekam z niecierpliwością na Twoją opinię!