Dziedziczenie to technika proceduralna

Wszyscy wiemy, że dziedziczenie jest złe, i że "composition over inheritance" to dobra zasada, ale czy wiemy dlaczego? We wszystkich artykułach na ten temat, które udało mi się znaleźć, autorzy twierdzą, że dziedziczenie może zaszkodzić kodowi, więc lepiej go nie używać. Zastanawia mnie to "lepiej nie" - czy to znaczy, że czasami dziedziczenie ma sens?

Jakiś czas temu przeprowadziłem wywiad z Davidem Westem (autor "Object Thinking", mojej ulubionej książki o OOP), w którym powiedział między innymi, że w programowaniu obiektowym w ogóle nie ma miejsca na dziedziczenie (video). Może dr West ma rację i powinniśmy całkowicie zapomnieć na przykład o istnieniu słowa kluczowego extends w Javie?




Myślę, że powinniśmy. I chyba wiem dlaczego.

Nie dlatego, że niepotrzebnie wprowadzamy coupling, jak napisał w swoim artykule "Why extends is evil" Allen Holub. Na pewno miał rację, ale uważam, że to nie jest główna przyczyna problemu.

Czasownik "dziedziczyć" ma wiele znaczeń. Zgaduję, że twórcy języka Simula mieli na myśli to: "Przejmować cechy, predyspozycje genetyczne od rodziców lub przodków".

Zaczerpnięcie cechy charakterystycznej z innego obiektu to świetny pomysł, i nazywa się to tworzeniem podtypów. Idealnie pasuje do OOP i umożliwia polimorfizm: obiekt klasy Article dziedziczy wszystkie cechy obiektów w klasie Manuscript i dodaje swoje własne. Na przykład dziedziczy zdolność samodzielnego drukowania i dodaje możliwość zgłaszania się na konferencję:

interface Manuscript {
  void print(Console console);
}
interface Article extends Manuscript {
  void submit(Conference cnf);
}

 

Tak działają podtypy, i jest to doskonała metoda; zawsze, gdy wymagany jest manuscript, możemy przekazać article i nikt nic nie zauważy, bo Article jest podtypem Manuscript (zasada podstawienia Liskov).

Ale co kopiowanie metod i atrybutów z klasy rodzica do dziecka ma wspólnego z "przejmowaniem cech"? Dziedziczenie implementacji to właśnie kopiowanie, i nie ma nic wspólnego ze znaczeniem słowa "dziedziczenie", które przytoczyłem powyżej.

Dziedziczenie implementacji jest dużo bliższe innemu znaczeniu tego słowa: "Otrzymać (pieniądze, majątek lub tytuł) jako spadkobierca po śmierci poprzedniego posiadacza". No dobra, ale kto tu umarł? To obiekt jest martwy, jeśli pozwala innym obiektom odziedziczyć swój kod i dane. Tak wygląda dziedziczenie implementacji:

class Manuscript {
  protected String body;
  void print(Console console) {
    console.println(this.body);
  }
}
class Article extends Manuscript {
  void submit(Conference cnf) {
    cnf.send(this.body);
  }
}

 

Klasa Article kopiuje metodę print() i atrybut body z klasy Manuscript, jakby to nie był żywy organizm, ale właśnie nieżyjący, po którym możemy odziedziczyć jego własność, "pieniądze, majątek lub tytuł."



Dziedziczenie implementacji stworzono jako mechanizm ponownego użycia kodu, i zupełnie nie pasuje do OOP. Jasne, na początku może wydawać się wygodne, ale jest całkowicie błędne w kontekście myślenia obiektowego. Podobnie jak gettery i settery, dziedziczenie implementacji zmienia obiekty w kontenery na dane i procedury. Oczywiście wygodnie jest kopiować niektóre dane i procedury do nowego obiektu żeby uniknąć powielania kodu. Ale nie o to chodzi w obiektach. One nie umarły, one żyją!

Nie zabijaj ich dziedziczeniem :)

Tak więc myślę, że dziedziczenie jest złe, bo to technika proceduralna, która służy do ponownego wykorzystania kodu. Nic dziwnego, że wprowadza wszystkie problemy, o których ludzie mówią od lat. Bo jest proceduralne! Właśnie dlatego nie pasuje do programowania obiektowego.

Swoją drogą, rozmawialiśmy o tym na naszym czacie tydzień temu i wtedy dotarło do mnie, co dokładnie jest nie tak z dziedziczeniem. Zobacz naszą dyskusję.

Więcej na ten temat w rozdziale 5.7 mojej książki Elegant Objects 2.

Artykuł na blogu autora: Inheritance is procedural