Czy interfejs jest uważany za „pusty”, jeśli dziedziczy z innych interfejsów?

12

O ile wiem, puste interfejsy są ogólnie uważane za złą praktykę - szczególnie tam, gdzie język obsługuje takie atrybuty, jak atrybuty.

Czy jednak interfejs jest uważany za „pusty”, jeśli dziedziczy po innych interfejsach?

interface I1 { ... }
interface I2 { ... } //unrelated to I1

interface I3
    : I1, I2
{
    // empty body
}

Wszystko, narzędzia I3będą potrzebne do realizacji I1i I2, a obiekty z różnych klas, które dziedziczą I3mogą następnie być stosowane zamiennie (patrz poniżej), więc jest to prawo zadzwonić I3 pusty ? Jeśli tak, jaki byłby lepszy sposób na architekturę?

// with I3 interface
class A : I3 { ... }
class B : I3 { ... }

class Test {
    void foo() {
        I3 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function();
    }
}

// without I3 interface
class A : I1, I2 { ... }
class B : I1, I2 { ... }

class Test {
    void foo() {
        I1 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function(); // we can't do this without casting first
    }
}
Kai
źródło
1
Brak możliwości określenia kilku interfejsów jako typu parametru / pola itp. Jest wadą projektową w języku C # / .NET.
CodesInChaos
Myślę, że lepszym sposobem na architekturę ta część powinna przejść do osobnego pytania. W tej chwili zaakceptowana odpowiedź nie ma nic wspólnego z tytułem pytania.
Kapol,
@CodesInChaos Nie, nie jest, ponieważ istnieją już dwa sposoby, aby to zrobić. Jedno jest w tym pytaniu, innym sposobem byłoby określenie parametru typu ogólnego dla argumentu metody i użycie ograniczenia where do określenia obu interfejsów.
Andy,
Czy ma sens, że klasa, która implementuje I1 i I2, ale nie I3, nie powinna być używana przez foo? (Jeśli odpowiedź brzmi „tak”, to śmiało!)
user253751,
@Andy Chociaż nie do końca zgadzam się z twierdzeniem CodesInChaos, że jest to wada sama w sobie, nie sądzę, że ogólne parametry typu w metodzie są rozwiązaniem - przesuwa odpowiedzialność za znajomość typu zastosowanego w implementacji metody na cokolwiek wywołuje metodę, która wydaje się błędna. Być może przydałaby się składnia taka jak var : I1, I2 something = new A();dla zmiennych lokalnych (gdybyśmy zignorowali dobre punkty podniesione w odpowiedzi Konrada)
Kai

Odpowiedzi:

27

Wszystko, co implementuje I3, będzie musiało zaimplementować I1 i I2, a obiekty z różnych klas dziedziczących I3 mogą być następnie używane zamiennie (patrz poniżej), więc czy słusznie jest nazywać I3 pustym? Jeśli tak, jaki byłby lepszy sposób na architekturę?

„Wiązanie” I1i I2do I3oferuje ładny (?) Skrót lub jakiś cukier składniowy, gdziekolwiek chcesz, aby oba były zaimplementowane.

Problem z tym podejściem polega na tym, że nie można powstrzymać innych programistów przed bezpośrednim wdrożeniem I1 i jednoczesnym wdrażaniem I2, ale nie działa to w obie strony - chociaż : I3jest równoważne : I1, I2, : I1, I2nie jest równoważne z : I3. Nawet jeśli są funkcjonalnie identyczne, klasa obsługuje oba te elementy I1i I2nie zostanie wykryta jako obsługująca I3.

Oznacza to, że oferujesz dwa sposoby osiągnięcia tego samego - „ręczny” i „słodki” (z cukrem składniowym). I może to mieć zły wpływ na spójność kodu. Oferowanie 2 różnych sposobów, aby osiągnąć to samo tutaj, tam, w innym miejscu, daje już 8 możliwych kombinacji :)

void foo() {
    I1 something = new A();
    something = new B();
    something.SomeI1Function();
    something.SomeI2Function(); // we can't do this without casting first
}

OK, nie możesz. Ale może to właściwie dobry znak?

Jeśli I1i I2pociągają za sobą różne obowiązki (w przeciwnym razie nie byłoby potrzeby ich rozdzielania w pierwszej kolejności), to może foo()błędem jest próba somethingwykonania dwóch różnych obowiązków za jednym razem?

Innymi słowy, może być tak, że foo()samo narusza zasadę pojedynczej odpowiedzialności, a fakt, że wymaga sprawdzenia typu rzutowania i wykonania w celu jego uruchomienia, ostrzega nas o tym czerwoną flagą.

EDYTOWAĆ:

Jeśli nalegałeś, aby upewnić się, że foopobiera coś, co implementuje I1i I2, możesz przekazać je jako osobne parametry:

void foo(I1 i1, I2 i2) 

I mogą być tym samym przedmiotem:

foo(something, something);

Co fooobchodzi, czy są to te same wystąpienia, czy nie?

Ale jeśli to obchodzi (np. Ponieważ nie są bezpaństwowcami) lub po prostu uważasz, że to wygląda brzydko, zamiast tego można użyć ogólnych:

void Foo<T>(T something) where T: I1, I2

Teraz ogólne ograniczenia dbają o pożądany efekt, nie zanieczyszczając jednak świata zewnętrznego niczym. Jest to idiomatyczny język C #, oparty na kontrolach czasu kompilacji, i ogólnie wydaje się bardziej odpowiedni.

Widzę I3 : I2, I1trochę jako próba naśladować typów związków w języku, który nie obsługuje go z pudełka (C # lub Java nie, natomiast typy związkowi istnieć w językach funkcyjnych).

Próba emulacji konstrukcji, która tak naprawdę nie jest obsługiwana przez język, jest często niezręczna. Podobnie w języku C # możesz użyć metod rozszerzenia i interfejsów, jeśli chcesz emulować mixiny . Możliwy? Tak. Dobry pomysł? Nie jestem pewny.

Konrad Morawski
źródło
4

Jeśli szczególnie ważne było, aby klasa implementowała oba interfejsy, nie widzę nic złego w tworzeniu trzeciego interfejsu, który dziedziczy po obu. Byłbym jednak ostrożny, aby nie wpaść w pułapkę nadinżynierii. Klasa może dziedziczyć po jednym lub drugim lub obu. O ile mój program nie ma wielu klas w tych trzech przypadkach, tworzenie interfejsu dla obu jest trochę, a na pewno nie, jeśli te dwa interfejsy nie mają ze sobą żadnego związku (proszę nie ma interfejsów IComparableAndSerializable).

Przypominam, że możesz również stworzyć klasę abstrakcyjną, która dziedziczy z obu interfejsów, i możesz użyć tej klasy abstrakcyjnej zamiast I3. Myślę, że błędem byłoby powiedzieć, że nie powinieneś tego robić, ale powinieneś przynajmniej mieć dobry powód.

Neil
źródło
Co? Z pewnością możesz zaimplementować dwa interfejsy w jednej klasie. Nie dziedziczysz interfejsów, wdrażasz je.
Andy
@Andy Gdzie powiedziałem, że jedna klasa nie może zaimplementować dwóch interfejsów? Jeśli chodzi o użycie słowa „dziedziczenie” zamiast „implement”, semantyka. W C ++ dziedziczenie działałoby idealnie, ponieważ interfejsy najbliższe klasom abstrakcyjnym.
Neil
Dziedziczenie i implementacja interfejsu to nie te same pojęcia. Klasy dziedziczące wiele podklas mogą prowadzić do dziwnych problemów, co nie ma miejsca w przypadku implementacji wielu interfejsów. Brak interfejsów w C ++ nie oznacza, że ​​różnica znika, oznacza to, że C ++ ma ograniczone możliwości. Dziedziczenie jest relacją „jest-a”, podczas gdy interfejsy określają „można zrobić”.
Andy,
@Andy Dziwne problemy spowodowane kolizjami między metodami zaimplementowanymi we wspomnianych klasach, tak. Jednak nie jest to już interfejs ani w nazwie, ani w praktyce. Klasy abstrakcyjne, które deklarują, ale nie implementują metod, są w każdym znaczeniu tego słowa oprócz nazwy, interfejsu. Terminologia zmienia się między językami, ale to nie znaczy, że odniesienie i wskaźnik to nie to samo pojęcie. To pedantyczna kwestia, ale jeśli nalegacie na to, będziemy musieli zgodzić się nie zgodzić.
Neil
„Zmiany terminologii między językami” Nie, nie zmienia się. Terminologia OO jest spójna we wszystkich językach; Nigdy nie słyszałem abstrakcyjnej klasy oznaczającej cokolwiek innego w każdym języku OO, którego użyłem. Tak, możesz mieć semantykę interfejsu w C ++, ale chodzi o to, że nic nie stoi na przeszkodzie, aby ktoś poszedł i dodał zachowanie abstrakcyjnej klasie w tym języku i złamał semantykę.
Andy,
2

Puste interfejsy są ogólnie uważane za złą praktykę

Nie zgadzam się. Przeczytaj o interfejsach znaczników .

Podczas gdy typowy interfejs określa funkcjonalność (w postaci deklaracji metod), którą klasa implementująca musi obsługiwać, interfejs znacznika nie musi tego robić. Sama obecność takiego interfejsu wskazuje na określone zachowanie klasy implementującej.

radarbob
źródło
Wyobrażam sobie, że OP jest ich świadomy, ale w rzeczywistości są one nieco kontrowersyjne. Chociaż niektórzy autorzy je polecają (np. Josh Bloch w „Skutecznej Javie”), niektórzy twierdzą, że są anty-wzorem i dziedzictwem z czasów, gdy Java nie miała jeszcze adnotacji. (Adnotacje w Javie są mniej więcej odpowiednikiem atrybutów w .NET). Mam wrażenie, że są one znacznie bardziej popularne w świecie Java niż .NET.
Konrad Morawski
1
Używam ich od czasu do czasu, ale wydaje mi się to trochę hackerskie - wady, poza moją głową, polegają na tym, że nie możesz pomóc faktowi, że są dziedziczone, więc kiedy oznaczysz klasę jednym, wszystkie jej dzieci dostaną zaznaczono również, czy tego chcesz, czy nie. I wydają się zachęcać do złej praktyki używania instanceofzamiast polimorfizmu
Konrad Morawski