Porównanie paradygmatów programowania - Comparison of programming paradigms

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

Główne podejścia do 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 popularne 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 imperatywnym, funkcjonalnym lub proceduralnym stylu, który może, ale nie musi, bezpośrednio zmieniać stan w imieniu programu wywołującego. Paradygmaty w pewnym stopniu nakładają się na siebie, ale główne cechy lub możliwe do zidentyfikowania różnice podsumowano w poniższej tabeli:

Paradygmat Opis Główne cechy Powiązane paradygmaty Krytyka Przykłady
Tryb rozkazujący Programy jako instrukcje, które bezpośrednio zmieniają stan obliczeniowy ( pola danych ) Przypisania 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 Structograms , wgniecenia , brak lub ograniczone stosowanie goto sprawozdania Tryb rozkazujący C , C++ , Java , Kotlin , Pascal , PHP , Python
Proceduralny Wywodzi się z programowania strukturalnego, opartego na koncepcji programowania modułowego lub wywołania procedury Zmienne lokalne , sekwencja, selekcja, iteracja i modularyzacja Zorganizowany, imperatyw C , C++ , Lisp , PHP , Python
Funkcjonalny Traktuje obliczenia jako ocenę funkcji matematycznych z pominięciem stanu i danych mutowalnych Rachunek lambda , kompozycyjność , formuła , rekurencja , przezroczystość referencyjna , brak skutków ubocznych Deklaracyjny C++ , C# , Clojure , CoffeeScript , Elixir , Erlang , F# , Haskell , Java (od wersji 8), Kotlin , Lisp , Python , R , Ruby , Scala , SequenceL , Standard ML , JavaScript , Elm
Oparte na wydarzeniach, w tym na czas Przepływ sterowania jest określany głównie przez zdarzenia , takie jak kliknięcia myszą lub przerwania, w tym timer Pętla główna , programy obsługi zdarzeń, procesy asynchroniczne Proceduralny, przepływ danych JavaScript , ActionScript , Visual Basic , Elm
Zorientowany obiektowo Przysmaki pola danych jako obiektów manipulowanych przez predefiniowanych metod tylko Obiekty , metody , przekazywanie wiadomości , ukrywanie informacji , abstrakcja danych , enkapsulacja , polimorfizm , dziedziczenie , serializacja - marshalling Proceduralny Wikipedia , inne 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 raportów SQL , wyrażenia regularne , Prolog , OWL , SPARQL , Datalog , XSLT
Programowanie w oparciu o automaty Traktuje programy jako model automatu skończonego lub dowolnego innego automatu formalnego Wyliczenie stanów , zmienna sterująca , zmiany stanów , izomorfizm , tablica przejść stanów Imperatywna, sterowana zdarzeniami Abstrakcyjny język maszynowy

Różnice w terminologii

Pomimo wielu (rodzajó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 , podprogramy , wywołania itp.) i muszą w jakiś sposób nieuchronnie być włączone do każdego oddzielnego paradygmatu z równie podobnymi atrybutami lub funkcjami. 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. Dalsze komplikacje to niestandaryzowane implementacje każdego paradygmatu, w wielu językach programowania , zwłaszcza w językach obsługujących wiele paradygmatów , każdy z własnym żargonem .

Wsparcie językowe

Cukier syntaktyczny to osłodzenie funkcjonalności programu poprzez wprowadzenie funkcji językowych, które ułatwiają dane użytkowanie, nawet jeśli efekt końcowy można by osiągnąć bez nich. Jednym z przykładów cukru składniowego mogą być prawdopodobnie klasy używane w obiektowych językach programowania . Język imperatywny C może obsługiwać programowanie obiektowe za pomocą funkcji 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. Co więcej, specjalna składnia działa w celu podkreślenia podejścia obiektowego. Podobnie funkcje i składnia pętli w C (i innych proceduralnych i ustrukturyzowanych językach programowania) można uznać za cukier składniowy. Język asembler może obsługiwać programowanie proceduralne lub strukturalne poprzez swoje funkcje do 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 zostały zaprojektowane tak, aby dobre praktyki programistyczne były bardziej widoczne i naturalne.

Niektórzy programiści uważają, że te funkcje są nieistotne, a nawet niepoważne. Na przykład Alan Perlis zażartował kiedyś, w odniesieniu do języków oddzielonych nawiasami , że „cukier składniowy powoduje raka średnika ” (zobacz Epigramy na temat programowania ).

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

Porównanie wydajności

Tylko w całkowitej długości ścieżki instrukcji program zakodowany w stylu imperatywnym, bez podprogramów, miałby najmniejszą liczbę. Jednak rozmiar 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 bardziej nielokalnych instrukcji fizycznych, które mogą zwiększać braki w pamięci podręcznej i narzuty na pobieranie instrukcji w nowoczesnych procesorach .

Paradygmaty, które intensywnie wykorzystują podprogramy (w tym funkcjonalne, proceduralne i zorientowane obiektowo) i nie wykorzystują również znaczącego rozwijania wbudowanego ( wstawianie , poprzez optymalizacje kompilatora ), w konsekwencji będą wykorzystywać większą część całkowitych zasobów na połączeniach podprogramów. Programy zorientowane obiektowo, które celowo nie zmieniają bezpośrednio stanu programu , zamiast tego używają metod mutatorowych (lub ustawiających ) do enkapsulacji tych zmian stanu, będą, w bezpośredniej konsekwencji, miały 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ą . Uzyskiwanie pamięci ze sterty i kopiowanie parametrów do przekazywania wiadomości może wymagać znacznych zasobów, które znacznie przekraczają te potrzebne do zmiany stanu. Akcesory (lub pobierające ), które jedynie zwracają wartości prywatnych zmiennych składowych, również zależą od podobnych podprogramów przekazywania komunikatów, zamiast używania bardziej bezpośredniego przypisania (lub porównania), dodając do całkowitej długości ścieżki.

Kod zarządzany

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

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

Pseudokod porównanie imperatywu, proceduralnych i obiektowego podejścia wykorzystywane do obliczania powierzchni koła (πr²), zakładając, że nie podprogram inline żadne makro Preprocesory , zarejestruj arytmetyczny i ważąc każde „krok” instrukcji jak tylko 1 instrukcji - jako zgrubna miara długości ścieżki instrukcji – przedstawiona 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 powierzchni okręgu są takie same we wszystkich trzech paradygmatach, z tą różnicą, że paradygmaty proceduralne i obiektowe otaczają te operacje wywołaniem podprogramu, dzięki czemu obliczenia są ogólne i nadają się do wielokrotnego użytku. Ten sam efekt można osiągnąć w całkowicie imperatywnym programie używającym preprocesora makr tylko kosztem zwiększonego rozmiaru programu (tylko w każdym miejscu wywołania makr) bez odpowiadającego mu proporcjonalnego kosztu działania (proporcjonalnego do n wywołań – który może znajdować się w obrębie jednego na przykład wewnętrzna pętla ). I odwrotnie, wstawianie podprogramów przez kompilator może zredukować programy proceduralne do czegoś podobnego rozmiarem do kodu czysto imperatywnego. Jednak dla programów zorientowanych obiektowo, nawet z inlinem, komunikaty nadal muszą być budowane (z kopii argumentów) do przetwarzania przez metody obiektowe. Narzut wywołań, wirtualnych lub innych, nie jest zdominowany przez zmianę 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ę slajdową autorstwa Erica S. Robertsa ("Przydział pamięci do zmiennych", rozdział 7) - ilustrującą użycie pamięci stosu i sterty podczas sumowania trzech liczb wymiernych w zorientowanym obiektowo języku Java .

Tryb rozkazujący Proceduralny Zorientowany obiektowo
 load r;                      1
 r2 = r * r;                  2
 result = r2 * "3.142";       3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.... storage .............
result variable
constant "3.142"
area proc(r2,res):
   push stack                                 5
   load r2;                                   6
   r3 = r2 * r2;                              7
   res = r3 * "3.142";                        8
   pop stack                                  9
   return;                                   10
...............................................
main proc:
   load r;                                    1
   call area(r,result);
    +load p = address of parameter list;      2
    +load v = address of subroutine 'area';   3
    +goto v with return;                      4
.
.
.
.
.... storage .............
result variable
constant "3.142"
parameter list variable
function pointer (==>area)
stack storage
circle.area method(r2):
   push stack                                 7
   load r2;                                   8
   r3 = r2 * r2;                              9
   res = r3 * "3.142";                       10
   pop stack                                 11
   return(res);                           12,13
...............................................
main proc:
   load r;                                    1
   result = circle.area(r);
      +allocate heap storage;                 2
      +copy r to message;                     3
      +load p = address of message;           4
      +load v = addr. of method 'circle.area' 5
      +goto v with return;                    6
.
.
.... storage .............
result variable (assumed pre-allocated)
immutable variable "3.142" (final)
(heap) message variable for circle method call
vtable(==>area)
stack storage

Zalety abstrakcji proceduralnej i polimorfizmu w stylu obiektowym są słabo zilustrowane na małym przykładzie, takim 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, obciążenie wywołania metody

Obecność (nazywanej) podprogramu w programie nie wnosi nic dodatkowego do funkcjonalności programu, niezależnie od paradygmatu, ale może znacznie przyczynić się do strukturyzowania i ogólności programu, czyniąc go znacznie łatwiejszym do pisania, modyfikowania i rozszerzania. Zakres, 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 zauważył 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, w większości wdrożeń, ż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 przedstawia również przemyślany przypadek programowania opartego na automatach (używając wywołań procedur z rekurencją ogonową ) 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 w dużej mierze zależy od liczby dyskretnych procedur lub modułów .
  • W przypadku programowania funkcjonalnego częste są wywołania podprogramów bibliotecznych , ale często mogą być wbudowane przez kompilator optymalizujący
  • W przypadku programowania obiektowego liczba wywoływanych wywołań metod jest również częściowo określana 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 wykorzystania kodu , istnieje tendencja do tworzenia drobnoziarnistych struktur danych i odpowiedniego wzrostu liczby obiektów dyskretnych (i ich metod), a w konsekwencji wywołań podprogramów. Aktywnie odradza się tworzenie boskich obiektów . Konstruktory również dodają do licznika, ponieważ są również wywołaniami podprogramów (chyba że są wbudowane). 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, stosowanie podprogramów jest mniej przewidywalne.

Przydział pamięci dynamicznej do przechowywania wiadomości i obiektów

Wyjątkowo paradygmat zorientowany obiektowo obejmuje dynamiczną alokację pamięci z pamięci 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 firmę Digital Equipment Corporation na różnych programach, przy użyciu narzędzia do profilowania na poziomie instrukcji, mierzył liczbę instrukcji wymaganych w ramach dynamicznej alokacji pamięci. Wyniki pokazały, że najniższa bezwzględna liczba wykonanych instrukcji wynosiła średnio około 50, ale inne osiągały aż 611. Zobacz także „Heap:Pleasures and pains” autorstwa Murali R. Krishnana, który stwierdza: „Implementacje sterty mają tendencję do pozostawania ogólnymi na wszystkich platformach, a stąd mają duże obciążenie”. 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 ich liczbę 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 zaangażowanych kosztów prefiksów pamięci i związanych z tym śmieci koszty zbiórki. Jeden mikroalokator oprogramowania o otwartym kodzie źródłowym , autorstwa twórcy gier Johna W. Ratcliffa , który wyraźnie sugeruje, że alokacja sterty jest nietrywialnym zadaniem , składa się z prawie 1000 wierszy kodu.

Dynamicznie wysyłane wywołania wiadomości a bezpośrednie koszty wywołania procedury

Jeffrey Dean, David Grove i Craig Chambers z Wydziału Informatyki i Inżynierii Uniwersytetu Waszyngtońskiego w swoim streszczeniu „ Optymalizacja programów zorientowanych obiektowo przy użyciu statycznej analizy hierarchii klas ” twierdzą, że „Ciężkie wykorzystanie dziedziczenia i dynamiczne -Wiadomości powiązane mogą sprawić, że kod będzie bardziej rozszerzalny i możliwy do ponownego użycia, ale wiąże się to również ze znacznym obciążeniem wydajnościowym w stosunku do równoważnego, ale nierozszerzalnego programu napisanego w sposób niezorientowany obiektowo. , koszt wydajności dodatkowej elastyczności zapewnianej przez użycie stylu silnie zorientowanego obiektowo jest akceptowalny.Jednak w innych dziedzinach, takich jak biblioteki podstawowych struktur danych, pakiety do obliczeń numerycznych, biblioteki renderujące i frameworki 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 narzuty podczas przekazywania obiektów z jednego systemu do drugiego, zwłaszcza gdy przesyłanie 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 są zaangażowane w proces serializacji, który obejmuje również świadomość złożonych problemów, takich jak dziedziczenie, enkapsulacja i ukrywanie danych.

Równoległe obliczenia

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

Zobacz też

Bibliografia

Dalsza lektura

Zewnętrzne linki