Porównanie paradygmatów programowania

W tym artykule podjęto próbę przedstawienia różnych podobieństw i różnic między różnymi paradygmatami programowania w postaci podsumowania zarówno w formacie graficznym, jak i tabelarycznym, z linkami do oddzielnych dyskusji dotyczących tych podobieństw i różnic w zachowanych artykułach w Wikipedii.

Główne podejścia paradygmatu

Istnieją dwa główne podejścia do programowania:

Następujące są powszechnie uważane za główne paradygmaty programowania, co widać podczas pomiaru popularności języka programowania :

Poniżej przedstawiono typowe typy programowania, które można zaimplementować przy użyciu różnych paradygmatów:

Podprogramy, które implementują metody OOP, mogą być ostatecznie zakodowane w stylu imperatywnym, funkcjonalnym lub proceduralnym, który może, ale nie musi, bezpośrednio zmieniać stan w imieniu wywołującego programu. Nieuchronnie pewne nakładanie się paradygmatów, ale główne cechy lub możliwe do zidentyfikowania różnice podsumowano w tej tabeli:

Paradygmat Opis Główne cechy Powiązany paradygmat (y) Krytyka Przykłady
Pilny Programy jako instrukcje , które bezpośrednio zmieniają obliczony stan ( pola danych ) Przyporządkowania bezpośrednie , wspólne struktury danych , zmienne globalne Edsger W. Dijkstra , Michael A. Jackson C , C++ , Java , Kotlin , PHP , Python , Ruby
Zbudowany Styl programowania imperatywnego z bardziej logiczną strukturą programu Struktogramy , wcięcia , brak lub ograniczone użycie instrukcji goto Pilny C , C++ , Java , Kotlin , Pascal , PHP , Python
Proceduralny Wywodzące się z programowania strukturalnego, oparte na koncepcji programowania modułowego lub wywołania procedury Zmienne lokalne , kolejność, wybór, iteracja i modularyzacja Zorganizowany, imperatywny C , C++ , LISP , PHP , Python
Funkcjonalny Traktuje obliczenia jako ocenę funkcji matematycznych z pominięciem danych stanu i zmiennych Rachunek lambda , skład , formuła , rekurencja , przejrzystość referencyjna , brak efektów ubocznych Deklaracyjny C++ , C# , [ odwołanie cykliczne ] Clojure , CoffeeScript , Elixir , Erlang , F# , Haskell , Java (od wersji 8), Kotlin , Lisp , Python , R , Ruby , Scala , SequenceL , Standard ML , JavaScript , Elm
Sterowany zdarzeniami , w tym sterowany czasem Przepływ sterowania jest określany głównie przez zdarzenia , takie jak kliknięcia myszką lub przerwania, w tym timer Pętla główna , procedury obsługi zdarzeń, procesy asynchroniczne Proceduralny, przepływ danych JavaScript , ActionScript , Visual Basic , Elm
Zorientowany obiektowo Traktuje pola danych jako obiekty , którymi manipuluje się tylko za pomocą predefiniowanych metod Obiekty , metody , przekazywanie komunikatów , ukrywanie informacji , abstrakcja danych , enkapsulacja , polimorfizm , dziedziczenie , serializacja -marshalling Proceduralny Zobacz jego wybór krytyki i gdzie indziej Common Lisp , C++ , C# , Eiffel , Java , Kotlin , PHP , Python , Ruby , Scala , JavaScript
Deklaracyjny Definiuje logikę programu, ale nie szczegółowy przebieg sterowania Języki czwartej generacji , arkusze kalkulacyjne , generatory programów raportowych SQL , wyrażenia regularne , Prolog , OWL , SPARQL , Datalog , XSLT
Programowanie oparte na automatach Traktuje programy jako model skończonej maszyny stanowej lub dowolnego innego automatu formalnego Wyliczanie stanów , zmienna sterująca , zmiany stanów , izomorfizm , tablica przejść stanów Imperatyw, sterowany zdarzeniami Abstrakcyjny język maszyny stanowej

Różnice w terminologii

Pomimo wielu (typów) paradygmatów programowania istniejących równolegle (z czasami pozornie sprzecznymi definicjami), wiele podstawowych komponentów pozostaje mniej więcej takich samych ( stałe , zmienne , pola danych , wywołania podprogramów itp.) i nieuchronnie muszą być włączone do każdego odrębny paradygmat o jednakowo podobnych atrybutach lub funkcjach. Powyższa tabela nie ma służyć jako przewodnik po dokładnych podobieństwach, ale raczej jako indeks, gdzie szukać więcej informacji, w oparciu o różne nazewnictwo tych podmiotów w ramach każdego paradygmatu. Dalszą komplikacją są niestandardowe implementacje każdego paradygmatu w wielu językach programowania , zwłaszcza w językach obsługujących wiele paradygmatów , z których każdy ma swój własny żargon .

Wsparcie językowe

Cukier składniowy to dosładzanie funkcjonalności programu poprzez wprowadzanie cech językowych ułatwiających dane użycie, nawet jeśli bez nich można by osiągnąć efekt końcowy. Jednym z przykładów cukru syntaktycznego mogą być prawdopodobnie klasy używane w obiektowych językach programowania. Imperatywny język C może obsługiwać programowanie zorientowane obiektowo za pomocą wskaźników funkcji , rzutowania typów i struktur. Jednak języki takie jak C++ mają na celu uczynienie programowania obiektowego wygodniejszym poprzez wprowadzenie składni specyficznej dla tego stylu kodowania. Ponadto wyspecjalizowana składnia działa w celu podkreślenia podejścia obiektowego. Podobnie funkcje i składnia pętli w C (i innych proceduralnych i strukturalnych językach programowania) można uznać za cukier składniowy. Język asemblera może wspierać programowanie proceduralne lub strukturalne poprzez swoje możliwości modyfikowania wartości rejestrów i wykonywania rozgałęzień w zależności od stanu programu. Jednak języki takie jak C wprowadziły składnię specyficzną dla tych stylów kodowania, aby uczynić programowanie proceduralne i strukturalne wygodniejszym. Funkcje języka C# (C Sharp), takie jak właściwości i interfejsy, podobnie nie udostępniają żadnych nowych funkcji, ale mają na celu uczynienie dobrych praktyk programistycznych bardziej widocznymi i naturalnymi.

Niektórzy programiści uważają, że te funkcje są nieważne, a nawet niepoważne. Na przykład Alan Perlis zażartował kiedyś, odnosząc się do języków rozdzielonych nawiasami kwadratowymi , że „cukier składniowy powoduje raka średnika (patrz Epigramy o programowaniu ).

Rozszerzeniem tego jest syntaktyczna sacharyna lub nieuzasadniona składnia, która nie ułatwia programowania.

Porównanie wydajności

pod względem całkowitej długości ścieżki instrukcji program zakodowany w stylu imperatywnym, bez podprogramów, miałby najniższą liczbę. Jednak binarny takiego programu może być większy niż ten sam program zakodowany przy użyciu podprogramów (jak w programowaniu funkcjonalnym i proceduralnym) i odwołuje się do większej liczby nielokalnych fizycznych , które mogą zwiększać instrukcji braki w pamięci podręcznej i narzut pobierania instrukcji w nowoczesnych procesorach .

Paradygmaty, które intensywnie wykorzystują podprogramy (w tym funkcjonalne, proceduralne i zorientowane obiektowo) i nie używają również znaczącego rozszerzenia wbudowanego (wstawianie poprzez optymalizacje kompilatora ), będą w konsekwencji zużywać większą część całkowitych zasobów na powiązania podprogramów. Programy zorientowane obiektowo, które celowo nie zmieniają stanu programu , zamiast tego używają metod mutator (lub setterów ) do enkapsulacji tych zmian stanu, będą, w bezpośredniej konsekwencji, mieć większy narzut. Dzieje się tak, ponieważ przekazywanie komunikatów jest zasadniczo wywołaniem podprogramu, ale z trzema dodatkowymi kosztami: dynamiczną alokacją pamięci , kopiowaniem parametrów i dynamiczną wysyłką . Pobieranie pamięci ze sterty i kopiowanie parametrów do przekazywania komunikatów może wymagać znacznych zasobów, znacznie przekraczających te potrzebne do zmiany stanu. Akcesory (lub gettery ), które jedynie zwracają wartości prywatnych zmiennych składowych, również zależą od podobnych podprogramów przekazywania komunikatów, zamiast używać bardziej bezpośredniego przypisania (lub porównania), dodając do całkowitej długości ścieżki.

Zarządzany kod

W przypadku programów uruchamianych w środowisku kodu zarządzanego , takim jak .NET Framework , wiele problemów wpływa na wydajność, na które znacząco wpływa paradygmat języka programowania i różne używane funkcje języka.

Przykłady pseudokodów porównujące różne paradygmaty

Pseudokodowe porównanie podejść imperatywnych, proceduralnych i obiektowych stosowanych do obliczania pola koła (πr²), przy założeniu braku wstawiania podprogramów , żadnych preprocesorów makr , arytmetyki rejestrów i ważenia każdej instrukcji „krok” jako tylko 1 instrukcja - jako przybliżona miara długości ścieżki instrukcji – przedstawiono poniżej. Krok instrukcji, który koncepcyjnie przeprowadza zmianę stanu, jest w każdym przypadku wyróżniony pogrubioną czcionką. Operacje arytmetyczne używane do obliczania pola koła są takie same we wszystkich trzech paradygmatach, z tą różnicą, że paradygmaty proceduralne i zorientowane obiektowo owijają te operacje w wywołanie podprogramu, dzięki czemu obliczenia są ogólne i wielokrotnego użytku. Ten sam efekt można osiągnąć w czysto imperatywnym programie używającym preprocesora makr tylko kosztem zwiększonego rozmiaru programu (tylko w każdym miejscu wywołania makra) bez odpowiedniego proporcjonalnego kosztu czasu wykonania (proporcjonalnego do n wywołań – które mogą znajdować się w na przykład wewnętrzna pętla ). I odwrotnie, wstawianie podprogramów przez kompilator może zredukować programy proceduralne do czegoś podobnego pod względem rozmiaru do kodu czysto imperatywnego. Jednak w przypadku programów zorientowanych obiektowo, nawet z wstawianiem, komunikaty nadal muszą być budowane (z kopii argumentów) do przetwarzania metodami obiektowymi. Narzut wywołań, wirtualnych lub innych, nie jest zdominowany przez przepływu sterowania – ale przez otaczające koszty konwencji wywołania , takie jak kod prologu i epilogu , konfiguracja stosu i przekazywanie argumentów (zobacz tutaj, aby uzyskać bardziej realistyczną długość ścieżki instrukcji, stos i inne koszty związane z połączeniami na platformie x86 ). Zobacz także tutaj prezentację slajdów autorstwa Erica S. Robertsa („The Allocation of Memory to Variables”, rozdział 7) - ilustrującą użycie pamięci stosu i sterty podczas sumowania trzech liczb wymiernych w obiektowym języku Java .

Pilny Proceduralny Zorientowany obiektowo
 załaduj r; 1 r2 = r * r;  2   wynik = r2 * "3,142";  3 . .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  ....pamięć............zmienna wynikowa stała "3,142"  
 area proc(r2,res): push stack 5 load r2; 6 r3 = r2 * r2;  7   res = r3 * "3,142";  8 pop stos 9 powrót; 10 ....................................................... główny proc : obciążenie r;  1 obszar wywołania(r,wynik);  +load p = adres listy parametrów;  2 +load v = adres podprogramu 'obszar';  3 +goto v z powrotem;  4 .  .  .  .  ....pamięć ...............zmienna wynikowa stała "3.142" lista parametrów zmienna wskaźnik funkcji (==>obszar) pamięć stosu  
 metoda okręgu.obszar (r2): wciśnij stos 7 załaduj r2; 8 r3 = r2 * r2;  9 res = r3 * "3,142";  10 pop stack 11   return(res);  12,13 ......................................................... główny proces: załaduj r; 1 wynik = okrąg.obszar(r);  +przydzielanie pamięci sterty;  2 + skopiuj r do wiadomości;  3 +load p = adres wiadomości;  4 +obciążenie v = adres.  metody 'circle.area' 5 +goto v z powrotem;  6 .  .  .... storage ............... zmienna wyniku (zakłada się, że jest wstępnie przydzielona) niezmienna zmienna "3.142" (końcowa) (sterta) zmienna komunikatu dla wywołania metody okręgu vtable(==>area) przechowywanie stosu  

Zalety abstrakcji proceduralnej i polimorfizmu zorientowanego obiektowo słabo ilustruje mały przykład, taki jak ten powyżej. Ten przykład został zaprojektowany głównie w celu zilustrowania pewnych wewnętrznych różnic w wydajności, a nie abstrakcji lub ponownego użycia kodu.

Podprogram, narzut wywołania metody

Obecność (nazywanego) podprogramu w programie nie wnosi nic więcej do funkcjonalności programu, niezależnie od paradygmatu, ale może znacznie przyczynić się do ustrukturyzowania i ogólności programu, znacznie ułatwiając pisanie, modyfikowanie i rozszerzanie. Stopień, w jakim różne paradygmaty wykorzystują podprogramy (i wynikające z nich wymagania dotyczące pamięci) wpływa na ogólną wydajność całego algorytmu, chociaż, jak Guy Steele w artykule z 1977 r., Dobrze zaprojektowana implementacja języka programowania może mieć bardzo niskie koszty ogólne abstrakcji proceduralnej (ale ubolewa, że ​​w większości wdrożeń rzadko osiągają to w praktyce - będąc „raczej bezmyślnymi lub nieostrożnymi w tym względzie”). W tym samym artykule Steele rozważa również programowanie oparte na automatach (używając wywołań procedur z rekurencją ogona ) i stwierdza, że ​​„powinniśmy mieć zdrowy szacunek dla wywołań procedur” (ponieważ są one potężne), ale zasugerował „używaj ich oszczędnie "

W częstotliwości wywołań podprogramów:

  • W przypadku programowania proceduralnego ziarnistość kodu jest w dużej mierze określana przez liczbę dyskretnych procedur lub modułów .
  • W przypadku programowania funkcjonalnego częste są wywołania podprogramów bibliotecznych , [ potrzebne źródło ] , ale często mogą być wstawiane przez kompilator optymalizujący
  • W przypadku programowania zorientowanego obiektowo liczba wywoływanych metod jest również częściowo określona przez ziarnistość struktur danych, a zatem może obejmować wiele dostępów tylko do odczytu do obiektów niskiego poziomu, które są hermetyzowane, a zatem dostępne w żadnym innym, bardziej bezpośrednim, sposób. Ponieważ zwiększona ziarnistość jest warunkiem wstępnym większego ponownego użycia kodu , istnieje tendencja w kierunku drobnoziarnistych struktur danych i odpowiedniego wzrostu liczby dyskretnych obiektów (i ich metod), aw konsekwencji wywołań podprogramów. Aktywnie odradza się tworzenie boskich obiektów . Konstruktory również zwiększają liczbę, ponieważ są również wywołaniami podprogramów (chyba że są wstawiane). Problemy z wydajnością spowodowane nadmierną szczegółowością mogą nie być widoczne, dopóki skalowalność nie stanie się problemem.
  • W przypadku innych paradygmatów, w których można zastosować mieszankę powyższych paradygmatów, użycie podprogramu jest mniej przewidywalne.

Alokacja pamięci dynamicznej do przechowywania komunikatów i obiektów

W wyjątkowy sposób paradygmat zorientowany obiektowo obejmuje dynamiczną alokację pamięci z magazynu sterty zarówno do tworzenia obiektów, jak i przekazywania komunikatów. Test porównawczy z 1994 r. — „Koszty alokacji pamięci w dużych programach C i C++”, przeprowadzony przez Digital Equipment Corporation na różnych programach przy użyciu narzędzia do profilowania na poziomie instrukcji, zmierzył liczbę instrukcji wymaganych na dynamiczną alokację pamięci. Wyniki pokazały, że najniższa bezwzględna liczba wykonanych instrukcji wynosiła średnio około 50, ale inne osiągnęły nawet 611. Zobacz także „Heap: Pleasures and pains” autorstwa Murali R. Krishnan, który stwierdza: „Implementacje sterty zwykle pozostają ogólne dla wszystkich platform i stąd mają duże koszty ogólne”. Artykuł IBM z 1996 r. „Scalability of Dynamic Storage Allocation Algorithms” autorstwa Aruna Iyengara z IBM demonstruje różne algorytmy dynamicznej pamięci masowej i odpowiadające im liczby instrukcji. Nawet zalecany algorytm MFLF I (HS Stone, RC 9674) pokazuje liczbę instrukcji w zakresie od 200 do 400. Powyższy przykład pseudokodu nie zawiera realistycznego oszacowania długości ścieżki alokacji pamięci ani narzutów związanych z prefiksem pamięci i późniejszych powiązanych śmieci koszty poboru. Sugerując mocno, że alokacja sterty nie jest trywialnym zadaniem, jeden oprogramowania typu open source , autorstwa twórcy gier Johna W. Ratcliffa , składa się z prawie 1000 linii kodu.

Dynamicznie wysyłane wywołania komunikatów a bezpośrednie narzuty wywołań procedur

W swoim streszczeniu „ Optymalizacja programów obiektowych przy użyciu statycznej analizy hierarchii klas ” Jeffrey Dean, David Grove i Craig Chambers z Wydziału Informatyki i Inżynierii Uniwersytetu Waszyngtońskiego twierdzą, że „duże wykorzystanie dziedziczenia i dynamiczne komunikaty powiązane prawdopodobnie sprawią, że kod będzie bardziej rozszerzalny i będzie można go ponownie wykorzystać, ale powoduje to również znaczny wzrost wydajności w stosunku do równoważnego, ale nierozszerzalnego programu napisanego w sposób nie zorientowany obiektowo. W niektórych domenach, takich jak pakiety grafiki strukturalnej , koszt wydajności związany z dodatkową elastycznością zapewnianą przez stosowanie stylu silnie zorientowanego obiektowo jest akceptowalny.Jednakże w innych dziedzinach, takich jak biblioteki podstawowych struktur danych, pakiety obliczeń numerycznych, biblioteki renderowania i ramy symulacji oparte na śledzeniu, koszt przekazywanie komunikatów może być zbyt duże, zmuszając programistę do unikania programowania obiektowego w „gorących punktach” ich aplikacji”.

Serializacja obiektów

Serializacja nakłada duże koszty ogólne podczas przekazywania obiektów z jednego systemu do drugiego, zwłaszcza gdy transfer odbywa się w formatach czytelnych dla człowieka, takich jak Extensible Markup Language ( XML ) i JavaScript Object Notation ( JSON ). Kontrastuje to z kompaktowymi formatami binarnymi dla danych niezorientowanych obiektowo. Zarówno kodowanie, jak i dekodowanie wartości danych obiektu i jego atrybutów jest zaangażowane w proces serializacji, który obejmuje również świadomość złożonych zagadnień, takich jak dziedziczenie, enkapsulacja i ukrywanie danych.

Równoległe obliczenia

Profesor Uniwersytetu Carnegie-Mellon, Robert Harper , napisał w marcu 2011 r.: „W tym semestrze Dan Licata i ja wspólnie prowadzimy nowy kurs programowania funkcjonalnego dla przyszłych kierunków CS na pierwszym roku… Programowanie obiektowe jest całkowicie wyeliminowane z programu wprowadzającego , ponieważ jest z natury zarówno antymodułowy, jak i antyrównoległy, a zatem nie nadaje się do nowoczesnego programu nauczania CS Proponowany nowy kurs metodologii projektowania zorientowanego obiektowo będzie oferowany na poziomie drugiego stopnia dla tych studentów, którzy chcą studiować ten temat."

Zobacz też

Dalsza lektura

Linki zewnętrzne