Czy istnieje metodologia inżynierii oprogramowania dla programowania funkcjonalnego? [Zamknięte]


203

Inżynieria oprogramowania, jak się ją obecnie uczy, koncentruje się całkowicie na programowaniu obiektowym i „naturalnym” obiektowym spojrzeniu na świat. Istnieje szczegółowa metodologia opisująca sposób przekształcenia modelu domeny w model klasy z kilkoma krokami i wieloma artefaktami (UML), takimi jak diagramy przypadków użycia lub diagramy klas. Wielu programistów zinternalizowało to podejście i ma dobry pomysł na temat projektowania aplikacji obiektowych od zera.

Nowym hype jest programowanie funkcjonalne, którego uczy wiele książek i samouczków. A co z funkcjonalną inżynierią oprogramowania? Czytając o Lisp i Clojure, doszedłem do dwóch interesujących stwierdzeń:

  1. Programy funkcjonalne są często opracowywane oddolnie zamiast odgórnie („On Lisp”, Paul Graham)

  2. Funkcjonalni programiści używają map, w których OO-programiści używają obiektów / klas („Clojure for Java Programmers”, wykład Rich Hickley).

Jaka jest zatem metodologia systematycznego (opartego na modelu?) Projektowania aplikacji funkcjonalnej, tj. W Lisp lub Clojure? Jakie są typowe kroki, jakich artefaktów używam, jak zamapować je z obszaru problemu na obszar rozwiązania?


3
Mam tutaj komentarz: wiele programów jest napisanych z góry na dół, praktyczna prezentacja procesu tworzenia oprogramowania w języku funkcjonalnym znajduje się w książce „Functional Programming in Concurrent Clean” (sam język jest bardzo akademicki, chociaż).
Artyom Shalkhakov

4
1. Parnas twierdzi, że większość programów powinna być oddolna, a następnie sfałszowana, aby wyglądać jak odgórna, więc te podejścia powinny być mieszane, nie ma właściwej odpowiedzi.
Gabriel Ščerbák

2
2. Obiekty zapewniają zachowanie w zależności od ich enkapsulowanego stanu strukturalnego, w FP masz cały stan i strukturę jawną, a zachowanie (funkcje) jest oddzielone od struktury. Więc do modelowania danych używasz map dla obiektów, ale podczas projektowania aplikacji obiektów nie można zastąpić funkcjami - FP jest dużym wyrażeniem generowanym i ocenianym za pomocą potoków, OOP polega na tworzeniu modelu i wysyłaniu komunikatów między obiektami.
Gabriel Ščerbák

1
Zadałem kiedyś podobne pytanie: „w jaki sposób jeden model danych z relacyjnych baz danych w clojure?” stackoverflow.com/questions/3067261/…
Sandeep

4
Hehe, w jednym z wykładów SICP Hal Abelson mówi w połowie żartem, coś w stylu „Istnieje słynna metodologia, a może raczej mitologia, zwana inżynierią oprogramowania [...] tworzącą skomplikowane diagramy i wymagania, a następnie budującą systemy z nimi, ci ludzie nie zaprogramowali wiele ”. Pochodzę ze „Szkoły Java”, w której od wieków uczymy UML, artefaktów i innych rzeczy, a choć odrobina tego jest dobra, zbyt wiele planowania i schematów (gra słów zamierzonych) jest bardziej szkodliwa niż pożyteczna: nigdy nie wiesz, w jaki sposób oprogramowanie będzie dostępne, dopóki nie dostaniesz kodu.
lfborjas,

Odpowiedzi:


165

Dzięki Bogu, że inżynierowie oprogramowania nie odkryli jeszcze programowania funkcjonalnego. Oto kilka podobieństw:

  • Wiele „wzorców projektowych” OO jest wychwytywanych jako funkcje wyższego rzędu. Na przykład wzorzec gościa jest znany w świecie funkcjonalnym jako „pasowanie” (lub jeśli jesteś sprytnym teoretykiem, „katamorfizmem”). W językach funkcjonalnych typami danych są głównie drzewa lub krotki, a każdy typ drzewa ma z sobą naturalny katamorfizm.

    Te funkcje wyższego rzędu często mają pewne prawa programowania, zwane także „wolnymi twierdzeniami”.

  • Programiści funkcjonalni używają diagramów znacznie mniej obciążająco niż programiści OO. Znaczna część tego, co jest wyrażone na diagramach OO, jest zamiast tego wyrażona w typach lub w „podpisach”, które należy traktować jako „typy modułów”. Haskell ma również „klasy typów”, które są trochę jak typ interfejsu.

    Ci funkcjonalni programiści, którzy używają typów, ogólnie myślą, że „gdy już poprawnie dopasujesz typy, kod praktycznie sam się pisze”.

    Nie wszystkie języki funkcjonalne używają wyraźnych typów, ale książka How To Design Programs , doskonała książka do nauki Scheme / Lisp / Clojure, w dużej mierze opiera się na „opisach danych”, które są ściśle powiązane z typami.

Jaka jest zatem metodologia systematycznego (opartego na modelu?) Projektowania aplikacji funkcjonalnej, tj. W Lisp lub Clojure?

Każda metoda projektowania oparta na abstrakcji danych działa dobrze. Zdarza mi się myśleć, że jest to łatwiejsze, gdy język ma wyraźne typy, ale działa nawet bez niego. Dobrą książką na temat metod projektowania abstrakcyjnych typów danych, którą łatwo przystosować do programowania funkcjonalnego, jest Abstrakcja i specyfikacja w rozwoju programu autorstwa Barbary Liskov i Johna Guttaga, pierwsze wydanie. Liskov częściowo zdobył nagrodę Turinga za tę pracę.

Inną metodologią projektowania, która jest unikalna dla Lisp, jest ustalenie, które rozszerzenia językowe byłyby przydatne w dziedzinie problemów, w której pracujesz, a następnie użycie makr higienicznych w celu dodania tych konstrukcji do twojego języka. Dobrym miejscem do zapoznania się z tego rodzaju projektami jest artykuł Matthew Flatt Creating Languages ​​in Racket . Artykuł może znajdować się za zaporą. Możesz także znaleźć bardziej ogólny materiał na temat tego rodzaju projektu, wyszukując termin „język osadzony specyficzny dla domeny”; po konkretne porady i przykłady poza tym, co obejmuje Matthew Flatt, prawdopodobnie zacznę od Grahama On Lisp lub może ANSI Common Lisp .

Jakie są typowe kroki, jakich artefaktów używam?

Typowe kroki:

  1. Zidentyfikuj dane w programie i operacje na nim oraz zdefiniuj abstrakcyjny typ danych reprezentujący te dane.

  2. Zidentyfikuj typowe działania lub wzorce obliczeń i wyrażaj je jako funkcje wyższego rzędu lub makra. Spodziewaj się, że zrobisz ten krok w ramach refaktoryzacji.

  3. Jeśli używasz pisanego języka funkcjonalnego, często i często używaj sprawdzania typów. Jeśli korzystasz z Lisp lub Clojure, najlepszą praktyką jest najpierw napisanie kontraktów funkcyjnych, w tym testów jednostkowych - jest to rozwój oparty na testach do maksimum. I będziesz chciał użyć dowolnej wersji QuickCheck, która została przeniesiona na twoją platformę, która w twoim przypadku wygląda na to, że nazywa się ClojureCheck . Jest to niezwykle potężna biblioteka do konstruowania losowych testów kodu korzystającego z funkcji wyższego rzędu.


2
Odwiedzający IMO nie składa się - fold jest podzbiorem odwiedzającego. Wysyłka wielokrotna nie jest (bezpośrednio) rejestrowana przez złożenie.
Michael Ekstrand,

6
@Michael - w rzeczywistości można bardzo starannie uchwycić wiele wysyłek za pomocą różnego rodzaju katamorfizmów wyższego rzędu. Praca Jeremy'ego Gibbonsa jest jednym z miejsc, w których można tego szukać, ale ogólnie polecam pracę nad programowaniem generycznym dla typów danych - szczególnie lubię prace kompozytorskie.
sclv

6
Zgadzam się, że widzę diagramy używane rzadziej do opisywania funkcjonalnych projektów i myślę, że to wstyd. Przy użyciu dużej ilości HOF trudno jest przedstawić odpowiednik schematu sekwencji. Ale chciałbym, żeby przestrzeń, w której można opisać funkcjonalne projekty za pomocą zdjęć, była lepiej zbadana. Chociaż nienawidzę UML (jako specyfikacji), uważam, że UML (jako szkic) jest całkiem przydatny w Javie i chciałbym, aby były najlepsze praktyki, jak zrobić równoważny. Trochę eksperymentowałem z robieniem tego z protokołami i rejestrami Clojure, ale nie mam nic, co naprawdę mi się podoba.
Alex Miller,

22
+1 za „Dzięki Bogu, że inżynierowie oprogramowania nie odkryli jeszcze programowania funkcjonalnego”. ;)
Aky

1
OO sam w sobie jest sposobem na programowanie z typami, więc podejścia nie są tak różne. Problem z projektami OO zwykle wydaje się wynikać z tego, że ludzie nie wiedzą, co robią.
Marcin

46

W przypadku Clojure zalecam powrót do starego, dobrego modelowania relacyjnego. Out of Tarpit to inspirująca lektura.


To świetny artykuł, stare dobre czasy w informatyce musiały być naprawdę imponująco dobre, kiedy wszystkie te koncepcje przetrwały do ​​dzisiejszego renesansu. Prawdopodobnie wynika to z silnych podstaw matematyki.
Thorsten

1
To. TO. TO! Czytam ten artykuł i to naprawdę interesujące, jak wydaje się obejmować wszystkie podstawy tego, co jest potrzebne do budowy prawdziwych systemów, przy jednoczesnym zachowaniu minimalnego stanu zmienności w wysoce kontrolowany sposób. Zabawiam się budowaniem Ponga i Tetrisa w stylu FRelP (przepraszam za dziwny inicjalizm, ale jest już inny popularny FRP: programowanie reaktywne).
John Cromartie,

Po przeczytaniu artykułu myślę, że clojure byłby idealnym językiem dla FR (el) P, przynajmniej dla podstawowej logiki , stanu przypadkowego i kontroli oraz innych elementów. Zastanawiam się, jak stworzyć relacyjną definicję stanu podstawowego w clojure bez ponownego wynalezienia sql (bez jego wad)? A może pomysł polega na użyciu dobrego relacyjnego DB i zbudowaniu na nim programu funkcjonalnego bez niedopasowania koncepcyjnego wprowadzonego przez OOP?
Thorsten

1
@Thorsten podstawową ideą jest set = table, map = index. Trudność polega na synchronizowaniu indeksów i tabel, ale ten problem można rozwiązać za pomocą lepszych typów zestawów. Jednym prostym zestawem typu I, który zaimplementowałem, jest zestaw kluczowy, który jest zestawem, który używa funkcji klucza do testowania niepowtarzalności. Oznacza to, że połączenie wartości wstawienia lub aktualizacji, wywołanie get z polami klucza podstawowego zwraca cały wiersz.
cgrand


38

Osobiście uważam, że wszystkie zwykłe dobre praktyki opracowywania OO mają zastosowanie także w programowaniu funkcjonalnym - z kilkoma drobnymi poprawkami, aby uwzględnić funkcjonalny światopogląd. Z punktu widzenia metodologii tak naprawdę nie trzeba robić nic zasadniczo innego.

Moje doświadczenie pochodzi z przeniesienia się z Javy do Clojure w ostatnich latach.

Kilka przykładów:

  • Poznaj swoją domenę biznesową / model danych - równie ważne, czy zamierzasz zaprojektować model obiektowy, czy stworzyć funkcjonalną strukturę danych z zagnieżdżonymi mapami. Pod pewnymi względami FP może być łatwiejszy, ponieważ zachęca do myślenia o modelu danych osobno od funkcji / procesów, ale nadal musisz robić oba.

  • Orientacja na usługi w projektowaniu - faktycznie działa bardzo dobrze z perspektywy FP, ponieważ typowa usługa jest tak naprawdę tylko funkcją z pewnymi efektami ubocznymi. Myślę, że „oddolne” spojrzenie na rozwój oprogramowania, które czasami jest propagowane w świecie Lisp, w rzeczywistości jest po prostu dobrymi zorientowanymi na usługi zasadami projektowania API pod inną postacią.

  • Test Driven Development - działa dobrze w językach FP, a czasem nawet lepiej, ponieważ czyste funkcje nadają się bardzo dobrze do pisania jasnych, powtarzalnych testów bez potrzeby konfigurowania środowiska z pełnym stanem. Możesz także zbudować osobne testy w celu sprawdzenia integralności danych (np. Czy ta mapa zawiera wszystkie klucze, których oczekuję, aby zrównoważyć fakt, że w języku OO definicja klasy wymusiłaby to dla ciebie w czasie kompilacji).

  • Prototying / iteration - działa równie dobrze z FP. Możesz nawet być w stanie prototypować na żywo z użytkownikami, jeśli bardzo dobrze radzisz sobie z budowaniem narzędzi / DSL i używaniem ich w REPL.


3
Praktyki te wydają mi się dość znajome. Nadal uważam, że ktoś powinien napisać funkcjonalny odpowiednik „Inżynierii oprogramowania obiektowego przy użyciu UML, wzorców i Java” Bruegge / Dutoit zamiast szóstej książki „Programowanie w Clojure”. Można go nazwać „inżynierią oprogramowania funkcjonalnego za pomocą Clojure i? What ??”. Czy używają UML i wzorców w FP? Pamiętam, że Paul Graham napisał, że wzorce są oznaką braku abstrakcji w Lisp, czemu należy zaradzić poprzez wprowadzenie nowych makr.
Thorsten

3
Ale jeśli tłumaczysz wzorce jako najlepsze praktyki, mogą istnieć wzorce w świecie FP, które warto udostępnić niezainicjowanym.
Thorsten

2
W książce PAIP jest kilka interesujących zasad. norvig.com/paip.html
mathk

1
istnieją również funkcjonalne wzorce programowania (schematy rekurencji itp.)
Gabriel Ščerbák

13

Programowanie OO ściśle łączy dane z zachowaniem. Programowanie funkcjonalne rozdziela je. Nie masz więc diagramów klas, ale masz struktury danych, a szczególnie algebraiczne typy danych. Te typy mogą być napisane tak, aby bardzo ściśle pasowały do ​​Twojej domeny, w tym eliminując niemożliwe wartości przez konstrukcję.

Nie ma więc o tym książek i książek, ale istnieje ugruntowane podejście, które, jak mówi przysłowie, sprawia, że ​​niemożliwe jest reprezentowanie niemożliwych wartości.

W ten sposób możesz dokonać szeregu wyborów dotyczących reprezentowania określonych typów danych jako funkcji, a odwrotnie, reprezentowania niektórych funkcji jako połączenia typów danych, aby uzyskać np. Serializację, ściślejszą specyfikację, optymalizację itp. .

Następnie, biorąc pod uwagę to, piszesz funkcje na swoich reklamach, tak że ustanawiasz jakąś algebrę - tj. Istnieją stałe prawa, które obowiązują dla tych funkcji. Niektóre mogą być idempotentne - to samo po wielu aplikacjach. Niektóre są skojarzone. Niektóre są przechodnie itp.

Teraz masz domenę, nad którą masz funkcje, które komponują zgodnie z dobrze zachowanymi przepisami. Prosta wbudowana DSL!

Aha, a biorąc pod uwagę właściwości, możesz oczywiście napisać ich automatyczne losowe testy (ala QuickCheck) .. i to dopiero początek.


1
Podejście polegające na tym, że niemożliwe jest reprezentowanie niemożliwych wartości, w mniejszym stopniu dotyczy języków z dynamicznym pisaniem, takich jak Clojure i Scheme, niż języków z pisaniem statycznym, takich jak Haskell i ML.
Zak

@Zak - cóż, nie można statycznie sprawdzić, czy są niereprezentatywne, ale i tak można budować struktury danych w ten sam sposób.
sclv

7

Projektowanie obiektowe to nie to samo, co inżynieria oprogramowania. Inżynieria oprogramowania ma związek z całym procesem przejścia od wymagań do działającego systemu, na czas i przy niskim wskaźniku defektów. Programowanie funkcjonalne może różnić się od OO, ale nie eliminuje wymagań, wysokiego poziomu i szczegółowych projektów, weryfikacji i testowania, metryk oprogramowania, szacunków i wszystkich innych „inżynierii oprogramowania”.

Ponadto programy funkcjonalne wykazują modułowość i inną strukturę. Twoje szczegółowe projekty muszą być wyrażone w kategoriach pojęć w tej strukturze.


5

Jednym z podejść jest stworzenie wewnętrznej DSL w wybranym funkcjonalnym języku programowania. „Model” jest wówczas zbiorem reguł biznesowych wyrażonych w DSL.


1
Rozumiem podejście, by najpierw zbudować język w kierunku dziedziny problemów, dopóki nie zostanie osiągnięty poziom abstrakcji, że w kodzie nie występują już powtarzające się wzorce, niż rozwiązać problem z tymi abstrakcjami.
Thorsten

1
Ale jak to wygląda, gdy „model jest zbiorem reguł biznesowych wyrażonych w DSL”? W aplikacji Java EE model jest zapisywany jako jednostki POJO, które są wywoływane z kontrolerów EJB, które z kolei aktualizują JSP widoku - na przykład. Czy w FP są podobne wzory architektoniczne (takie jak wzór MVC)? Jak to wygląda?
Thorsten

2
Nie ma powodu, dla którego nie możesz mieć wzoru MVC w FP, dokładnie tak. FP nadal pozwala budować bogate struktury danych, a zapewne dzięki narzędziom ADT i dopasowywaniu wzorców, możesz budować znacznie bogatsze . Co więcej, ponieważ FP oddziela dane i zachowanie, systemy typu MVC powstają znacznie bardziej naturalnie.
sclv

5

Zobacz moją odpowiedź na inny post:

Jak Clojure podchodzi do rozdziału obaw?

Zgadzam się, że należy napisać więcej na ten temat, jak tworzyć struktury dużych aplikacji, które wykorzystują podejście FP (plus więcej należy zrobić, aby udokumentować interfejsy oparte na FP)


3
Podoba mi się podejście oparte na 90% potoku i 10% makro. Wydaje się całkiem naturalne, że program funkcjonalny jest ciągiem transformacji niezmiennych danych. Nie jestem pewien, czy rozumiem, co rozumiesz przez „włożenie całej inteligencji w dane, a nie w kod”, ponieważ podejście polegające na posiadaniu 100 funkcji pracujących na 1 strukturze danych (zamiast 10 funkcji na 10 strukturach danych) wydaje się sugerować przeciwieństwo. Czy struktury danych w OOP nie są bardziej inteligentne niż w FP, ponieważ mają wbudowane własne zachowanie?
Thorsten

3

Chociaż można to uznać za naiwne i uproszczone, myślę, że „przepisy projektowe” (systematyczne podejście do rozwiązywania problemów stosowane w programowaniu, jak zalecają Felleisen i in. W swojej książce HtDP ), byłyby zbliżone do tego, czego się wydaje.

Oto kilka linków:

http://www.northeastern.edu/magazine/0301/programming.html

http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.86.8371


Link do strony północno-wschodniej wydaje się być martwy.
James Kingsbery

1
James, masz rację i nie pamiętam, co tam było, aby to naprawić, niestety. Wiem tylko, że autorzy HtDP zaczęli tworzyć język Pyret (i prawdopodobnie przeglądają drugą edycję HtDP, aby używać go zamiast Racket, wcześniej PLT Scheme).
Artem Shalkhakov,

3

Niedawno znalazłem tę książkę: Funkcjonalne i reaktywne modelowanie domen

Myślę, że idealnie odpowiada twojemu pytaniu.

Z opisu książki:

Funkcjonalne i reaktywne modelowanie domen uczy, jak myśleć o modelu domeny w kategoriach czystych funkcji i jak je komponować, aby budować większe abstrakcje. Zaczynasz od podstaw programowania funkcjonalnego i stopniowo przechodzisz do zaawansowanych koncepcji i wzorców, które musisz znać, aby wdrażać złożone modele domen. Książka pokazuje, w jaki sposób zaawansowane wzorce FP, takie jak algebraiczne typy danych, projektowanie oparte na typach liter oraz izolacja efektów ubocznych, mogą sprawić, że Twój model skomponuje się pod kątem czytelności i weryfikowalności.


2

Istnieje styl „obliczania programu” / „projektowania według obliczeń” związany z prof. Richardem Birdem i grupą Algebra of Programming na Uniwersytecie Oksfordzkim (Wielka Brytania), nie sądzę, aby zbyt daleko posunięte było rozważanie tej metodyki.

Osobiście, chociaż lubię pracę wykonaną przez grupę AoP, nie mam dyscypliny, by samodzielnie ćwiczyć projektowanie. To jednak moja wada, a nie kalkulacja programu.


2

Przekonałem się, że Behaviour Driven Development jest naturalnym rozwiązaniem dla szybko rozwijającego się kodu zarówno w Clojure, jak i SBCL. Prawdziwą zaletą korzystania z BDD za pomocą języka funkcjonalnego jest to, że zwykle piszę o wiele dokładniejsze testy jednostek ziarna, niż zwykle przy użyciu języków proceduralnych, ponieważ znacznie lepiej rozkładam problem na mniejsze części funkcjonalności.


jakich narzędzi używasz do robienia BDD w clojure?
murtaza52

Lubię Midje. Jest aktualny i bardzo wyrazisty. Sprawdź to: github.com/marick/Midje
Marc

1

Szczerze mówiąc, jeśli chcesz zaprojektować przepisy na programy funkcjonalne, spójrz na standardowe biblioteki funkcji, takie jak Preludium Haskella. W FP wzorce są zwykle rejestrowane przez same procedury wyższego rzędu (funkcje działające na funkcjach). Jeśli więc widać wzór, często po prostu tworzy się funkcję wyższego rzędu, aby uchwycić ten wzór.

Dobrym przykładem jest fmap. Ta funkcja przyjmuje funkcję jako argument i stosuje ją do wszystkich „elementów” drugiego argumentu. Ponieważ jest on częścią klasy typu Functor, każde wystąpienie Functora (takie jak lista, wykres itp.) Może zostać przekazane jako drugi argument do tej funkcji. Przechwytuje ogólne zachowanie zastosowania funkcji do każdego elementu drugiego argumentu.


-7

Dobrze,

Zasadniczo wiele funkcjonalnych języków programowania jest używanych na uniwersytetach od dawna w przypadku „problemów z małymi zabawkami”.

Stają się one coraz bardziej popularne, ponieważ OOP ma trudności z „programowaniem równoległym” z powodu „stanu”. A czasem funkcjonalny styl jest lepszy w przypadku problemów takich jak Google MapReduce.

Jestem pewien, że kiedy faceci funkioanl uderzą w ścianę [spróbujcie wdrożyć systemy większe niż 1.000.000 linii kodu], niektórzy z nich pojawią się z nowymi metodologiami inżynierii oprogramowania ze słowami :-). Powinni odpowiedzieć na stare pytanie: jak podzielić system na części, abyśmy mogli „ugryźć” każdy z elementów pojedynczo? [praca iteracyjna, incerementalna i ewolucyjna] przy użyciu stylu funkcjonalnego.

Na pewno styl funkcjonalny wpłynie na nasz styl obiektowy. „Wciąż” wiele koncepcji z systemów funkcjonalnych i dostosowanych do naszych języków OOP.

Ale czy programy funkcjonalne będą wykorzystywane w tak dużych systemach? Czy staną się głównym strumieniem? To jest pytanie .

I nikt nie może przyjść z realistyczną metodologią bez wdrożenia tak dużych systemów, które brudzą mu ręce. Najpierw powinieneś zabrudzić sobie ręce, a następnie zaproponować rozwiązanie. Rozwiązania-sugestie bez „prawdziwych bólów i brudu” będą „fantazją”.


Powstało już wystarczająco dużo dużych systemów z funkcjonalnymi językami. Nawet jeśli nie, to wcale nie jest to argument.
Svante

Wymień niektóre z nich? Znam tylko kilka systemów „Erlang”. [średni rozmiar] Ale Haskel? Clojure? Seplenienie?
Hippias Minor

I to [pisanie dużych systemów] jest prawdziwym argumentem. Ponieważ to jest przypadek testowy. Ten przypadek testowy pokazuje, że jeśli ten funkcjonalny styl jest przydatny i czy możemy robić z nim praktyczne rzeczy w prawdziwym świecie.
Hippias Minor

2
Zabawną rzeczą w językach, które nie są anonimowo „OOP”, jest to, że często dają ci one wolność od „metodologii profilologii”, do samodzielnego myślenia i cięcia programu w najbardziej odpowiedni sposób, zamiast ślepo podążać za ustalonym wzorcem i żyć z biurokratyczny bojler. Niestety, nie ma tutaj 10-punktowego 3-tygodniowego kursu.
Svante

1
Widziałem rzeczy, w które nie uwierzyłbyś.
Svante
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.