Pytanie Czy interfejsy są kompatybilne z polimorfizmem?


Mam problem z koncepcją interfejsów oddziałujących z typami polimorficznymi (lub nawet z interfejsami polimorficznymi). Pracuję w języku C # i będę wdzięczny za udzielenie odpowiedzi blisko do tej definicji, chociaż uważam, że wciąż daje wiele miejsca dla wszystkich, aby udzielić odpowiedzi.

Na przykład, powiedzmy, że chcesz zrobić program do malowania rzeczy. Definiujesz interfejs dla aktora, który maluje, i definiujesz interfejs dla malowanego przedmiotu. Ponadto masz kilka tematów, które można pomalować w bardziej konkretny sposób.

interface IPainter {
  void paint(IPaintable paintable);
}
interface IPaintable {
  void ApplyBaseLayer(Color c);
}
interface IDecalPaintable : IPaintable {
  void ApplyDecal(HatchBrush b);
}

Mogę sobie wyobrazić malarza podobnego do następującego:

class AwesomeDecalPainter : IPainter
  {
    public void paint(IPaintable paintable) {
      IDecalPaintable decalPaintable = (IDecalPaintable)paintable;
      decalPaintable.ApplyBaseLayer(Color.Red);
      decalPaintable.ApplyDecal(new HatchBrush(HatchStyle.Plaid, Color.Green));
    }
  }

Oczywiście to się wyrzuci, jeśli nie można malować za pomocą IDecalPaintable. Natychmiast wprowadza sprzężenie między implementacją IPaintera a operacją, którą można przepuszczać. Jednak nie sądzę, że sensowne jest stwierdzenie, że AwesomeDecalPainter nie jest platformą IPainter tylko dlatego, że jej użycie ogranicza się do podzbioru domeny IPainable.

Moje pytanie jest naprawdę czterokrotne:

  • Czy interfejs jest zgodny z polimorfizm w ogóle?
  • Czy to jest dobre projekt, aby zaimplementować to urządzenie może działać na IDecalPaintable?
  • A co, jeśli może działać wyłącznie na IDecalPaintable?
  • Czy istnieje jakaś literatura lub kod źródłowy? ilustruje, jak interfejsy i typy polimorficzne powinien wchodzić w interakcje

21
2018-05-27 03:49


pochodzenie


Może dodać tag C #? - Tony Delroy
Z powodów, dla których IDecalPainter.paint to zły projekt, patrz "Co to jest zasada substytucyjna Liskov?". To, co powinieneś mieć, to przeciążenie paint to wymaga IDecalPaintable po to aby AwesomeDecalPainter może zaakceptować oba IPaintables i IDecalPaintables. - outis
... An IPainter powinien być w stanie paint na każdym IPaintable. To ma doskonały sens, aby powiedzieć, że AwesomeDecalPainter nie jest IPainter ponieważ nie może działać na wszystkich IPaintables. Zobacz problem z elipsą okrągłą, znany również jako problem kwadratowo-prostokątny - outis
Możesz dodać wszystko, co chcesz, o ile wspierasz IPaintable. To, co mówi interfejs, oznacza, że ​​każdy, kto jest platformą IPainter, może również działać na dowolnym podłożu, które jest IPsite do ApplyBaseLayer. To oznacza, że ​​mogę kazać komuś z dzieciaka z getta pokazać się uczniowi Rembrandta, o ile są oni własnością intelektualną, i mogę dać im wszystko od płótna po ceglaną ścianę, o ile jest to łatwe do nadania, a oni można zastosowaćBaseColor. Kropka. Co mogą zrobić, to cokolwiek innego mogą zrobić. Nie jest zdefiniowany przez IPainter i IPaintable, a jako klient nie wiem i nie dbam. - Sisyphus
@SmokingRope: interfejsy nie są polimorficzne, ponieważ nie mają żadnych metod; definiują tylko sygnatury metod. one wsparcie polimorfizm, ponieważ różne klasy implementujące ten sam interfejs mogą mieć własne implementacje metod, ale wszystkie mają te same metody z tymi samymi sygnaturami. Definiowanie a paint metoda z IPaintable argument, który działa tylko IDecalPaintable łamie nie tylko zasadę substytucji Liskov, ale łamie system typu. - outis


Odpowiedzi:


Interfejs klasy jest rozumiany jako narzędzie dla "użytkownika" tej klasy. Interfejs jest publiczną prezentacją dla klasy i powinien ogłaszać, każdemu, kto rozważa jego użycie, jakie metody i stałe są dostępne i dostępne z zewnątrz. Tak więc, jak sama nazwa wskazuje, zawsze znajduje się pomiędzy użytkownikiem a klasą implementującą go.

Z drugiej strony, klasa abstrakcyjna jest narzędziem mającym pomóc "implementorowi" klas, które go rozszerzają. Jest to infrastruktura, która może nakładać ograniczenia i wytyczne dotyczące tego, jak powinny wyglądać konkretne klasy. Z perspektywy klasowej klasy abstrakcyjne są bardziej architektonicznie ważne niż interfejsy. W tym przypadku implementor znajduje się pomiędzy klasą abstrakcyjną a konkretną, budując tę ​​ostatnią na pierwszej.

Aby odpowiedzieć na twoje pytanie, interfejs jest "kontraktem" na kodeks, który należy uszanować. Kiedy jest używany w ten sposób, odnosi się bardziej do dziedziczenia niż polimorfizm.

Klasy abstrakcyjne definiują "typ". A gdy konkretne podklasy używają klas abstrakcyjnych i redefiniują metody, dodają nowe itd. ... w tym miejscu widać polimorfizm.

Wiem, że ten post może cię bardziej mylić, nie miało to dla mnie sensu, dopóki nie nauczyłem się wzorców projektowych. Z kilkoma prostymi wzorami pod paskiem lepiej zrozumiesz rolę każdego obiektu i jak dziedziczenie, polimorfizm i enkapsulacja grają ręka w rękę, aby zbudować czysto zaprojektowane aplikacje.

Powodzenia


16
2018-05-27 04:10



Doskonała odpowiedź! - Petr Abdulin
Zgoda! Wziąłem książkę Head First do wzornictwa i czułem, że to wszystko, co zostało z programu studiów licencjackich. Bez tego to tylko garść narzędzi do twojej skrzynki narzędziowej, ale nie masz jeszcze taśmy klejącej, żeby je wszystkie razem połączyć. - Mohgeroth


Interfejsy są nie tylko kompatybilne z polimorfizmem - są dla niego niezbędne. Częścią idei, którą wydaje się być brakującą, jest to, że jeśli ma się interfejs taki jak IPaintable, oczekuje się, że każdy obiekt, który ją implementuje, zapewni pewną domyślną metodę malowania, zwykle obejmując inne metody, które skonfigurują obiekt w jakiś użyteczny sposób. .

Na przykład interfejs umożliwiający określenie adresu IP może definiować metodę malowania:

void Paint (płótno ICanvas);

Zauważ, że metoda Paint nie mówi nic o tym, co powinno zostać pomalowane, ani o jakim kolorze, ani o czymkolwiek innym. Różne obiekty do malowania ujawniałyby właściwości kontrolujące takie rzeczy. Na przykład metoda Polygon może wykorzystywać właściwości OutlineColor i FillColor, wraz z metodą SetVertices (). Rutyna, która chce namalować wiele obiektów, może zaakceptować IEnumerable (Of IPaintable) i po prostu wywołać Paint na wszystkich z nich, bez konieczności dowiedzenia się, jak będą malowane. Fakt, że obiekt jest malowany przez wywoływanie Paint bez parametrów innych niż obszar roboczy, na którym powinien być pomalowany, w żaden sposób nie uniemożliwia w programie Paint wykonywania różnego rodzaju fantazyjnych wypełnień gradientowych, mapowania tekstur, ray tracing lub innych efektów graficznych. Kod, który nakazuje obraz, nie znałby się na takich rzeczach, ale same obiekty, które można było rozsyłać, zawierałyby wszystkie potrzebne informacje, a zatem nie wymagałyby kodu wywołującego, który by je dostarczył.


7
2018-05-27 15:00



To najlepsza odpowiedź grupy. - Zak
@Zak: Cieszę się, że Ci się podoba. Pomysł, że aplikacja może coś pomalować, nie mając pojęcia, co to właściwie oznacza, to przynajmniej na komputerach Macintosh; Pamiętam, że uruchomiłem starą wersję TeachText (np. Notatnik, ale z możliwością wyświetlania obrazów, które są osadzone w widelcu źródłowym dokumentu za pomocą jakiegoś innego programu), który został napisany przed kolorowymi komputerami Macintosh, na Macintoshu II i jest raczej zaskoczony kiedy otworzyłem dokument, który zawierał kolorowy obrazek i pokazał się w kolorze. Okazuje się, że obsługa obrazów w systemie Macintosh ... - supercat
... nie był tak uniwersalny jak to pierwsze wrażenie, które mnie zastanowiło (stare aplikacje będą ograniczone do ośmiu kolorów), ale myślę, że zasada ma tu zastosowanie. Fakt, że metoda IPaintable udostępnia tylko jedną nudną metodę Paint, w żaden sposób nie ogranicza możliwości metody Paint do robienia rzeczy znacznie wykraczających poza to, czego może oczekiwać aplikacja korzystająca z protokołu IPaintable. - supercat


Problem polega na tym, że zdefiniowałeś niejasny interfejs, umowa jest bardziej odpowiednim terminem. To tak, jakbyś wszedł do McDonalda i po prostu zamów burgera:

interface Resturant
{
    Burger BuyBurger();
}

Osoba przyjmująca twoje zamówienie będzie przez jakiś czas wyglądała na zagubioną, ale w końcu będzie ci podawać każdego hamburgera, ponieważ nie określisz tego, czego chcesz.

To samo tutaj. Czy naprawdę chcesz tylko zdefiniować, że coś można pomalować? Jeśli mnie pytasz, to jakbyś pytał o kłopoty. Zawsze staraj się, aby interfejs był jak najbardziej konkretny. Zawsze lepiej jest mieć kilka małych konkretnych interfejsów niż duży ogólny.

Ale wróćmy do twojego przykładu. W twoim przypadku wszystkie klasy muszą tylko coś namalować. Dlatego dodałbym jeszcze bardziej specyficzny interfejs:

    interface IPainter
    {
        void Paint(IPaintable paintable);
    }
    interface IDecalPainter : IPainter
    {
        void Paint(IDecalPaintable paintable);
    }
    interface IPaintable
    {
        void ApplyBaseLayer(Color c);
    }
    interface IDecalPaintable : IPaintable
    {
        void ApplyDecal(HatchBrush b);
    }

    class AwesomeDecalPainter : IDecalPainter
    {
        public void Paint(IPaintable paintable)
        {
            IDecalPaintable decalPaintable = paintable as IDecalPaintable;
            if (decalPaintable != null)
                Paint(decalPaintable);
            else
                paintable.ApplyBaseLayer(Color.Red);
        }

        public void Paint(IDecalPaintable paintable)
        {
            paintable.ApplyBaseLayer(Color.Red);
            paintable.ApplyDecal(new HatchBrush(HatchStyle.Plaid, Color.Green));
        }
    }

Zaleciłbym przeczytanie o zasadach SOLID.

Aktualizacja

Im więcej implementacji interfejsu dźwiękowego

    interface IPaintingContext
    {
        //Should not be object. But my System.Drawing knowledge is limited
        object DrawingSurface { get; set; }
    }
    interface IPaintable
    {
        void Draw(IPaintingContext context);
    }

    class AwesomeDecal : IPaintable
    {
        public void Draw(IPaintingContext paintable)
        {
            // draw decal
        }
    }

    class ComplexPaintableObject : IPaintable
    {
        public ComplexPaintableObject(IEnumerable<IPaintable> paintable)
        {
            // add background, border 
        }
    }

Tutaj możesz tworzyć tak złożone elementy malarskie, jak to tylko możliwe. Są teraz świadomi tego, co malują, lub jakie inne farby są używane na tej samej powierzchni.


5
2018-05-27 04:55



Sugerujesz, że AwesomeDecalPainter degraduje się do malowania rzeczy w mniej niesamowity sposób? Wydaje mi się, że narusza to jego cel. - hannasm
Nie, wygląda na to, że zachowuje się dokładnie tak, jak definiuje interfejs. Problem nie polega na poniżającym, ale zdefiniowanym interfejsie. - jgauffin
Dodałem alternatywną implementację, która lepiej wykorzystuje interfejsy. Interfejs kontekstowy może mieć zamiast tego kilka ogólnych metod malowania, umożliwiających malowanie na dowolnym elemencie. - jgauffin
W tej drugiej wersji interfejsy, które można przepuszczać, są nadal ograniczone do malowania w dokładnie taki sposób, jaki dopuszcza protokół IPaintingContext. Nie ma sposobu, aby IPaintingContext miał wyprowadzony IAwesomePaintingContext, który byłby obsługiwany przez inny interfejs. Zatem interfejsy są niekompatybilne z polimorfizmem? - hannasm
Nie. Nie prawda. W OOP chodzi o to, że MUSISZ implementować klasy / interfejsy, jak opisują ich umowy. Dlatego jeśli IAwesomePaintingContext dziedziczy IPaintingContext powinno działać zgodnie z opisem w IPaintingContext jeśli obiekt może tylko użyć IPaintingContext ale nie IAwesomePaintingContext. Jeśli klasa nie może pracować IPaintingContext nie powinien tego akceptować, ale tylko IAwesomePaintingContext. Jeśli nie możesz wykonać tej pracy, musisz zmienić swoją architekturę. - jgauffin


Czy poprawnie rozumiem twoje pytanie? Myślę, że twoje pytanie brzmi, czy interfejs może korzystać z podstaw dziedziczenia? (np. interfejs potomny dziedziczący z interfejsu nadrzędnego).

1) jeśli tak, syntaktycznie pewny, dlaczego nie? 2) co do wykonalności, może mieć niewielką wartość, ponieważ twoje interfejsy potomne zasadniczo nie wykorzystują niczego z interfejsu nadrzędnego.


0
2018-05-27 04:01



Moje pytanie jest naprawdę szerokie, czy "umowa" interfejsu może określać zachowanie polimorficzne. Czy specyfikacja interfejsów może wykazywać zachowanie polimorficzne. Jest to oczywiście możliwe, ale czy to dobry pomysł? Myślę, że wiele komentarzy w tym wątku pomogło rzucić nieco światła. - hannasm
#SmokingRobe, mógłbym zamienić twoje pytanie na inne, które opiera się na moim zrozumieniu twojego pytania i udokumentowałem to. gray-suit.blogspot.com/2011/05/...  Jeśli chodzi o odpowiedź, czy jest to dobry pomysł, najpierw chciałbym go zapytać. Dlaczego warto korzystać z interfejsu, a nie tylko z sup / podkategorii? Jeśli już dziedziczysz z jednego miejsca i nie możesz "bezpośrednio" dziedziczyć z innego. Następnie, podobnie jak wyżej podane posty, możesz potrzebować refactor abit. - Gary Tsui
Główną zaletą interfejsu w języku C # jest wielokrotna inerytancja. Bardziej ogólnie, interfejs oznacza, że ​​dwa implementatory mogą być rzutowane na ten sam interfejs bez konieczności implementowania implementacji w formalnej hierarchii dziedziczenia. Niepowiązane typy mogą obsługiwać typowe zachowanie, bez przestrzegania relacji is-a. - hannasm