Dziedziczenie i metody abstrakcyjne można zastąpić do pewnego stopnia domknięciami. Tzn zamiast:
class Klasa {
def metoda: Int =
doNadpisania + 1
def doNadpisania: Int // abstrakcyjna metoda do nadpisania w podklasach
}
można napisać:
class Klasa(doPodaniaZZewnątrz: () => Int) {
def metoda: Int =
doPodaniaZZewnątrz() + 1
}
Ktoś kto bardzo nie lubi OOP, dziedziczenia, metod wirtualnych, etc będzie się bardzo cieszył, ale moim zdaniem nawigacja i analiza kodu bardzo cierpi na tym, przynajmniej w obecnych IDE typu IntelliJ. Mając klasę IntelliJ po jednym skrócie klawiaturowym pokaże mi hierarchię dziedziczenia. Przy metodzie IntelliJ pokaże ikonki dzięki którym szybko można dojść do metod nadpisanych lub nadpisujących/ implementujących. Inaczej mówiąc w przypadku OOP nawigacja w kodzie jest "pierwsza klasa". W przypadku domknięć/ lambd natomiast jest strasznie kiepsko bo te lambdy mają tendencję do bycia przesyłanymi wzdłuż i w poprzek, więc samo ich szukanie wybija mnie z toku myślenia. Dziedziczenie i kompozycja działają dla mnie lepiej niż szastanie lambdami.
Z drugiej strony mamy pojedynek dziedziczenie vs kompozycja i tutaj nie ma wygranego, bo oba podejścia mają sens. Dziedziczenie jest lekkie jeśli hierarchia klas jest lekka i w takich przypadkach dobrze się sprawdza. Kompozycja wprowadza pewien narzut (bolierplate), więc rozdrabnianie wszystkiego na siłę jak najmniejsze komponenty rozdmucha kod nie przynosząc zysku netto. Z drugiej strony jednak w wielu przypadkach ma sens, zwłaszcza jeśli hierarchia dziedziczenia staje się skomplikowana i można ją uprościć rozbijając ją na kompozycję i mniejsze hierarchie dziedziczenia.
Co do programowania funkcyjnego to powtórzę jeszcze raz, że podstawą funkcyjności jest niemutowalność danych, a co za tym idzie kolekcje w bibliotece standardowej muszą wspierać ten sposób pisania. Dodawanie elementu do niemutowalnej mapy ma zwracać nową mapę, a nie rzucać błędem. Tego typu kolekcje są w standardzie w Scali i Haskellu, ale np w C, C++, Ruście, Javie, Kotlinie, C#, Pythonie, JavaScripcie itp itd ich w standardzie nie ma. Gdy ich nie ma to przy pisaniu funkcyjnym trzeba by skorzystać z bibliotek niestandardowych, a potem mieć poważny problem z integracją z kodem, który używa standardowych kolekcji. Bez funkcyjnych kolekcji dane stają się dużym grafem mutowalnych obiektów/ struktur/ czegokolwiek tak jak tu zostało przytoczone.
Co do obiekt vs struktura to jak napisał @zyxist struktury często udają obiekty jeżeli robimy ręcznie to co kompilator zrobiłby za nas. W szczególności takie podejście wydaje się strukturalne:
class Klasa {
val typ: Int
val dane: Int
}
def wyciągnijDane(klasa: Klasa): Int = {
klasa.typ match {
case 0 => klasa.dane + 1
case 1 => klasa.dane + 8
case 2 => klasa.dane * 3
case _ => klasa.dane - 9
}
}
Ale to tak naprawdę zakamuflowane dziedziczenie typu:
class Klasa(dane: Int) {
def wyciągnijDane: Int = dane - 9
}
class KlasaTyp0(dane: Int) extends Klasa(dane) {
override def wyciągnijDane: Int = dane + 1
}
... // kolejne klasy analogicznie
Ifologia emulująca dziedziczenie jest antywzorcem w językach OOP, bo jest zwyczajnie gorsza. Kompilator nie jest w stanie sprawdzić czy ifologia ma sens i jest poprawna, ale jest w stanie to sprawdzić w przypadku hierarchii klas. Podobnie analiza kod i nawigacja działa wygodnie i szybko w przypadku hierarchii klas, ale w przypadku ifologii jest z tym sporo gorzej.
PS:
Podawanie C++ jako przykładowej implementacji OOPa jest jak podawanie COBOLa jako przykładowej implementacji języka strukturalnego. Solidnymi implementacjami OOPa są Java i C#, więc w ich kontekście powinno się analizować skuteczność OOPa.