Czy „jeśli metoda jest ponownie używana bez zmian, umieść ją w klasie bazowej, w przeciwnym razie stwórz interfejs” to dobra zasada?


10

Mój kolega wymyślił ogólną zasadę wyboru między tworzeniem klasy podstawowej a interfejsem.

On mówi:

Wyobraź sobie każdą nową metodę, którą zamierzasz wdrożyć. Rozważ to dla każdego z nich: czy ta metoda zostanie zaimplementowana przez więcej niż jedną klasę w dokładnie tej formie, bez żadnych zmian? Jeśli odpowiedź brzmi „tak”, utwórz klasę podstawową. W każdej innej sytuacji utwórz interfejs.

Na przykład:

Rozważ klasy cati dog, które rozszerzają klasę mammali mają jedną metodę pet(). Następnie dodajemy klasę alligator, która niczego nie rozszerza i ma jedną metodę slither().

Teraz chcemy dodać eat()metodę do wszystkich z nich.

Jeśli implementacja eat()metody będzie dokładnie taka sama dla cat, dogi alligatorpowinniśmy stworzyć klasę podstawową (powiedzmy animal), która implementuje tę metodę.

Jeśli jednak jego implementacja alligatorróżni się w najmniejszym stopniu, powinniśmy stworzyć IEatinterfejs oraz go stworzyć mammali alligatorwdrożyć.

Podkreśla, że ​​ta metoda obejmuje wszystkie przypadki, ale wydaje mi się, że to nadmierne uproszczenie.

Czy warto stosować tę zasadę?


27
alligatorImplementacja eatróżni się oczywiście tym, że akceptuje cati dogjako parametry.
sbichenko,

1
Tam, gdzie naprawdę chcesz abstrakcyjnej klasy bazowej do udostępniania implementacji, ale powinieneś używać interfejsów dla poprawnej rozszerzalności, często możesz zamiast tego użyć cech . To znaczy, jeśli twój język to obsługuje.
amon

2
Kiedy malujesz się w kącie, najlepiej być w tym najbliższym drzwi.
JeffO,

10
Największym błędem, jaki popełniają ludzie, jest przekonanie, że interfejsy to po prostu puste klasy abstrakcyjne. Interfejs jest sposobem dla programisty na powiedzenie: „Nie dbam o to, co mi dajesz, o ile przestrzega tej konwencji”. Ponowne użycie kodu powinno być następnie (najlepiej) skomponowane. Twój współpracownik się myli.
riwalk

2
Mój profesor CS nauczał, że powinny być nadklasy, is aa interfejsy to acts likelub is. Więc psi is assak i acts likezjadacz. To powiedziałoby nam, że ssak powinien być klasą, a zjadacz powinien być interfejsem. To zawsze był bardzo pomocny przewodnik. Sidenote: Przykładem ismoże być The cake is eatablelub The book is writable.
MirroredFate

Odpowiedzi:


13

Nie sądzę, że to dobra zasada. Jeśli obawiasz się ponownego wykorzystania kodu, możesz wdrożyć PetEatingBehaviorrolę polegającą na zdefiniowaniu funkcji żywieniowych dla kotów i psów. Następnie możesz mieć możliwość IEatponownego wykorzystania kodu.

Obecnie widzę coraz mniej powodów do korzystania z dziedziczenia. Dużą zaletą jest łatwość użytkowania. Weź ramy GUI. Popularnym sposobem zaprojektowania interfejsu API w tym celu jest ujawnienie ogromnej klasy bazowej i udokumentowanie metod, które użytkownik może zastąpić. Możemy więc zignorować wszystkie inne skomplikowane sprawy, którymi musi zarządzać nasza aplikacja, ponieważ „domyślna” implementacja jest zapewniana przez klasę podstawową. Ale nadal możemy dostosowywać rzeczy, ponownie wdrażając poprawną metodę, gdy jest to konieczne.

Jeśli trzymasz się interfejsu API, użytkownik zwykle ma więcej pracy, aby proste przykłady działały. Ale API z interfejsami są często mniej sprzężone i łatwiejsze w utrzymaniu z prostego powodu, który IEatzawiera mniej informacji niż Mammalklasa podstawowa. Tak więc konsumenci będą polegać na słabszej umowie, otrzymasz więcej możliwości refaktoryzacji itp.

Cytując Rich Hickey: Prosty! = Łatwy


Czy możesz podać podstawowy przykład PetEatingBehaviorwdrożenia?
sbichenko,

1
@exizt Po pierwsze, jestem zły w wyborze nazwisk. Więc PetEatingBehaviorto chyba źle jeden. Nie mogę dać ci implementacji, ponieważ jest to tylko przykład zabawki. Ale mogę opisać etapy refaktoryzacji: ma konstruktor, który bierze wszystkie informacje używane przez koty / psy, aby zdefiniować eatmetodę (wystąpienie zębów, wystąpienie żołądka itp.). Zawiera tylko kod wspólny dla kotów i psów używanych w ich eatmetodzie. Musisz po prostu utworzyć PetEatingBehaviorczłonka i przekazać go wywołaniom na dog.eat/cat.eat.
Simon Bergot,

Nie mogę uwierzyć, że jest to jedyna odpowiedź wielu osób na odwrócenie PO od dziedziczenia :( Dziedziczenie nie służy do ponownego użycia kodu!
Danny Tuppeny,

1
@DannyTuppeny Dlaczego nie?
sbichenko

4
@exizt Dziedziczenie po klasie to wielka sprawa; bierzesz (prawdopodobnie-trwałą) zależność od czegoś, co w wielu językach możesz mieć tylko jeden. Ponowne użycie kodu jest dość trywialną rzeczą do wykorzystania tej często jedynej klasy bazowej. Co jeśli skończysz z metodami, w których chcesz dzielić się nimi z inną klasą, ale ma ona już klasę podstawową z powodu ponownego użycia innych metod (a nawet, że jest to część „prawdziwej” hierarchii używanej polimorficznie (?)). Użyj dziedziczenia, gdy ClassA jest rodzajem klasy B (np. Samochód dziedziczy po pojeździe), a nie wtedy, gdy ma kilka wewnętrznych szczegółów implementacyjnych :-)
Danny Tuppeny

11

Czy warto stosować tę zasadę?

To przyzwoita zasada, ale znam wiele miejsc, w których ją naruszam.

Aby być uczciwym, używam (abstrakcyjnych) klas podstawowych o wiele bardziej niż moi rówieśnicy. Robię to jako programowanie obronne.

Dla mnie (w wielu językach) abstrakcyjna klasa podstawowa dodaje kluczowe ograniczenie, że może istnieć tylko jedna klasa bazowa. Decydując się na użycie klasy podstawowej zamiast interfejsu, mówisz „to jest podstawowa funkcjonalność klasy (i dowolnego spadkobiercy) i niewłaściwe jest łączenie nie chcących z innymi funkcjami”. Interfejsy są odwrotne: „To jakaś cecha obiektu, niekoniecznie jego podstawowa odpowiedzialność”.

Tego rodzaju wybory projektowe tworzą niejawne przewodniki dla implementatorów o tym, w jaki sposób powinni używać twojego kodu, a przez to pisząc jego kod. Ze względu na ich większy wpływ na bazę kodu, przy podejmowaniu decyzji projektowej staram się silniej brać to pod uwagę.


1
Ponownie czytając tę ​​odpowiedź 3 lata później, uważam, że jest to świetna kwestia: decydując się na użycie klasy podstawowej zamiast interfejsu, mówisz „to jest podstawowa funkcjonalność klasy (i dowolnego spadkobiercy) i jest to niewłaściwe mieszanie willy-nilly z innymi funkcjami ”
sbichenko

9

Problem z uproszczeniem twojego przyjaciela polega na tym, że określenie, czy dana metoda kiedykolwiek będzie wymagać zmiany, jest wyjątkowo trudne. Lepiej jest zastosować regułę, która mówi o mentalności stojącej za superklasami i interfejsami.

Zamiast zastanawiać się, co powinieneś zrobić, patrząc na to, co uważasz za funkcjonalność, spójrz na hierarchię, którą próbujesz utworzyć.

Oto jak mój profesor nauczył różnicę:

Nadklasy powinny być, is aa interfejsy to acts likelub is. Więc psi is assak i acts likezjadacz. To powiedziałoby nam, że ssak powinien być klasą, a zjadacz powinien być interfejsem. Przykładem isbyłoby The cake is eatablelub The book is writable(co jak eatablei writableinterfejsy).

Korzystanie z takiej metodologii jest dość proste i łatwe, i powoduje, że kodujesz do struktury i pojęć, a nie do tego, co według ciebie zrobi kod. Ułatwia to konserwację, kod jest bardziej czytelny, a projekt łatwiejszy do utworzenia.

Niezależnie od tego, co kod może faktycznie powiedzieć, jeśli użyjesz takiej metody, możesz wrócić za pięć lat i użyć tej samej metody, aby odtworzyć kroki i łatwo dowiedzieć się, jak zaprojektowano program i jak on działa.

Tylko moje dwa centy.


6

Na prośbę exizt rozszerzam swój komentarz na dłuższą odpowiedź.

Największym błędem, jaki popełniają ludzie, jest przekonanie, że interfejsy to po prostu puste klasy abstrakcyjne. Interfejs jest sposobem dla programisty na powiedzenie: „Nie dbam o to, co mi dajesz, o ile przestrzega tej konwencji”.

Biblioteka .NET stanowi wspaniały przykład. Na przykład, kiedy piszesz funkcję, która akceptuje IEnumerable<T>, mówisz: „Nie obchodzi mnie, jak przechowujesz swoje dane. Chcę tylko wiedzieć, że mogę użyć foreachpętli.

To prowadzi do bardzo elastycznego kodu. Nagle, aby się zintegrować, wystarczy grać zgodnie z zasadami istniejących interfejsów. Jeśli implementacja interfejsu jest trudna lub myląca, być może jest to wskazówka, że ​​próbujesz wepchnąć kwadratowy kołek w okrągły otwór.

Ale potem pojawia się pytanie: „Co z ponownym użyciem kodu? Moi profesorowie CS powiedzieli mi, że dziedziczenie jest rozwiązaniem wszystkich problemów związanych z ponownym użyciem kodu i że dziedziczenie pozwala pisać raz i używać wszędzie, a leczy zapalenie opon mózgowych w procesie ratowania sierot z podnoszących się mórz i nie byłoby już łez i tak dalej, dalej itd. itd. itd. ”

Używanie dziedziczenia tylko dlatego, że podoba Ci się brzmienie słów „ponowne użycie kodu” jest dość złym pomysłem. Przewodnik po stylowaniu kodu od Google dość zwięźle:

Skład jest często bardziej odpowiedni niż dziedziczenie. ... [B] Ponieważ kod implementujący podklasę jest rozproszony między bazą a podklasą, może być trudniej zrozumieć implementację. Podklasa nie może przesłonić funkcji, które nie są wirtualne, więc podklasa nie może zmienić implementacji.

Aby zilustrować, dlaczego dziedziczenie nie zawsze jest odpowiedzią, zamierzam użyć klasy o nazwie MySpecialFileWriter †. Osoba, która ślepo wierzy, że dziedziczenie jest rozwiązaniem wszystkich problemów, argumentowałaby, że powinieneś spróbować odziedziczyć FileStream, aby nie powielić FileStreamkodu. Mądrzy ludzie wiedzą, że to głupie. Powinieneś po prostu mieć FileStreamobiekt w swojej klasie (jako zmienną lokalną lub zmienną składową) i korzystać z jego funkcjonalności.

FileStreamPrzykładem może wydawać wymyślony, ale tak nie jest. Jeśli masz dwie klasy, które obydwie implementują ten sam interfejs w dokładnie ten sam sposób, powinieneś mieć trzecią klasę, która zawiera dowolną powieloną operację. Twoim celem powinno być pisanie klas, które są samodzielnymi blokami wielokrotnego użytku, które można łączyć jak legos.

Nie oznacza to, że należy unikać dziedziczenia za wszelką cenę. Jest wiele punktów do rozważenia, a większość zostanie omówiona w badaniu pytania „Kompozycja a dziedziczenie”. Nasz własny przepełnienie stosu ma kilka dobrych odpowiedzi na ten temat.

Na koniec dnia uczucia twojego współpracownika nie mają głębi ani zrozumienia niezbędnego do podjęcia świadomej decyzji. Zbadaj temat i sam go wymyśl.

† Przy ilustrowaniu dziedziczenia wszyscy używają zwierząt. To jest bezużyteczne. W ciągu 11 lat rozwoju nigdy nie napisałem klasy o nazwie Cat, więc nie będę jej używał jako przykładu.


Tak więc, jeśli dobrze cię rozumiem, wstrzykiwanie zależności zapewnia takie same możliwości ponownego wykorzystania kodu jak dziedziczenie. Ale do czego więc użyłbyś dziedziczenia? Czy dostarczyłbyś przypadek użycia? (Naprawdę doceniam ten z FileStream)
sbichenko

1
@exizt, nie, nie zapewnia takich samych możliwości ponownego wykorzystania kodu. Zapewnia lepsze możliwości ponownego wykorzystania kodu (w wielu przypadkach), ponieważ zapewnia dobrą kontrolę implementacji. Z klasami można bezpośrednio wchodzić w interakcje za pośrednictwem ich obiektów i ich publicznego interfejsu API, zamiast domyślnie zyskać możliwości i całkowicie zmienić połowę funkcjonalności przez przeciążenie istniejących funkcji.
riwalk

4

Przykłady rzadko wskazują na to, szczególnie gdy dotyczą samochodów lub zwierząt. Sposób jedzenia (lub przetwarzania żywności) zwierzęcia nie jest tak naprawdę związany z jego dziedzictwem, nie ma jednego sposobu żywienia, który obowiązywałby wszystkie zwierzęta. Jest to bardziej zależne od jego fizycznych możliwości (że tak powiem, sposobów wprowadzania, przetwarzania i produkcji). Ssaki mogą być na przykład mięsożercami, roślinożercami lub wszystkożercami, podczas gdy ptaki, ssaki i ryby mogą jeść owady. To zachowanie można uchwycić za pomocą określonych wzorów.

Lepiej jest w tym przypadku wyodrębnić zachowanie, takie jak to:

public interface IFoodEater
{
    void Eat(IFood food);
}

public class Animal : IFoodEater
{
    private IFoodProcessor _foodProcessor;
    public Animal(IFoodProcessor foodProcessor)
    {
        _foodProcessor = foodProcessor;
    }

    public void Eat(IFood food)
    {
        _foodProcessor.Process(food);
    }
}

Lub zaimplementuj wzór odwiedzin, w którym FoodProcessorpróbuje się karmić kompatybilne zwierzęta ( ICarnivore : IFoodEater, MeatProcessingVisitor). Myśl o żywych zwierzętach może przeszkadzać w myśleniu o zerwaniu ich przewodu pokarmowego i zastąpieniu go ogólnym, ale naprawdę wyświadczasz im przysługę.

To sprawi, że twoje zwierzę będzie klasą podstawową, a jeszcze lepiej, której już nigdy nie będziesz musiał zmieniać, dodając nowy rodzaj jedzenia i odpowiedni procesor, dzięki czemu możesz skupić się na rzeczach, które sprawiają, że ta konkretna klasa zwierząt jest pracować nad tak wyjątkowymi.

Tak więc, klasy podstawowe mają swoje miejsce, obowiązuje zasada kciuka (często nie zawsze, ponieważ jest to zasada kciuka), ale szukaj dalej zachowania, które możesz izolować.


1
IFoodEater jest trochę zbędny. Czego oczekujesz od jedzenia? Tak, zwierzęta jako przykłady są okropne.
Euforia

1
Co z roślinami mięsożernymi? :) Poważnie, jednym z głównych problemów z nieużywaniem interfejsów w tym przypadku jest to, że ściśle związałeś koncepcję jedzenia ze zwierzętami i dużo więcej pracy jest, aby wprowadzić nowe abstrakcje, dla których podstawowa klasa Zwierząt nie jest odpowiednia.
Julia Hayward

1
Uważam, że „jedzenie” jest tutaj złym pomysłem: idź bardziej abstrakcyjnie. Co powiesz na IConsumerinterfejs. Mięsożercy mogą spożywać mięso, zwierzęta roślinożerne - rośliny, a rośliny - światło słoneczne i wodę.

2

Interfejs to tak naprawdę umowa. Brak funkcji. Wdrożenie należy do implementatora. Nie ma wyboru, ponieważ należy wdrożyć umowę.

Klasy abstrakcyjne dają implementatorom wybór. Można wybrać metodę, którą każdy musi wdrożyć, zapewnić metodę z podstawową funkcjonalnością, którą można zastąpić, lub zapewnić podstawową funkcjonalność, z której mogą korzystać wszyscy spadkobiercy.

Na przykład:

public abstract class Person
    {
        /// <summary>
        /// Inheritors must implement a hello
        /// </summary>
        /// <returns>Hello</returns>
        public abstract string SayHello();
        /// <summary>
        /// Inheritors can use base functionality, or override
        /// </summary>
        /// <returns></returns>
        public virtual string SayGoodBye()
        {
            return "Good Bye";
        }
        /// <summary>
        /// Base Functionality that is inherited 
        /// </summary>
        public void DoSomething()
        {
            //Do Something Here
        }
    }

Powiedziałbym, że twoją praktyczną zasadą mogłaby również zająć się klasa podstawowa. W szczególności, ponieważ wiele ssaków prawdopodobnie „je” tak samo, wówczas użycie klasy bazowej może być lepsze niż interfejs, ponieważ można zaimplementować metodę wirtualnego jedzenia, z której może skorzystać większość spadkobierców, a wówczas wyjątkowe przypadki mogą zostać pominięte.

Teraz, jeśli określenie „jedzenia” różni się znacznie w zależności od zwierzęcia, być może i interfejs byłby lepszy. Czasami jest to szara strefa i zależy od indywidualnej sytuacji.


1

Nie.

Interfejsy dotyczą sposobu implementacji metod. Jeśli opierasz swoją decyzję o tym, kiedy użyć interfejsu, na tym, w jaki sposób metody zostaną zaimplementowane, robisz coś złego.

Dla mnie różnica między interfejsem a klasą podstawową zależy od tego, jakie są wymagania. Interfejs tworzy się, gdy jakiś fragment kodu wymaga klasy z określonym interfejsem API i domyślnym zachowaniem wynikającym z tego interfejsu API. Nie można utworzyć interfejsu bez kodu, który faktycznie go wywoła.

Z drugiej strony, klasy podstawowe są zwykle rozumiane jako sposób na ponowne użycie kodu, więc rzadko zawracasz sobie głowę kodem, który wywoła tę klasę podstawową i bierzesz pod uwagę jedynie hierarchię obiektów, do których należy ta klasa podstawowa.


Zaraz, eat()po co wdrażać ponownie u każdego zwierzęcia? Możemy to mammalwdrożyć, prawda? Rozumiem jednak twoje zdanie na temat wymagań.
sbichenko,

@exizt: Przepraszam, źle odczytałem twoje pytanie.
Euforia

1

To nie jest dobra zasada, ponieważ jeśli chcesz różnych implementacji, zawsze możesz mieć abstrakcyjną klasę bazową z abstrakcyjną metodą. Każdy spadkobierca ma zapewnioną tę metodę, ale każdy określa własną implementację. Technicznie rzecz biorąc, mogłaby istnieć klasa abstrakcyjna bez zestawu metod abstrakcyjnych i byłaby w zasadzie taka sama jak interfejs.

Klasy podstawowe są przydatne, gdy chcesz rzeczywistej implementacji pewnego stanu lub zachowania, które może być użyte w kilku klasach. Jeśli znajdziesz się w kilku klasach, które mają identyczne pola i metody z identycznymi implementacjami, prawdopodobnie możesz to zmienić na klasę podstawową. Musisz po prostu zachować ostrożność podczas dziedziczenia. Każde istniejące pole lub metoda w klasie bazowej musi mieć sens w spadkobiercy, szczególnie gdy jest ona rzutowana jako klasa bazowa.

Interfejsy są przydatne, gdy trzeba wchodzić w interakcje z zestawem klas w ten sam sposób, ale nie można przewidzieć ani nie przejmować się ich faktyczną implementacją. Weźmy na przykład coś w rodzaju interfejsu kolekcji. Pan zna tylko wszystkie niezliczone sposoby, w jakie ktoś może zdecydować się na wdrożenie zbioru przedmiotów. Połączona lista? Lista tablic? Stos? Kolejka? Ta metoda SearchAndReplace nie ma znaczenia, wystarczy przekazać coś, co może wywołać Dodaj, Usuń i GetEnumerator.


1

W pewnym sensie osobiście obserwuję odpowiedzi na to pytanie, aby zobaczyć, co mają do powiedzenia inni ludzie, ale powiem trzy rzeczy, o których jestem skłonny pomyśleć:

  1. Ludzie pokładają zbyt wiele wiary w interfejsy, a zbyt mało wiary w klasy. Interfejsy są w gruncie rzeczy bardzo rozwodnionymi klasami podstawowymi i zdarzają się sytuacje, w których użycie czegoś lekkiego może prowadzić do takich rzeczy, jak nadmierny kod płyty bazowej.

  2. Nie ma nic złego w przesłonięciu metody, wywołaniu super.method()jej i pozwoleniu reszcie jego ciała na robienie rzeczy, które nie kolidują z klasą podstawową. Nie narusza to zasady substytucji Liskowa .

  3. Z reguły bycie tak sztywnym i dogmatycznym w inżynierii oprogramowania jest złym pomysłem, nawet jeśli coś ogólnie jest najlepszą praktyką.

Więc wziąłbym to, co powiedziano, z dodatkiem soli.


5
People put way too much faith into interfaces, and too little faith into classes.Zabawny. Wydaje mi się, że jest odwrotnie.
Simon Bergot,

1
„Nie narusza to zasady substytucji Liskowa”. Ale jest bardzo blisko, aby stać się naruszeniem LSP. Lepiej bądź bezpieczny niż przykro.
Euforia

1

Chcę przyjąć do tego językowe i semantyczne podejście. Interfejs nie istniejeDRY . Chociaż może być używany jako mechanizm centralizacji, a także posiada abstrakcyjną klasę, która go implementuje, nie jest przeznaczony do DRY.

Interfejs to umowa.

IAnimalInterfejs jest umową typu „wszystko albo nic” między aligatorem, kotem, psem i pojęciem zwierzęcia. Mówi w gruncie rzeczy „jeśli chcesz być zwierzęciem, albo musisz mieć wszystkie te cechy i cechy, albo nie jesteś już zwierzęciem”.

Dziedziczenie po drugiej stronie jest mechanizmem ponownego wykorzystania. W tym przypadku uważam, że dobre rozumowanie semantyczne może pomóc ci zdecydować, która metoda powinna być dokąd. Na przykład, podczas gdy wszystkie zwierzęta mogą spać i śpią tak samo, nie użyję do tego klasy podstawowej. Najpierw umieściłem Sleep()metodę w IAnimalinterfejsie jako nacisk (semantyczny) na to, co wymaga bycia zwierzęciem, a następnie używam podstawowej abstrakcyjnej klasy dla kota, psa i aligatora jako techniki programowania, aby się nie powtarzać.


0

Nie jestem pewien, czy jest to najlepsza zasada. Możesz umieścić abstractmetodę w klasie bazowej i pozwolić każdej klasie ją zaimplementować. Ponieważ każdy mammalmusi jeść, miałoby to sens. Następnie każdy mammalmoże użyć swojegoeat metody. Zwykle używam interfejsów tylko do komunikacji między obiektami lub jako znacznik do oznaczenia klasy jako czegoś. Myślę, że nadużywanie interfejsów powoduje niechlujny kod.

Zgadzam się również z @Panzercrisis, że ogólna zasada powinna być ogólną wytyczną. Nie coś, co powinno być napisane w kamieniu. Niewątpliwie będą chwile, w których to po prostu nie zadziała.


-1

Interfejsy są typami. Nie zapewniają wdrożenia. Po prostu definiują umowę na obsługę tego typu. Ta umowa musi być spełniona przez każdą klasę implementującą interfejs.

Zastosowanie interfejsów i klas abstrakcyjnych / podstawowych powinno być określone przez wymagania projektowe.

Pojedyncza klasa nie wymaga interfejsu.

Jeśli dwie funkcje są wymienne w ramach funkcji, powinny implementować wspólny interfejs. Każda funkcjonalność, która jest implementowana identycznie, mogłaby zostać przekształcona w abstrakcyjną klasę implementującą interfejs. Interfejs może być w tym momencie uznany za zbędny, jeśli nie ma funkcji wyróżniającej. Ale jaka jest różnica między tymi dwiema klasami?

Używanie klas abstrakcyjnych jako typów jest na ogół niechlujne, ponieważ dodaje się więcej funkcji i rozwija się hierarchia.

Preferuj kompozycję zamiast dziedziczenia - wszystko to odchodzi.

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.