Czy istnieje wzorzec projektowy, który eliminuje potrzebę sprawdzania flag?


28

Mam zamiar zapisać trochę ładunku ciągu w bazie danych. Mam dwie konfiguracje globalne:

  • szyfrowanie
  • kompresja

Można je włączyć lub wyłączyć za pomocą konfiguracji w taki sposób, że tylko jedna z nich jest włączona, obie są włączone lub obie są wyłączone.

Moja obecna implementacja to:

if (encryptionEnable && !compressEnable) {
    encrypt(data);
} else if (!encryptionEnable && compressEnable) {
    compress(data);
} else if (encryptionEnable && compressEnable) {
    encrypt(compress(data));
} else {
  data;
}

Myślę o wzorze dekoratora. Czy to właściwy wybór, czy może jest lepsza alternatywa?


5
Co jest nie tak z tym, co obecnie masz? Czy wymagania dotyczące tej funkcji mogą ulec zmianie? IE, czy będą prawdopodobnie nowe ifwypowiedzi?
Darren Young,

Nie, szukam innego rozwiązania w celu ulepszenia kodu.
Damith Ganegoda

46
Poruszasz się do tyłu. Nie znajdziesz wzorca, a następnie napisz kod, który będzie pasował do wzorca. Pisz kod, aby dopasować go do swoich wymagań, a następnie opcjonalnie użyj wzorca do opisania kodu.
Lekkość ściga się z Monicą

1
zwróć uwagę, że jeśli uważasz, że twoje pytanie jest w istocie duplikatem tego pytania , to jako pytający masz możliwość „zastąpienia” ostatniego ponownego otwarcia i samodzielnego zamknięcia go jako takiego. Zrobiłem to z niektórymi własnymi pytaniami i działa to jak urok. Oto jak to zrobiłem, 3 proste kroki - jedyną różnicą w moich „instrukcjach” jest to, że ponieważ masz mniej niż 3K powtórzeń, będziesz musiał przejść przez okno dialogowe flagi, aby przejść do opcji „duplikuj”
gnat

8
@LightnessRacesinOrbit: W tym, co mówisz, jest trochę prawdy, ale rozsądnie jest zapytać, czy istnieje lepszy sposób na ustrukturyzowanie własnego kodu i przywołanie wzorca projektowego opisującego proponowaną lepszą strukturę. (Mimo to zgadzam się, że to trochę problem z XY, kiedy pytasz o wzorzec projektowy , kiedy chcesz, który jest projektem , który może, ale nie musi , ściśle odpowiadać dobrze znanemu wzorowi.) Ponadto uzasadnione jest, aby „wzorce” nieznacznie wpływać na kod, ponieważ jeśli używasz dobrze znanego wzorca, często sensowne jest odpowiednie nazwanie komponentów.
ruakh

Odpowiedzi:


15

Projektując kod, zawsze masz dwie opcje.

  1. po prostu to załatw, w takim przypadku praktycznie każde rozwiązanie będzie dla Ciebie działać
  2. bądź pedantyczny i zaprojektuj rozwiązanie, które wykorzystuje dziwactwa języka i jego ideologię (w tym przypadku języki OO - wykorzystanie polimorfizmu jako środka do podjęcia decyzji)

Nie zamierzam skupiać się na pierwszym z nich, ponieważ tak naprawdę nie ma nic do powiedzenia. Jeśli po prostu chcesz go uruchomić, możesz zostawić kod bez zmian.

Ale co by się stało, gdybyś zdecydował się zrobić to pedantycznie i faktycznie rozwiązał problem z wzorami projektowymi, tak jak tego chciałeś?

Możesz patrzeć na następujący proces:

Podczas projektowania kodu OO większość z nich, ifktóre są w kodzie, nie musi tam być. Oczywiście, jeśli chcesz porównać dwa typy skalarne, takie jak ints lub floats, prawdopodobnie będziesz mieć if, ale jeśli chcesz zmienić procedury oparte na konfiguracji, możesz użyć polimorfizmu, aby osiągnąć to, czego chcesz, przenieść decyzje ( ifs) od logiki biznesowej do miejsca, w którym tworzone są obiekty - do fabryk .

W tej chwili proces może przebiegać 4 osobnymi ścieżkami:

  1. datanie jest zaszyfrowany ani skompresowany (nic nie dzwoń, zwracaj data)
  2. datajest skompresowany (zadzwoń compress(data)i zwróć)
  3. datajest zaszyfrowany (zadzwoń encrypt(data)i zwróć)
  4. datajest skompresowany i zaszyfrowany (zadzwoń encrypt(compress(data))i zwróć)

Patrząc na 4 ścieżki, możesz znaleźć problem.

Masz jeden proces, który wywołuje 3 (teoretycznie 4, jeśli nie nazywasz niczego niczym jednym) różnymi metodami, które manipulują danymi, a następnie je zwracają. Metody mają różne nazwy , różne tak zwane publiczne API (sposób, w jaki metody komunikują swoje zachowanie).

Używając wzorca adaptera , możemy rozwiązać występującą kolizję nazw (możemy zjednoczyć publiczny interfejs API). Mówiąc wprost, adapter pomaga współpracować ze sobą dwóch niekompatybilnych interfejsów. Adapter działa również poprzez zdefiniowanie nowego interfejsu adaptera, który to klasy próbują zjednoczyć swoją implementację API.

To nie jest konkretny język. Jest to podejście ogólne, każde słowo kluczowe, które może reprezentować, może być dowolnego typu, w języku takim jak C # można zastąpić je słowem ogólnym ( <T>).

Zakładam, że teraz możesz mieć dwie klasy odpowiedzialne za kompresję i szyfrowanie.

class Compression
{
    Compress(data : any) : any { ... }
}

class Encryption
{
    Encrypt(data : any) : any { ... }
}

W świecie korporacyjnym nawet te określone klasy najprawdopodobniej zostaną zastąpione interfejsami, takimi jak classsłowo kluczowe zostanie zastąpione interface(jeśli masz do czynienia z językami takimi jak C #, Java i / lub PHP) lub classsłowo kluczowe pozostanie, ale Compressa Encryptmetody byłyby zdefiniowane jako czysty wirtualny , jeśli kodujesz w C ++.

Aby utworzyć adapter, definiujemy wspólny interfejs.

interface DataProcessing
{
    Process(data : any) : any;
}

Następnie musimy zapewnić implementacje interfejsu, aby był on użyteczny.

// when neither encryption nor compression is enabled
class DoNothingAdapter : DataProcessing
{
    public Process(data : any) : any
    {
        return data;
    }
}

// when only compression is enabled
class CompressionAdapter : DataProcessing
{
    private compression : Compression;

    public Process(data : any) : any
    {
        return this.compression.Compress(data);
    }
}

// when only encryption is enabled
class EncryptionAdapter : DataProcessing
{
    private encryption : Encryption;

    public Process(data : any) : any
    {
        return this.encryption.Encrypt(data);
    }
}

// when both, compression and encryption are enabled
class CompressionEncryptionAdapter : DataProcessing
{
    private compression : Compression;
    private encryption : Encryption;

    public Process(data : any) : any
    {
        return this.encryption.Encrypt(
            this.compression.Compress(data)
        );
    }
}

W ten sposób powstają 4 klasy, z których każda robi coś zupełnie innego, ale każda z nich zapewnia ten sam publiczny interfejs API. ProcessMetoda.

W logice biznesowej, w której masz do czynienia z decyzją none / encryption / kompression / oba, zaprojektujesz swój obiekt, aby był on zależny od DataProcessinginterfejsu, który projektowaliśmy wcześniej.

class DataService
{
    private dataProcessing : DataProcessing;

    public DataService(dataProcessing : DataProcessing)
    {
        this.dataProcessing = dataProcessing;
    }
}

Sam proces może wtedy być tak prosty:

public ComplicatedProcess(data : any) : any
{
    data = this.dataProcessing.Process(data);

    // ... perhaps work with the data

    return data;
}

Nigdy więcej warunków warunkowych. Klasa DataServicenie ma pojęcia, co tak naprawdę zrobi z danymi, gdy zostaną one przekazane dataProcessingczłonkowi, i tak naprawdę nie dba o to, nie jest to jej odpowiedzialność.

Idealnie byłoby mieć testy jednostkowe testujące 4 klasy adapterów, które utworzyłeś, aby upewnić się, że działają, musisz zdać test. A jeśli przejdą, możesz być pewien, że będą działać bez względu na to, jak je wywołasz w kodzie.

Więc robiąc to w ten sposób, że już nigdy nie będę miał ifw swoim kodzie?

Nie. Mniej prawdopodobne jest, że w twojej logice biznesowej będą warunkowe, ale wciąż muszą gdzieś być. To miejsce to twoje fabryki.

I to jest dobre. Oddzielasz obawy związane z tworzeniem i faktycznym użyciem kodu. Jeśli sprawisz, że twoje fabryki będą niezawodne (w Javie możesz nawet posunąć się nawet do korzystania z czegoś takiego jak Google Guice ), w logice biznesowej nie martwisz się, że wybierzesz odpowiednią klasę do wstrzyknięcia. Ponieważ wiesz, że twoje fabryki działają i dostarczy to, o co poprosisz.

Czy konieczne jest posiadanie wszystkich tych klas, interfejsów itp.?

To przywraca nas do początku.

W OOP, jeśli wybierzesz ścieżkę do zastosowania polimorfizmu, naprawdę chcesz użyć wzorców projektowych, chcesz wykorzystać cechy języka i / lub chcesz podążać za wszystkim, jest ideologią obiektową, to jest. I nawet wtedy, przykład ten nawet nie pokazuje wszystkich fabryk masz zamiar trzeba, a jeśli były byłaby się Compressioni Encryptionklas i ich interfejsy zamiast, musisz włączyć ich wdrożenia, jak również.

W końcu dostajesz setki małych klas i interfejsów, skoncentrowanych na bardzo specyficznych rzeczach. Co niekoniecznie jest złe, ale może nie być najlepszym rozwiązaniem dla Ciebie, jeśli chcesz zrobić coś tak prostego jak dodanie dwóch liczb.

Jeśli chcesz to zrobić szybko i szybko, możesz pobrać rozwiązanie Ixreca , któremu przynajmniej udało się wyeliminować bloki else ifi else, które moim zdaniem są nawet odrobinę gorsze niż zwykły if.

Weź pod uwagę, że to mój sposób na dobry projekt OO. Zamiast kodowania interfejsów, a nie implementacji, robiłem to w ciągu ostatnich kilku lat i jest to podejście, które najbardziej mi odpowiada.

Osobiście bardziej podoba mi się programowanie if-less i znacznie bardziej doceniłbym dłuższe rozwiązanie w 5 liniach kodu. W taki sposób przyzwyczaiłem się do projektowania kodu i bardzo wygodnie go czytam.


Aktualizacja 2: Odbyła się szalona dyskusja na temat pierwszej wersji mojego rozwiązania. Dyskusja głównie spowodowana przeze mnie, za co przepraszam.

Postanowiłem edytować odpowiedź w taki sposób, że jest to jeden ze sposobów spojrzenia na rozwiązanie, ale nie jedyny. Usunąłem również część dekoratora, w której zamiast tego miałem na myśli fasadę, którą ostatecznie postanowiłem całkowicie pominąć, ponieważ adapter jest odmianą fasady.


28
Nie przegłosowałem, ale uzasadnieniem może być absurdalna ilość nowych klas / interfejsów do zrobienia czegoś, co oryginalny kod zrobił w 8 wierszach (a druga odpowiedź w 5). Moim zdaniem jedyne, co osiąga to zwiększenie krzywej uczenia się kodu.
Maurycy

6
@Maurycy Pytanie, jakie OP poprosiło, to próba znalezienia rozwiązania swojego problemu przy użyciu typowych wzorców projektowych, jeśli takie rozwiązanie istnieje. Czy moje rozwiązanie jest dłuższe niż jego kod lub Ixrec? To jest. Przyznaję to. Czy moje rozwiązanie rozwiązuje jego problem za pomocą wzorców projektowych, a tym samym odpowiada na jego pytanie, a także skutecznie usuwa wszystkie niezbędne ifs z procesu? To robi. Ixrec's nie.
Andy

26
Wierzę, że pisanie kodu, który jest przejrzysty, niezawodny, zwięzły, wydajny i łatwy w utrzymaniu, jest właściwą drogą. Gdybym miał dolara za każdym razem, gdy ktoś cytował SOLID lub cytował wzorzec oprogramowania bez wyraźnego wyrażenia swoich celów i uzasadnienia, byłbym bogatym człowiekiem.
Robert Harvey

12
Myślę, że mam tutaj dwa problemy. Po pierwsze, interfejsy Compressioni Encryptionwydają się całkowicie zbędne. Nie jestem pewien, czy sugerujesz, że są one w jakiś sposób niezbędne do procesu dekoracji, czy po prostu sugerujesz, że reprezentują wyodrębnione koncepcje. Drugi problem polega na tym, że tworzenie klasy takiej CompressionEncryptionDecoratorprowadzi do tego samego rodzaju kombinatorycznej eksplozji, co warunki warunkowe PO. Nie widzę też wystarczająco jasno wzoru dekoratora w sugerowanym kodzie.
cbojar

5
W debacie na temat SOLID a prosta nie ma sensu: ten kod nie jest ani taki, ani nie wykorzystuje wzorca dekoratora. Kod nie jest automatycznie SOLID tylko dlatego, że korzysta z wielu interfejsów. Wstrzykiwanie zależności interfejsu DataProcessing jest całkiem miłe; wszystko inne jest zbędne. SOLID jest zagadnieniem na poziomie architektury, mającym na celu dobre radzenie sobie ze zmianami. OP nie podał żadnych informacji o swojej architekturze ani o tym, jak oczekuje zmiany kodu, więc nie możemy nawet omówić SOLID w odpowiedzi.
Carl Leth,

120

Jedyny problem, jaki widzę w twoim obecnym kodzie, to ryzyko eksplozji kombinatorycznej, gdy dodajesz więcej ustawień, które można łatwo złagodzić, konstruując kod w ten sposób:

if(compressEnable){
  data = compress(data);
}
if(encryptionEnable) {
  data = encrypt(data);
}
return data;

Nie znam żadnego „wzorca projektowego” ani „idiomu”, którego można by uznać za przykład.


18
@DamithGanegoda Nie, jeśli dokładnie przeczytasz mój kod, zobaczysz, że robi dokładnie to samo w tym przypadku. Dlatego nie ma elsemiędzy moimi dwoma instrukcjami if i dlaczego przypisuję do datanich za każdym razem. Jeśli obie flagi są prawdziwe, wówczas kompresor () zostanie wykonany, a następnie szyfrowany () zostanie wykonany na wyniku kompresji (), tak jak chcesz.
Ixrec

14
@DavidPacker Technicznie, podobnie jak każda instrukcja if w każdym języku programowania. Postawiłem na prostotę, ponieważ wyglądało to na problem, w którym odpowiednia była bardzo prosta odpowiedź. Twoje rozwiązanie jest również ważne, ale osobiście bym je zachował, gdy mam dużo więcej niż dwie flagi boolowskie do zmartwienia.
Ixrec

15
@DavidPacker: poprawne nie jest zdefiniowane przez to, jak dobrze kod przestrzega pewnych wytycznych niektórych autorów na temat ideologii programowania. Prawidłowe jest „czy kod robi to, co powinien i został zaimplementowany w rozsądnym czasie”. Jeśli sensowne jest robienie tego w „niewłaściwy sposób”, to niewłaściwy sposób jest właściwy, ponieważ czas to pieniądz.
whatsisname

9
@DavidPacker: Gdybym był na pozycji OP i zadał to pytanie, Lightness Race w komentarzu Orbity jest tym, czego naprawdę potrzebuję. „Znalezienie rozwiązania przy użyciu wzorców projektowych” zaczyna się już od niewłaściwej stopy.
whatsisname

6
@DavidPacker W rzeczywistości, jeśli czytasz pytanie dokładniej, nie nalega na wzorzec. Mówi: „Myślę o wzorze Dekoratora. Czy to właściwy wybór, czy może jest lepsza alternatywa?” . Odniosłeś się do pierwszego zdania w moim cytacie, ale nie do drugiego. Inni ludzie przyjęli podejście, że nie, to nie jest właściwy wybór. Nie możesz twierdzić, że tylko twoje odpowiada na pytanie.
Jon Bentley,

12

Myślę, że twoje pytanie nie szuka praktyczności, w takim przypadku odpowiedź lxrec jest prawidłowa, ale do poznania wzorców projektowych.

Oczywiście wzorzec poleceń jest przesadą w przypadku tak trywialnego problemu jak ten, który proponujesz, ale dla ilustracji:

public interface Command {
    public String transform(String s);
}

public class CompressCommand implements Command {
    @Override
    public String transform(String s) {
        String compressedString=null;
        //Compression code here
        return compressedString;
    }
}

public class EncryptCommand implements Command {
    @Override
    public String transform(String s) {
        String EncrytedString=null;
        // Encryption code goes here
        return null;
    }

}

public class Test {
    public static void main(String[] args) {
        List<Command> commands = new ArrayList<Command>();
        commands.add(new CompressCommand());
        commands.add(new EncryptCommand()); 
        String myString="Test String";
        for (Command c: commands){
            myString = c.transform(myString);
        }
        // now myString can be stored in the database
    }
}

Jak widać, umieszczenie poleceń / transformacji na liście pozwala na ich sekwencyjne wykonywanie. Oczywiście wykona oba, lub tylko jedno z nich zależy od tego, co umieścisz na liście, bez spełnienia warunków.

Oczywiście warunki warunkowe skończą w jakiejś fabryce, która tworzy listę poleceń.

EDYTUJ dla komentarza @ texacre:

Istnieje wiele sposobów uniknięcia warunków if w części kreacyjnej rozwiązania, weźmy na przykład aplikację graficznego interfejsu użytkownika . Możesz mieć pola wyboru dla opcji kompresji i szyfrowania. W on clicprzypadku tych pól wyboru tworzysz odpowiednie polecenie i dodajesz je do listy lub usuwasz z listy, jeśli odznaczasz opcję.


O ile nie możesz podać przykładu „jakiegoś rodzaju fabryki, która tworzy listę poleceń” bez kodu, który zasadniczo wygląda jak odpowiedź Ixreca, to IMO nie odpowiada na pytanie. Zapewnia to lepszy sposób implementacji funkcji kompresji i szyfrowania, ale nie pozwala uniknąć flag.
thexacre

@ thexacre Dodałem przykład.
Tulains Córdova

Więc w odbiorniku zdarzeń pola wyboru masz „jeśli checkbox.ticked, a następnie dodaj polecenie”? Wydaje mi się, że po prostu
tasujesz

@ thexacre Nie, jeden detektor dla każdego pola wyboru. W zdarzeniu kliknięcia odpowiednio commands.add(new EncryptCommand()); lub commands.add(new CompressCommand());odpowiednio.
Tulains Córdova

Co z obsługą odznaczenia pola? W prawie każdym zestawie narzędzi języka / interfejsu użytkownika, który napotkałem, nadal musisz sprawdzić stan pola wyboru w detektorze zdarzeń. Zgadzam się, że jest to lepszy wzór, ale nie wyklucza to potrzeby, jeśli flaga gdzieś coś zrobi.
thexacre

7

Myślę, że „wzorce projektowe” są niepotrzebnie ukierunkowane na „wzorce oo” i całkowicie unikają znacznie prostszych pomysłów. Mówimy tutaj o (prostym) potoku danych.

Spróbowałbym to zrobić clojure. Każdy inny język, w którym funkcje są najwyższej klasy, jest prawdopodobnie również w porządku. Może mógłbym później użyć C #, ale to nie jest tak miłe. Moim sposobem rozwiązania tego są następujące kroki z niektórymi wyjaśnieniami dla osób niebędących clojurianami:

1. Reprezentuj zestaw transformacji.

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

To jest mapa, tzn. Tablica przeglądowa / słownik / cokolwiek, od słów kluczowych po funkcje. Kolejny przykład (słowa kluczowe do ciągów):

(def employees { :A1 "Alice" 
                 :X9 "Bob"})

(employees :A1) ; => "Alice"
(:A1 employees) ; => "Alice"

Więc pisanie (transformations :encrypt)lub (:encrypt transformations)zwróciłoby funkcję szyfrowania. ( (fn [data] ... )to tylko funkcja lambda).

2. Uzyskaj opcje jako sekwencję słów kluczowych:

(defn do-processing [options data] ;function definition
  ...)

(do-processing [:encrypt :compress] data) ;call to function

3. Filtruj wszystkie transformacje za pomocą dostarczonych opcji.

(let [ transformations-to-run (map transformations options)] ... )

Przykład:

(map employees [:A1]) ; => ["Alice"]
(map employees [:A1 :X9]) ; => ["Alice", "Bob"]

4. Połącz funkcje w jedną:

(apply comp transformations-to-run)

Przykład:

(comp f g h) ;=> f(g(h()))
(apply comp [f g h]) ;=> f(g(h()))

5. A następnie razem:

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )})

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress])

Zmiany TYLKO, jeśli chcemy dodać nową funkcję, powiedzmy „debug-print”, są następujące:

(def transformations { :encrypt  (fn [data] ... ) 
                       :compress (fn [data] ... )
                       :debug-print (fn [data] ...) }) ;<--- here to add as option

(defn do-processing [options data]
  (let [transformations-to-run (map transformations options)
        selected-transformations (apply comp transformations-to-run)] 
    (selected-transformations data)))

(do-processing [:encrypt :compress :debug-print]) ;<-- here to use it
(do-processing [:compress :debug-print]) ;or like this
(do-processing [:encrypt]) ;or like this

W jaki sposób zapełnione funkcje zawierają tylko te funkcje, które należy zastosować, bez zasadniczo używania szeregu instrukcji if w taki czy inny sposób?
thexacre

Rząd funcs-to-run-here (map options funcs)wykonuje filtrowanie, wybierając w ten sposób zestaw funkcji do zastosowania. Może powinienem zaktualizować odpowiedź i podać trochę więcej szczegółów.
NiklasJ

5

[Zasadniczo moja odpowiedź jest kontynuacją odpowiedzi @Ixrec powyżej . ]

Ważne pytanie: czy liczba odrębnych kombinacji, które musisz pokryć, wzrośnie? Jesteś lepiej świadomy swojej domeny tematycznej. To jest twój osąd.
Czy liczba wariantów może wzrosnąć? Cóż, nie jest to nie do pomyślenia. Na przykład może być konieczne dostosowanie większej liczby różnych algorytmów szyfrowania.

Jeśli spodziewasz się, że liczba różnych kombinacji będzie rosła, wzór strategii może ci pomóc. Jest zaprojektowany do enkapsulacji algorytmów i zapewnia wymienny interfejs do kodu wywołującego. Nadal będziesz mieć niewielką logikę podczas tworzenia (tworzenia) odpowiedniej strategii dla każdego konkretnego łańcucha.

Skomentowałeś powyżej , że nie spodziewasz się zmiany wymagań. Jeśli nie spodziewasz się, że liczba wariantów wzrośnie (lub jeśli możesz odroczyć to refaktoryzowanie), zachowaj logikę taką, jaka jest. Obecnie masz małą logikę, którą można zarządzać. (Może dodaj notatkę do siebie w komentarzach na temat możliwego refaktoryzacji do wzorca strategii).


1

Jednym ze sposobów zrobienia tego w scali byłoby:

val handleCompression: AnyRef => AnyRef = data => if (compressEnable) compress(data) else data
val handleEncryption: AnyRef => AnyRef = data => if (encryptionEnable) encrypt(data) else data
val handleData = handleCompression andThen handleEncryption
handleData(data)

Używanie wzorca dekoratora do osiągnięcia powyższych celów (oddzielenie logiki przetwarzania i sposobu ich łączenia) byłoby zbyt szczegółowe.

Tam, gdzie byś potrzebował wzorca projektowego do osiągnięcia tych celów projektowych w paradygmacie programowania OO, język funkcjonalny oferuje natywne wsparcie, używając funkcji jako obywateli pierwszej klasy (linia 1 i 2 w kodzie) i składu funkcjonalnego (linia 3)


Dlaczego jest to lepsze (lub gorsze) niż podejście PO? I / lub co sądzisz o pomyśle OP wykorzystującym wzór dekoratora?
Kasper van den Berg

ten fragment kodu jest lepszy i wyraźnie mówi o zamawianiu (kompresja przed szyfrowaniem); unika niechcianych interfejsów
Rag
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.