Programowanie proceduralne / funkcjonalne nie jest w żaden sposób słabsze niż OOP , nawet bez wchodzenia w argumenty Turinga (mój język ma moc Turinga i może robić cokolwiek innego, co robi), co niewiele znaczy. W rzeczywistości techniki obiektowe po raz pierwszy eksperymentowano w językach, które nie miały ich wbudowanych. W tym sensie programowanie OO jest tylko specyficznym stylem programowania proceduralnego . Ale to pomaga egzekwowania konkretnych dyscyplin, takich jak modułowość, abstrakcji i ukrywania informacji , które są niezbędne dla zrozumiałość i konserwacji programu.
Niektóre paradygmaty programowania ewoluują od teoretycznej wizji obliczeń. Język taki jak Lisp ewoluował z rachunku lambda i idei meta-okrężności języków (podobnej do zwrotności w języku naturalnym). Klauzule rogowe były prologiem i programowaniem ograniczeń. Rodzina Algol zawdzięcza także rachunku lambda, ale bez wbudowanej refleksyjności.
Lisp jest interesującym przykładem, ponieważ był testem wielu innowacji w języku programowania, które można powiązać z jego podwójnym dziedzictwem genetycznym.
Jednak języki ewoluują, często pod nowymi nazwami. Głównym czynnikiem ewolucji jest praktyka programowania. Użytkownicy identyfikują praktyki programistyczne, które poprawiają właściwości programów, takie jak czytelność, łatwość konserwacji, sprawdzalność poprawności. Następnie próbują dodać do języków funkcje lub ograniczenia, które będą obsługiwać, a czasem egzekwować te praktyki, aby poprawić jakość programów.
Oznacza to, że praktyki te są już możliwe w starszym języku programowania, ale ich zrozumienie i dyscyplina wymagają ich zastosowania. Włączenie ich do nowych języków jako podstawowych pojęć o określonej składni sprawia, że praktyki te są łatwiejsze w użyciu i łatwiejsze do zrozumienia, szczególnie dla mniej zaawansowanych użytkowników (tj. Zdecydowanej większości). Ułatwia także życie zaawansowanym użytkownikom.
W pewien sposób chodzi o zaprojektowanie języka, czym jest podprogram / funkcja / procedura dla programu. Po zidentyfikowaniu użytecznego pojęcia otrzymuje się nazwę (ewentualnie) i składnię, dzięki czemu można go łatwo używać we wszystkich programach opracowanych w tym języku. A gdy odniesie sukces, zostanie również włączony do przyszłych języków.
Przykład: odtworzenie orientacji obiektu
Teraz staram się to zilustrować na przykładzie (który z pewnością można by dopracować, biorąc pod uwagę czas). Celem tego przykładu nie jest pokazanie, że program zorientowany obiektowo może być napisany w proceduralnym stylu programowania, być może kosztem czytelności i łatwości obsługi. Spróbuję raczej pokazać, że niektóre języki bez funkcji OO mogą faktycznie korzystać z funkcji wyższego rzędu i struktury danych, aby faktycznie tworzyć środki do skutecznego naśladowania orientacji obiektowej , aby skorzystać z jej zalet w zakresie organizacji programu, w tym modułowości, abstrakcji i ukrywania informacji .
Jak powiedziałem, Lisp był testem wielu ewolucji języka, w tym paradygmatu OO (chociaż tym, co można uznać za pierwszy język OO, była Simula 67, w rodzinie Algol). Lisp jest bardzo prosty, a kod jego podstawowego interpretera jest mniejszy niż strona. Ale możesz programować OO w Lisp. Wszystko czego potrzebujesz to funkcje wyższego rzędu.
Nie będę używać ezoterycznej składni Lisp, ale raczej pseudo-kod, aby uprościć prezentację. Rozważę prosty podstawowy problem: ukrywanie informacji i modułowość . Definiowanie klasy obiektów, jednocześnie uniemożliwiając użytkownikowi dostęp do (większości) implementacji.
Załóżmy, że chcę utworzyć klasę o nazwie wektor, reprezentującą wektory dwuwymiarowe, z metodami obejmującymi: dodawanie wektora, rozmiar wektora i równoległość.
function vectorrec () {
function createrec(x,y) { return [x,y] }
function xcoordrec(v) { return v[0] }
function ycoordrec(v) { return v[1] }
function plusrec (u,v) { return [u[0]+v[0], u[1]+v[1]] }
function sizerec(v) { return sqrt(v[0]*v[0]+v[1]*v[1]) }
function parallelrec(u,v) { return u[0]*v[1]==u[1]*v[0]] }
return [createrec, xcoordrec, ycoordrec, plusrec, sizerec, parallelrec]
}
Następnie mogę przypisać utworzony wektor do rzeczywistych nazw funkcji, które będą używane.
[wektor, xcoord, ycoord, vplus, vsize, vparallel] = vectorclass ()
Po co być tak skomplikowanym? Ponieważ mogę zdefiniować w funkcji vectorrec konstrukcje pośredniczące, że nie chcę być widoczny dla reszty programu, aby zachować modułowość.
Możemy wykonać kolejną kolekcję we współrzędnych biegunowych
function vectorpol () {
...
function pluspol (u,v) { ... }
function sizepol (v) { return v[0] }
...
return [createpol, xcoordpol, ycoordpol, pluspol, sizepol, parallelpol]
}
Ale mogę używać obojętnie obu implementacji. Jednym ze sposobów jest dodanie komponentu typu do wszystkich wartości i zdefiniowanie wszystkich powyższych funkcji w tym samym środowisku: Następnie mogę zdefiniować każdą z zwracanych funkcji, aby najpierw przetestowała typ współrzędnych, a następnie zastosowała określoną funkcję dla tego.
function vector () {
...
function plusrec (u,v) { ... }
...
function pluspol (u,v) { ... }
...
function plus (u,v) { if u[2]='rec' and v[2]='rec'
then return plusrec (u,v) ... }
return [ ..., plus, ...]
}
Co zyskałem: określone funkcje pozostają niewidoczne (ze względu na zakres identyfikatorów lokalnych), a reszta programu może korzystać tylko z najbardziej abstrakcyjnych funkcji zwróconych przez wywołanie klasy wektorowej.
Jednym zastrzeżeniem jest to, że mógłbym bezpośrednio zdefiniować każdą z funkcji abstrakcyjnych w programie i pozostawić definicję funkcji zależnych od typu współrzędnych. Wtedy też byłby ukryty. To prawda, ale wtedy kod dla każdego typu współrzędnych zostałby pocięty na małe części rozłożone w programie, co jest mniej edytowalne i łatwe do utrzymania.
W rzeczywistości nie muszę nawet nadawać im nazwy i mógłbym po prostu zachować anonimowe wartości funkcjonalne w strukturze danych indeksowane według typu i ciągu reprezentującego nazwę funkcji. Ta struktura lokalna dla wektora funkcji byłaby niewidoczna dla reszty programu.
Aby uprościć użycie, zamiast zwracać listę funkcji, mogę zwrócić pojedynczą funkcję o nazwie Apply, przyjmując jako argument jawnie wpisaną wartość i ciąg znaków oraz zastosować funkcję o odpowiednim typie i nazwie. Wygląda to bardzo podobnie do wywoływania metody dla klasy OO.
Zatrzymam się tutaj w tej rekonstrukcji obiektu zorientowanego obiektowo.
Starałem się pokazać, że zbudowanie użytecznej orientacji obiektowej w wystarczająco mocnym języku, w tym dziedziczenia i innych podobnych cech, nie jest trudne. Metakrążenie interpretera może pomóc, ale głównie na poziomie syntaktycznym, co wciąż jest dalekie od nieistotnego.
Pierwsi użytkownicy orientacji obiektowej eksperymentowali w ten sposób z koncepcjami. Dotyczy to ogólnie wielu ulepszeń języków programowania. Oczywiście, analiza teoretyczna również odgrywa rolę i pomogła zrozumieć lub udoskonalić te pojęcia.
Ale pomysł, że języki, które nie mają funkcji OO są skazane na niepowodzenie w niektórych projektach, jest po prostu nieuzasadniony. W razie potrzeby mogą dość skutecznie naśladować implementację tych funkcji. Wiele języków ma moc syntaktyczną i semantyczną, aby dość skutecznie wykonywać orientację obiektową, nawet jeśli nie jest wbudowana. A to więcej niż argument Turinga.
OOP nie uwzględnia ograniczeń innych języków, ale wspiera lub wymusza metody programowania, które pomagają pisać lepszy program, pomagając w ten sposób mniej doświadczonym użytkownikom w stosowaniu dobrych praktyk, których bardziej zaawansowani programiści używali i rozwijali bez tego wsparcia.
Uważam, że dobrą książką, która to wszystko zrozumie, może być Abelson i Sussman: struktura i interpretacja programów komputerowych .