Jakie problemy programowania proceduralnego rozwiązuje OOP w praktyce?


17

Przestudiowałem książkę „C ++ Demystified” . Teraz zacząłem czytać „Object-Oriented Programming in Turbo C ++ pierwsze wydanie (1. wydanie)” Roberta Lafore'a. Nie mam żadnej wiedzy o programowaniu, która wykracza poza te książki. Ta książka może być nieaktualna, ponieważ ma 20 lat. Mam najnowsze wydanie, używam starego, ponieważ mi się podoba, głównie studiuję podstawowe koncepcje OOP używane w C ++ w pierwszym wydaniu książki Lafore.

Książka Lafore podkreśla, że ​​„OOP” jest użyteczny tylko w przypadku większych i złożonych programów. W każdej książce OOP (także w książce Lafore'a) jest powiedziane, że paradygmat proceduralny jest podatny na błędy, np. Dane globalne, które są podatne na działanie funkcji. Mówi się, że programista może popełniać uczciwe błędy w językach proceduralnych, np. Wykonując funkcję, która przypadkowo uszkadza dane.

Szczerze mówiąc, zamieszczam moje pytanie, ponieważ nie rozumiem wyjaśnień zawartych w tej książce: Programowanie obiektowe w C ++ (wydanie 4). Nie rozumiem tych stwierdzeń napisanych w książce Lafore:

Programowanie obiektowe zostało opracowane, ponieważ we wcześniejszych podejściach do programowania odkryto ograniczenia .... W miarę jak programy stają się coraz większe i bardziej złożone, nawet podejście do programowania strukturalnego zaczyna wykazywać oznaki obciążenia ... .... Analizując przyczyny niepowodzenia te ujawniają słabości samego paradygmatu proceduralnego. Bez względu na to, jak dobrze wdrażane jest podejście programowania strukturalnego, duże programy stają się nadmiernie złożone ... ... Istnieją dwa powiązane problemy. Po pierwsze, funkcje mają nieograniczony dostęp do danych globalnych. Po drugie, niepowiązane funkcje i dane, będące podstawą paradygmatu proceduralnego, stanowią zły model realnego świata ...

Studiowałem książkę „dysmystified C ++” Jeffa Kenta, bardzo ją lubię, w tej książce wyjaśniono głównie programowanie proceduralne. Nie rozumiem, dlaczego programowanie proceduralne (strukturalne) jest słabe!

Książka Lafore'a bardzo ładnie wyjaśnia tę koncepcję, podając kilka dobrych przykładów. Zrozumiałem też intuicję, czytając książkę Lafore'a, że ​​OOP jest lepsze niż programowanie proceduralne, ale jestem ciekawy, jak dokładnie w praktyce programowanie proceduralne jest słabsze niż OOP.

Chcę się przekonać, jakie są praktyczne problemy, z jakimi można się spotkać w programowaniu proceduralnym, w jaki sposób OOP ułatwi programowanie. Myślę, że otrzymam odpowiedź, czytając po prostu książkę Lafore'a, ale chcę zobaczyć na własne oczy problemy z kodem proceduralnym, chcę zobaczyć, w jaki sposób kod w stylu OOP programu usuwa powyższe błędy, które wystąpiłyby, gdyby ten sam program miał być napisany z wykorzystaniem paradygmatu proceduralnego.

Istnieje wiele funkcji OOP i rozumiem, że ktoś nie może mi wyjaśnić, w jaki sposób wszystkie te funkcje usuwają powyższe błędy, które generowałyby się, pisząc kod w stylu proceduralnym.

Oto moje pytanie:

Jakie ograniczenia programowania proceduralnego rozwiązuje OOP i jak skutecznie usuwa te ograniczenia w praktyce?

W szczególności, czy istnieją przykłady programów trudnych do zaprojektowania za pomocą paradygmatu proceduralnego, ale które można łatwo zaprojektować za pomocą OOP?

PS: Krzyż wysłany z: /programming//q/22510004/3429430


3
Rozróżnienie między programowaniem proceduralnym a programowaniem obiektowym jest do pewnego stopnia kwestią prezentacji i nacisku. Większość języków, które reklamują się jako zorientowane obiektowo, ma również charakter proceduralny - terminy dotyczą różnych aspektów języka.
Gilles „SO- przestań być zły”

2
Ponownie otworzyłem pytanie; zobaczmy jak to idzie. Należy pamiętać, że każde leczenie, które przedstawia OOP, to święty Graal, rozwiązujący wszystkie problemy mesjasz języków programowania jest bzdurą. Są plusy i minusy. Pytasz o zalety, a to jest uczciwe pytanie; nie oczekuj więcej.
Raphael

Myśl o zaprojektowaniu (nowoczesnego) graficznego interfejsu użytkownika bez OOP i niektórych technik, które się na nim wyrosły (np. Wzorzec zdarzenia / detektora) przeraża mnie. Nie oznacza to jednak, że nie można tego zrobić (na pewno można ). Ponadto, jeśli chcesz zobaczyć awarię PP w pracy, spójrz na PHP i porównaj API z, powiedzmy, Ruby.
Raphael

1
„Bez względu na to, jak dobrze zorganizowane podejście Programowanie jest realizowane duże programy stają się nadmiernie skomplikowane ...” to jest bardzo prawdziwe OOP też ale to w zasadzie robi zarządzać złożoności lepiej jeśli zastosowane w wyznaczonym sposób przez programistów ... i robi w dużej mierze dzięki lepszym / ostrzejszym granicom / ograniczeniom zakresu, tj.
rodzajowi

Odpowiedzi:


9

W języku proceduralnym niekoniecznie wyrażasz ograniczenia, które są wymagane, aby udowodnić, że osoba dzwoniąca używa modułu w obsługiwany sposób. W przypadku braku ograniczenia możliwego do sprawdzenia przez kompilator, musisz napisać dokumentację i mieć nadzieję, że będzie ona przestrzegana, oraz użyj testów jednostkowych, aby wykazać zamierzone zastosowania.

Deklarowanie typów jest najbardziej oczywistym ograniczeniem deklaratywnym (tj .: „udowodnij, że x jest liczbą zmiennoprzecinkową”). Zmuszanie mutacji danych do przejścia przez funkcję, o której wiadomo, że jest zaprojektowane dla tych danych, to kolejna sprawa. Wymuszanie protokołu (kolejność wywoływania metod) to kolejne częściowo obsługiwane ograniczenie, tj .: „Konstruktor -> Inne metody * -> Destruktor”.

Istnieją również prawdziwe zalety (i kilka wad), gdy kompilator wie o wzorcu. Wpisywanie statyczne z typami polimorficznymi stanowi pewien problem, gdy emulujesz enkapsulację danych z języka proceduralnego. Na przykład:

typ x1 jest podtypem x, t1 jest podtypem t

Jest to jeden ze sposobów enkapsulacji danych w języku proceduralnym w celu uzyskania typu t metodami f i g oraz podklasy t1, która działa podobnie:

t_f (t, x, y, z, ...), t_g (t, x, y, ...) t1_f (t1, x, y, z, ...)

Aby użyć tego kodu w obecnej postaci, musisz sprawdzić typ i włączyć typ t przed podjęciem decyzji o rodzaju f, który chcesz wywołać. Możesz obejść to w ten sposób:

wpisz t {d: dane f: funkcja g: funkcja}

Aby zamiast tego wywołać tf (x, y, z), w którym sprawdzenie typu i przełącznik w celu znalezienia metody jest teraz zastępowany jedynie jawnym wskazywaniem wskaźników instancji dla każdej instancji. Teraz, jeśli masz ogromną liczbę funkcji dla każdego typu, jest to marnotrawna reprezentacja. Mógłbyś wtedy użyć innej strategii, takiej jak wskazanie t zmiennej m, która zawiera wszystkie funkcje składowe. Jeśli ta funkcja jest częścią języka, możesz pozwolić kompilatorowi dowiedzieć się, jak radzić sobie z wydajnym przedstawieniem tego wzorca.

Ale enkapsulacja danych to rozpoznanie, że stan zmiennych jest zły. Obiektowym rozwiązaniem jest ukrywanie go za metodami. Idealnie byłoby, gdyby wszystkie metody w obiekcie miały dobrze zdefiniowaną kolejność wywoływania (tj .: konstruktor -> open -> [read | write] -> close -> destruct); który jest czasem nazywany „protokołem” (badanie: „Microsoft Singularity”). Ale poza konstruowaniem i niszczeniem, wymagania te na ogół nie są częścią systemu typów - ani dobrze udokumentowane. W tym sensie obiekty są równoczesnymi wystąpieniami maszyn stanów, które są przenoszone przez wywołania metod; tak, że możesz mieć wiele instancji i używać ich w sposób arbitralnie przeplatany.

Ale rozpoznając, że zmienny stan współdzielenia jest zły, można zauważyć, że orientacja obiektowa może stworzyć problem współbieżności, ponieważ struktura danych obiektowych jest stanem zmiennym, do którego odwołuje się wiele obiektów. Większość języków zorientowanych obiektowo wykonuje się w wątku wywołującego, co oznacza, że ​​w wywołaniu metody występują warunki wyścigu; nie mówiąc już o nieatomowych ciągach wywołań funkcji. Alternatywnie, każdy obiekt może otrzymywać wiadomości asynchroniczne w kolejce i obsługiwać je wszystkie w wątku obiektu (metodami prywatnymi) i odpowiadać na telefon wywołujący, wysyłając mu wiadomości.

Porównaj wywołania metod Java w kontekście wielowątkowym z procesami Erlanga wysyłającymi do siebie wiadomości (które odwołują się tylko do niezmiennych wartości).

Nieograniczona orientacja obiektu w połączeniu z równoległością stanowi problem z powodu blokowania. Istnieją techniki od Software Transactional Memory (tj. Transakcje ACID w obiekcie pamięci podobnym do baz danych) po stosowanie podejścia „współdziel pamięć przez komunikację (dane niezmienne)” (hybrydowe programowanie funkcjonalne).

Moim zdaniem literatura Object Orientation zbytnio koncentruje FAR na dziedziczeniu, a zbyt mało na protokole (porządkowanie wywoływanych metod, warunki wstępne, warunki dodatkowe itp.). Dane wejściowe, które zużywa obiekt, powinny zwykle mieć dobrze zdefiniowaną gramatykę, wyrażalną jako typ.


Czy mówisz, że w językach OO kompilator może sprawdzić, czy metody są używane w zalecanej kolejności, czy inne ograniczenia dotyczące używania modułów? Dlaczego „enkapsulacja danych [...] rozpoznaje zły stan zmienności”? Kiedy mówisz o polimorfizmie, czy zakładasz, że używasz języka OO?
babou

W OO najważniejszą możliwością jest ukrywanie struktury danych (tj. Wymaganie, aby referencje były z tego typu. X), aby udowodnić, że wszystkie dostępy przechodzą metodami. W statycznie pisanym języku OO deklarujesz jeszcze więcej ograniczeń (w zależności od typów). Uwaga na temat porządkowania metod mówi tylko, że OO zmusza konstruktora do wywołania pierwszego, a destruktora ostatniego; co jest pierwszym krokiem strukturalnie zabraniającym zleceń dotyczących złych połączeń. Łatwe proofy to ważny cel w projektowaniu języka.
Rob

8

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 .


8

Myślę, że trochę historii jest w porządku.

Era od połowy lat sześćdziesiątych do połowy lat siedemdziesiątych jest dziś znana jako „kryzys oprogramowania”. Nie potrafię tego lepiej ująć niż Dijkstra w wykładzie z nagrody Turinga z 1972 r .:

Główną przyczyną kryzysu oprogramowania jest to, że maszyny stały się o kilka rzędów wielkości potężniejsze! Mówiąc wprost: tak długo, jak nie ma maszyn, programowanie nie stanowi żadnego problemu; kiedy mieliśmy kilka słabych komputerów, programowanie stało się łagodnym problemem, a teraz mamy gigantyczne komputery, programowanie stało się równie gigantycznym problemem.

To był czas pierwszych 32-bitowych komputerów, pierwszych prawdziwych wieloprocesorów i pierwszych komputerów wbudowanych, i dla badaczy było jasne, że będą one ważne dla programowania w przyszłości. Był to czas w historii, w którym po raz pierwszy zapotrzebowanie klientów przekroczyło możliwości programistyczne.

Nic dziwnego, że był to niezwykle płodny okres w programowaniu badań. Przed połową lat sześćdziesiątych mieliśmy LISP i AP / L, ale „główne” języki były zasadniczo proceduralne: FORTRAN, ALGOL, COBOL, PL / I i tak dalej. Od połowy lat 60. do połowy lat 70. dostaliśmy Logo, Pascal, C, Forth, Smalltalk, Prolog, ML i Modula, a to nie liczy DSL, takich jak SQL i jego poprzednicy.

Był to także czas w historii, w którym opracowywano wiele kluczowych technik wdrażania języków programowania. W tym okresie otrzymaliśmy analizę LR, analizę przepływu danych, wspólną eliminację podwyrażeń i pierwsze rozpoznanie, że niektóre problemy kompilatora (np. Alokacja rejestru) były trudne dla NP, i próbowaliśmy sobie z nimi poradzić.

W tym kontekście powstał OOP. Tak więc odpowiedź na pytanie, jakie problemy OOP rozwiązuje w praktyce na początku lat siedemdziesiątych, pierwsza odpowiedź jest taka, że ​​wydawało się, że rozwiązało wiele problemów (zarówno współczesnych, jak i przewidywanych), z którymi borykali się programiści w tym okresie historii. Nie jest to jednak czas, kiedy OO stało się głównym nurtem. Wkrótce do tego dojdziemy.

Kiedy Alan Kay wymyślił termin „obiektowy”, miał na myśli obraz, że systemy oprogramowania będą miały strukturę biologiczną. Miałbyś coś w rodzaju pojedynczych komórek („obiektów”), które współdziałają ze sobą, wysyłając coś analogicznego do sygnałów chemicznych („wiadomości”). Nie można (a przynajmniej nie) zajrzeć do wnętrza komórki; wchodzilibyście w interakcję tylko za pośrednictwem ścieżek sygnalizacyjnych. Co więcej, możesz mieć więcej niż jedną komórkę każdego rodzaju, jeśli zajdzie taka potrzeba.

Widać tutaj, że istnieje kilka ważnych tematów: koncepcja dobrze zdefiniowanego protokołu sygnalizacyjnego (we współczesnej terminologii, interfejs), koncepcja ukrywania implementacji z zewnątrz (we współczesnej terminologii, prywatności) oraz koncepcja mając wiele „rzeczy” tego samego typu kręcących się w tym samym czasie (w nowoczesnej terminologii, instancji).

Brakuje jednej rzeczy, którą możesz zauważyć, a mianowicie dziedziczenia, i jest ku temu powód.

Programowanie obiektowe jest pojęciem abstrakcyjnym, a pojęcie abstrakcyjne można wdrażać na różne sposoby w różnych językach programowania. Na przykład abstrakcyjna koncepcja „metody” mogłaby zostać zaimplementowana w C za pomocą wskaźników funkcji, aw C ++ za pomocą funkcji składowych oraz w Smalltalk za pomocą metod (co powinno być zaskakujące, ponieważ Smalltalk implementuje abstrakcyjną koncepcję niemal bezpośrednio). To właśnie ludzie mają na myśli, gdy zwracają uwagę (całkiem słusznie), że można „robić” OOP w (prawie) dowolnym języku.

Dziedziczenie jest natomiast konkretną cechą języka programowania. Dziedziczenie może być przydatne do wdrażania systemów OOP. A przynajmniej tak było do wczesnych lat dziewięćdziesiątych.

Czas od połowy lat 80. do połowy lat 90. był także czasem w historii, gdy wszystko się zmieniało. W tym czasie pojawił się tani, wszechobecny 32-bitowy komputer, więc firmy i wiele domów mogło sobie pozwolić na umieszczenie na każdym biurku komputera, który był prawie tak potężny jak mainframe najniższej klasy dnia. Był to także okres świetności. To był także okres rozwoju nowoczesnego GUI i sieciowego systemu operacyjnego.

Właśnie w tym kontekście pojawiła się analiza i projektowanie obiektowe.

Wpływ OOAD, pracy „trzech Amigos” (Booch, Rumbar i Jacobsona) i innych (np. Metoda Shlaera – Mellora, projektowanie zorientowane na odpowiedzialność itp.) Nie może być niedoceniany. To jest powód, dla którego większość nowych języków, które zostały opracowane od początku lat 90. (przynajmniej większość z tych, o których słyszałeś) ma obiekty w stylu Simula.

Tak więc odpowiedź na twoje pytanie z lat 90. brzmi: obsługuje najlepsze (w tym czasie) rozwiązanie dla analizy zorientowanej na domenę i metodologii projektowania.

Od tego czasu, ponieważ mieliśmy młotek, zastosowaliśmy OOP do prawie każdego problemu, który pojawił się od tego czasu. OOAD i użyty przez niego model obiektowy zachęcały do ​​zwinnego i opartego na testach rozwoju, klastra i innych systemów rozproszonych itd.

Nowoczesne GUI i wszelkie systemy operacyjne, które zostały zaprojektowane w ciągu ostatnich 20 lat, zwykle świadczą swoje usługi jako obiekty, więc każdy nowy praktyczny język programowania potrzebuje przynajmniej sposobu na połączenie się z systemami, z których obecnie korzystamy.

Tak więc współczesna odpowiedź brzmi: rozwiązuje problem łączenia się ze współczesnym światem. Współczesny świat zbudowany jest na OOP z tego samego powodu, dla którego świat 1880 został zbudowany na parze: rozumiemy go, możemy go kontrolować i wystarczająco dobrze sobie radzi.

Nie oznacza to oczywiście, że badania kończą się tutaj, ale zdecydowanie wskazuje, że każda nowa technologia będzie wymagać OO jako ograniczającego przypadku. Nie musisz być OO, ale nie możesz być z nim zasadniczo niezgodny.


Poza tym, którego nie chciałem umieszczać w głównym eseju, GUI WIMP i OOP wydają się niezwykle naturalne. Wiele złych rzeczy można powiedzieć o hierarchiach głębokiego dziedziczenia, ale jest to jedna sytuacja (prawdopodobnie TYLKO sytuacja), w której wydaje się to mieć jakiś sens.
pseudonim

1
OOP pojawił się pierwszy w Simula-67 (symulacja), w wewnętrznej organizacji systemów operacyjnych (idea „klasy urządzeń” w Uniksie jest zasadniczo klasą, z której dziedziczą sterowniki). Parnas „O kryteriach stosowanych przy rozkładaniu systemów na moduły” , CACM 15:12 (1972), str. 1052-1058, język Modula Wirtha z lat siedemdziesiątych, „abstrakcyjne typy danych” są prekursorami w jeden sposób inny.
vonbrand,

To wszystko prawda, ale utrzymuję, że OOP nie było postrzegane jako „rozwiązanie problemu programowania proceduralnego” aż do połowy lat 70. Zdefiniowanie „OOP” jest niezwykle trudne. Oryginalne użycie tego terminu przez Alana Kaya nie zgadza się z modelem Simuli i niestety świat ustandaryzował model Simuli. Niektóre modele obiektów mają interpretację Curry-Howarda, ale Simula nie. Stepanov prawdopodobnie miał rację, gdy zauważył, że dziedziczenie nie jest uzasadnione.
pseudonim

6

Naprawdę brak. OOP tak naprawdę nie rozwiązuje problemu, mówiąc ściśle; nic nie można zrobić z systemem obiektowym, czego nie można zrobić z systemem nie zorientowanym obiektowo - w rzeczywistości nic nie można zrobić z żadnym z nich, czego nie można zrobić za pomocą maszyny Turinga. W końcu wszystko zamienia się w kod maszynowy, a ASM z pewnością nie jest obiektowy.

To, co robi dla ciebie paradygmat OOP, ułatwia organizowanie zmiennych i funkcji oraz pozwala łatwiej je przenosić.

Powiedz, że chcę napisać grę karcianą w języku Python. Jak miałbym reprezentować karty?

Gdybym nie wiedział o OOP, mógłbym to zrobić w ten sposób:

cards=["1S","2S","3S","4S","5S","6S","7S","8S","9S","10S","JS","QS","KS","1H","2H",...,"10C","JC","QC","KC"]

Prawdopodobnie napisałbym kod, aby wygenerować te karty, zamiast pisać je ręcznie, ale masz rację. „1S” oznacza 1 pik, „JD” oznacza walet karo i tak dalej. Potrzebowałbym również kodu dla Jokera, ale udajemy, że na razie nie ma Jokera.

Teraz, gdy chcę przetasować talię, potrzebuję tylko „przetasować” listę. Następnie, aby zdjąć kartę ze szczytu talii, usuwam górny wpis z listy, podając mi sznurek. Prosty.

Teraz, jeśli chcę dowiedzieć się, z którą kartą pracuję w celu wyświetlenia jej w odtwarzaczu, potrzebowałbym takiej funkcji:

def card_code_to_name(code):
    suit=code[1]

    if suit=="S":
        suit="Spades"
    elif suit=="H"
        suit="Hearts"
    elif suit=="D"
        suit="Diamonds"
    elif suit=="C"
        suit="Clubs"

    value=code[0]

    if value=="J":
        value="Jack"
    elif value="Q":
        value="Queen"
    elif value="K"
        value="King"

    return value+" of "+suit

Trochę duży, długi i nieefektywny, ale działa (i jest bardzo mało mityczny, ale tutaj nie ma sensu).

Co teraz, jeśli chcę, aby karty mogły poruszać się po ekranie? Muszę jakoś zapamiętać ich pozycję. Mógłbym dodać to na końcu kodu karty, ale to może być trochę niewygodne. Zamiast tego stwórzmy inną listę, gdzie jest każda karta:

cardpositions=( (1,1), (2,1), (3,1) ...)

Następnie piszę mój kod, aby indeks pozycji każdej karty na liście był taki sam jak indeks samej karty w talii.

A przynajmniej powinno być. Chyba że popełniam błąd. Co mogę bardzo dobrze, ponieważ mój kod będzie musiał być dość skomplikowany, aby obsłużyć tę konfigurację. Kiedy chcę przetasować karty, muszę przetasować pozycje w tej samej kolejności. Co się stanie, jeśli całkowicie wyjmę kartę z talii? Będę musiał także zająć jego stanowisko i umieścić je gdzie indziej.

A jeśli chcę przechowywać jeszcze więcej informacji o kartach? Co jeśli chcę zapisać, czy każda karta jest odwrócona? Co jeśli chcę jakiegoś silnika fizyki i muszę znać prędkość kart? Potrzebuję kolejnej listy do przechowywania grafiki dla każdej karty! I dla wszystkich tych punktów danych potrzebuję osobnego kodu, aby wszystkie były odpowiednio zorganizowane, więc każda karta w jakiś sposób odwzorowuje wszystkie swoje dane!

Teraz spróbujmy to na OOP.

Zamiast listy kodów zdefiniujmy klasę Card i stwórzmy z niej listę obiektów Card.

class Card:

    def __init__(self,value,suit,pos,sprite,flipped=False):
        self.value=value
        self.suit=suit
        self.pos=pos
        self.sprite=sprite
        self.flipped=flipped

    def __str__(self):
        return self.value+" of "+self.suit

    def flip(self):
        if self.flipped:
            self.flipped=False
            self.sprite=load_card_sprite(value, suit)
        else:
            self.flipped=True
            self.sprite=load_card_back_sprite()

deck=[]
for suit in ("Spades","Hearts","Diamonds","Clubs"):
    for value in ("1","2","3","4","5","6","7","8","9","10","Jack","Queen","King"):
        sprite=load_card_sprite(value, suit)
        thecard=Card(value,suit,(0,0),sprite)
        deck.append(thecard)

Teraz nagle wszystko jest znacznie prostsze. Jeśli chcę przesunąć kartę, nie muszę ustalać, gdzie jest ona w talii, a następnie użyj jej, aby uzyskać jej pozycję z szeregu pozycji. Muszę tylko powiedziećthecard.pos=newpos . Kiedy wyjmuję kartę z listy głównej talii, nie muszę tworzyć nowej listy do przechowywania wszystkich innych danych; kiedy obiekt karty się porusza, wszystkie jego właściwości poruszają się wraz z nim. A jeśli chcę karty, która zachowuje się inaczej po odwróceniu, nie muszę modyfikować funkcji odwracania w moim głównym kodzie, aby wykrywał te karty i zachowywał się inaczej; Muszę tylko podklasować kartę i modyfikować funkcję flip () w podklasie.

Ale nic, co tam zrobiłem, nie byłoby możliwe bez OO. Po prostu z językiem zorientowanym obiektowo, język wykonuje wiele pracy, aby utrzymać wszystko razem, co oznacza, że ​​masz znacznie mniejszą szansę na popełnienie błędów, a Twój kod jest krótszy i łatwiejszy do odczytania i napisania.

Lub, podsumowując jeszcze bardziej, OO pozwala pisać prostsze z pozoru programy, które wykonują tę samą pracę, co programy bardziej złożone, ukrywając wiele typowych zawiłości obsługi danych za zasłoną abstrakcji.


1
Jeśli jedyną rzeczą, którą zabierasz OOP, jest „zarządzanie pamięcią”, to nie sądzę, że dobrze to zrozumiałeś. Istnieje cała filozofia projektowania i hojna butelka „poprawnego projektu”! Ponadto zarządzanie pamięcią z pewnością nie jest nieodłącznym elementem orientacji obiektowej (C ++?), Nawet jeśli potrzeba staje się bardziej wyraźna.
Raphael

Jasne, ale to wersja z jednym zdaniem. Użyłem tego terminu również w dość niestandardowy sposób. Być może lepiej byłoby powiedzieć „obsługa informacji” niż „zarządzanie pamięcią”.
Schilcote,

Czy istnieją języki inne niż OOP, które pozwalałyby funkcji pobrać wskaźnik do czegoś, a także wskaźnik do funkcji, której pierwszym parametrem jest wskaźnik do tego samego rodzaju, i umożliwić kompilatorowi sprawdzenie, czy funkcja jest odpowiednia dla przekazanego wskaźnika?
supercat

3

Po kilku latach pisania wbudowanego C zarządzającego takimi urządzeniami, portami szeregowymi i pakietami komunikacyjnymi między portami szeregowymi, portami sieciowymi i serwerami; Znalazłem się, wyszkolony inżynier elektryk z ograniczonym doświadczeniem w programowaniu proceduralnym, wymyślając własne abstrakty ze sprzętu, który ostatecznie objawił się w tym, co później zrozumiałem, że to, co normalni ludzie nazywają programowaniem obiektowym.

Kiedy przeniosłem się na stronę serwera, byłem wystawiony na fabrykę, która ustawiała reprezentację obiektową każdego urządzenia w pamięci podczas tworzenia instancji. Z początku nie rozumiałem słów ani tego, co się działo - po prostu wiedziałem, że poszedłem do pliku o nazwie tak i tam i napisałem kod. Później znów odkryłem wartość OOP.

Ja osobiście uważam, że jest to jedyny sposób nauczenia orientacji obiektowej. Miałem klasę Intro to OOP (Java) na pierwszym roku i było to całkowicie nad moją głową. Opisy OOP oparte na klasyfikacji kociaka-> kota-> ssaka-> żywego-> rzeczy lub liścia-> gałęzi-> drzewa-> ogrodu są, moim skromnym zdaniem, całkowicie absurdalną metodologią, ponieważ nikt nigdy nie będzie próbował rozwiązać tych problemów problemy, jeśli można je nawet nazwać problemami ...

Myślę, że łatwiej jest odpowiedzieć na twoje pytanie, jeśli spojrzysz na to w sposób mniej bezwzględny - nie „co rozwiązuje”, ale bardziej z punktu widzenia „oto problem, a oto, jak to ułatwia”. W moim szczególnym przypadku portów szeregowych mieliśmy kilka kompilacji #ifdefs, które dodały i usuwały kod, który statycznie otwierał i zamykał porty szeregowe. Funkcje otwierania portów były wywoływane wszędzie i mogły być zlokalizowane w dowolnym miejscu w 100k liniach kodu systemu operacyjnego, który mieliśmy, a IDE nie wyszarzyło tego, co nie zostało zdefiniowane - trzeba było to wyśledzić ręcznie i nosić to w głowie. Nieuchronnie możesz mieć kilka zadań próbujących otworzyć dany port szeregowy, oczekując ich urządzenia na drugim końcu, a następnie żaden z napisanych kodów nie działa i nie możesz zrozumieć, dlaczego.

Abstrakcja była, choć wciąż w C, „klasą” portu szeregowego (no cóż, tylko typem struktury), którą mieliśmy tablicę - po jednym dla każdego portu szeregowego - i zamiast [ekwiwalentu DMA w portach szeregowych] Funkcje „OpenSerialPortA” „SetBaudRate” itp. Wywoływane bezpośrednio na sprzęcie z zadania, wywołaliśmy funkcję pomocniczą, do której przekazano wszystkie parametry komunikacyjne (baud, parzystość itp.), Która najpierw sprawdziła tablicę struktury, aby sprawdzić, czy to port został już otwarty - jeśli tak, to według którego zadania, które powiedziałby ci jako printf debugowania, abyś mógł natychmiast przejść do sekcji kodu, którą musisz wyłączyć - a jeśli nie, to przystąpił do ustawiania wszystkie parametry dzięki ich funkcjom montażu HAL i wreszcie otworzyły port.

Oczywiście istnieje również niebezpieczeństwo dla OOP. Kiedy w końcu posprzątałem tę bazę kodu i wszystko uporządkowałem i uporządkowałem - pisanie nowych sterowników dla tej linii produktów było w końcu możliwe do obliczenia, przewidywalne, mój kierownik EOL opracował produkt specjalnie, ponieważ był to o jeden projekt mniej potrzebny zarządzać, a on był na granicy usuwalnego średniego kierownictwa. Co mnie bardzo zabrało / bardzo mnie zniechęciło, więc rzuciłem pracę. LOL.


1
Cześć! To bardziej przypomina osobistą historię niż odpowiedź na pytanie. Z twojego przykładu widzę, że przepisałeś jakiś okropny kod w stylu obiektowym, co poprawiło go. Nie jest jednak jasne, czy poprawa miała wiele wspólnego z orientacją obiektową, czy tylko dlatego, że do tego czasu byłeś bardziej zaawansowanym programistą. Na przykład, znaczna część twojego problemu wydaje się być spowodowana rozrzuceniem kodu nie chcąc o tym miejscu. Można to rozwiązać, pisząc bibliotekę proceduralną, bez żadnych przedmiotów.
David Richerby,

2
@DavidRicherby mieliśmy bibliotekę procedur, ale to, co zaniechaliśmy, nie dotyczyło tylko tego, że kod jest wszędzie. Chodziło o to, że zrobiliśmy to wstecz. Nikt nie próbował OOP, stało się to po prostu naturalnie.
paIncrease

@DavidRicherby czy możesz podać jakieś przykłady implementacji bibliotek proceduralnych, aby upewnić się, że mówimy o tym samym?
paIncrease

2
Dziękujemy za odpowiedź i +1. Dawno temu inny doświadczony programista podzielił się tym, jak OOP uczynił swój projekt bardziej niezawodnym forums.devshed.com/programming-42/... Myślę, że OOP został zaprojektowany bardzo inteligentnie przez niektórych profesjonalistów, którzy mogli mieć problemy z podejściem proceduralnym.
user31782,

2

istnieje wiele twierdzeń i zamiarów dotyczących tego, co / gdzie programowanie OOP ma przewagę nad programowaniem proceduralnym, w tym przez jego wynalazców i użytkowników. ale tylko dlatego, że technologia została zaprojektowana w określonym celu przez jej projektantów, nie gwarantuje osiągnięcia tych celów. jest to kluczowe zrozumienie w dziedzinie inżynierii oprogramowania pochodzącej ze słynnego eseju Brooksa „No silver bullet”, które jest nadal aktualne pomimo rewolucji kodowania OOP. (zobacz także cykl Gypener Hype dla nowych technologii).

wielu, którzy skorzystali z obu, ma również opinie z niepotwierdzonego doświadczenia, a to ma pewną wartość, ale w wielu badaniach naukowych wiadomo, że samoocena analizy może być niedokładna. wydaje się, że niewiele jest ilościowej analizy tych różnic, a jeśli tak, to nie jest ona tak często cytowana. jest to nieco zdumiewające ilu komputerowych naukowcy mówić autorytatywnie na pewnych tematów centralnych do swojej dziedzinie jeszcze nie faktycznie przytoczyć badania naukowe na poparcie swoich poglądów i nie zdają sobie sprawy, że są faktycznie przekazując konwencjonalnej mądrości w swojej dziedzinie (choć powszechne ).

ponieważ jest to strona naukowa / forum, oto krótka próba postawienia licznych opinii na solidniejszych podstawach i oszacowania rzeczywistej różnicy. mogą istnieć inne badania i mam nadzieję, że inni mogą je wskazać, jeśli o nich usłyszą. (pytanie zen: jeśli naprawdę jest zasadnicza różnica, a tak duży wysiłek w dziedzinie inżynierii oprogramowania komercyjnego i gdzie indziej został włożony / zainwestowany w realizację tego, dlaczego naukowe dowody tego są tak trudne do zdobycia? jakieś klasyczne, często cytowane odniesienie w tej dziedzinie, które ostatecznie określa różnicę?)

ten artykuł wykorzystuje analizę eksperymentalną / ilościową / naukową i szczególnie potwierdza, że ​​zrozumienie przez początkujących programistów jest ulepszone metodami kodowania OOP w niektórych przypadkach, ale w innych przypadkach było niejednoznaczne (w odniesieniu do rozmiarów programów). zauważ, że jest to tylko jedno z wielu / głównych twierdzeń na temat wyższości OOP, które jest podnoszone w innych odpowiedziach i przez zwolenników OOP. badanie prawdopodobnie mierzyło element psychologiczny zwany rozumieniem kodowania „obciążenia poznawczego / kosztów ogólnych” .

  • Porównanie rozumienia programów obiektowych i proceduralnych przez początkujących programistów wchodzących w interakcje z komputerami. Susan Wiedenbeck, Vennila Ramalingam, Suseela Sarasamma, Cynthia L Corritore (1999)

    W tym artykule opisano dwa eksperymenty porównujące reprezentacje mentalne i rozumienie programu przez nowicjuszy w stylach obiektowych i proceduralnych. Badani byli początkującymi programistami zapisanymi na drugi kurs programowania, który uczył paradygmatu zorientowanego obiektowo lub proceduralnego. W pierwszym eksperymencie porównano reprezentacje mentalne i zrozumienie krótkich programów napisanych w stylach proceduralnych i obiektowych. Drugi eksperyment rozszerzył badanie na większy program obejmujący bardziej zaawansowane funkcje językowe. W przypadku krótkich programów nie było znaczącej różnicy między obiema grupami w odniesieniu do całkowitej liczby pytań, na które udzielono prawidłowych odpowiedzi, ale podmioty zorientowane obiektowo były lepsze od przedmiotów proceduralnych w odpowiedziach na pytania dotyczące funkcji programu. Sugeruje to, że informacje o funkcjach były łatwiej dostępne w ich mentalnej reprezentacji programów i popiera argument, że notacja obiektowa podkreśla funkcje na poziomie poszczególnych klas. W przypadku długiego programu nie znaleziono odpowiedniego efektu. Rozumienie tematów proceduralnych było lepsze niż tematów zorientowanych obiektowo we wszystkich rodzajach pytań. Trudności napotykane przez podmioty zorientowane obiektowo w odpowiadaniu na pytania w większym programie sugerują, że napotkali oni problemy z gromadzeniem informacji i wyciąganiem z nich wniosków. Sugerujemy, że wynik ten może być związany z dłuższą krzywą uczenia się dla nowicjuszy stylu obiektowego, a także z cechami stylu OO i konkretnej notacji języka OO.

Zobacz też:


1
Mam szacunek dla badań eksperymentalnych. Pozostaje jednak kwestia ustalenia, czy odpowiadają one na właściwe pytania. Istnieje zbyt wiele zmiennych w tym, co można nazwać OOP i pod względem sposobów jego wykorzystania, aby jedno badanie miało sens, imho. Jak wiele rzeczy w programowaniu, OOP został stworzony przez ekspertów w celu zaspokojenia ich własnych potrzeb . Omawiając przydatność OOP (której nie wziąłem za temat PO, czyli raczej to, czy dotyczy on niedociągnięcia programowania proceduralnego), można zapytać: jaką cechę, dla kogo, w jakim celu? Dopiero wtedy badania terenowe stają się w pełni sensowne.
babou

1
Ostrzeżenie anegdoty: jeśli problem jest niewielki (np. Do około 500-1000 wierszy kodu), OOP nie robi różnicy w moim doświadczeniu, może nawet przeszkadzać, martwiąc się o rzeczy, które mają niewielką różnicę. Jeśli problem jest duży i zawiera pewną formę „elementów wymiennych”, które ponadto muszą być możliwe do dodania później (okna w graficznym interfejsie użytkownika, urządzenia w systemie operacyjnym, ...) , dyscyplina organizacyjna OOP jest nieunikniona. Z pewnością możesz programować OOP bez obsługi języka (patrz np. Jądro Linuksa).
vonbrand,

1

Bądź ostrożny. Przeczytaj klasyk R. Kinga „Mój kot jest zorientowany obiektowo” w „Pojęciach zorientowanych obiektowo, bazach danych i aplikacjach” (Kim i Lochovsky, red.) (ACM, 1989). „Obiektowe” stało się bardziej modnym słowem niż jednoznaczną koncepcją.

Poza tym istnieje wiele odmian tematu, które niewiele mają wspólnego. Istnieją języki oparte na prototypach (dziedziczenie pochodzi od obiektów, nie ma klas jako takich) i języki oparte na klasach. Istnieją języki, które pozwalają na wielokrotne dziedziczenie, inne nie. Niektóre języki mają takie pojęcie, jak interfejsy Javy (można je traktować jako formę rozwodnionego wielokrotnego dziedziczenia). Istnieje pomysł na mixiny. Dziedziczenie może być raczej ścisłe (jak w C ++, tak naprawdę nie może tak naprawdę zmienić tego, co dostajesz w podklasie), lub bardzo swobodnie obsługiwane (w Perlu podklasa może przedefiniować prawie wszystko). Niektóre języki mają jeden katalog główny do dziedziczenia (zwykle nazywany Obiektem, z zachowaniem domyślnym), inne pozwalają programistowi na tworzenie wielu drzew. Niektóre języki twierdzą, że „wszystko jest przedmiotem”, inne obsługują obiekty i obiekty niebędące obiektami, niektóre (jak Java) mają „większość to obiekty, ale tych kilku typów tutaj nie ma”. Niektóre języki kładą nacisk na ścisłe enkapsulowanie stanu w obiektach, inne czynią go opcjonalnym (C ++ 'prywatny, chroniony, publiczny), inne nie mają wcale enkapsulacji. Jeśli spojrzysz na język taki jak Scheme pod odpowiednim kątem, zobaczysz, że OOP jest wbudowane bez specjalnego wysiłku (może zdefiniować funkcje zwracające funkcje, które zawierają pewien stan lokalny).


0

Mówiąc w skrócie Programowanie obiektowe rozwiązuje problemy bezpieczeństwa danych występujące w programowaniu proceduralnym. Odbywa się to za pomocą koncepcji enkapsulacji danych, umożliwiając dziedziczenie danych tylko przez uzasadnione klasy. Modyfikatory dostępu ułatwiają osiągnięcie tego celu. Mam nadzieję, że to pomoże :)


Jakie problemy bezpieczeństwa danych występują w programowaniu proceduralnym?
user31782

W programowaniu proceduralnym nie można ograniczyć użycia zmiennej globalnej. Każda funkcja może użyć jej wartości. Jednak w OOP mogłem ograniczyć użycie zmiennej tylko do pewnej klasy lub tylko do klas, które ją dziedziczą.
Manu

Również w programowaniu proceduralnym możemy ograniczyć użycie zmiennej globalnej, używając zmiennej do niektórych funkcji, czyli nie deklarując globalnych danych.
user31782

Jeśli nie zadeklarujesz tego globalnie, nie będzie to zmienna globalna.
Manu

1
„Bezpieczne” lub „prawidłowe” nic nie znaczą bez specyfikacji. Są to próby nadania kodowi specyfikacji do osiągnięcia tego celu: typy, definicje klas, DesignByContract itp. Otrzymujesz „Bezpieczeństwo” w tym sensie, że możesz uczynić granice danych prywatnych nietykalnymi; zakładając, że musisz wykonać zestaw instrukcji maszyny wirtualnej, aby wykonać. Orientacja obiektowa nie ukryje pamięci wewnętrznej przed kimś, kto może bezpośrednio odczytywać pamięć, a zły protokół protokołu obiektu ujawnia sekrety według projektu.
Rob
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.