Ile jest za dużo interfejsów w klasie? [Zamknięte]

28

Prawdopodobnie uważałbym za zapach kodu, a nawet anty-wzorzec, aby mieć klasę, która implementuje 23 interfejsy. Jeśli to rzeczywiście jest anty-wzór, jak byś to nazwał? A może po prostu nie przestrzega zasady pojedynczej odpowiedzialności?

Jonas Elfström
źródło
3
Cóż, w obu przypadkach jest to wątpliwe. Jeśli klasa ma 23 metody, z których wszystkie odpowiadają za jedną odpowiedzialność (nie wszystkie klasy wymagają tak wielu metod, ale nie jest to niemożliwe - zwłaszcza jeśli większość z nich to trzy-liniowe owijki wokół innych metod), masz po prostu zbyt drobnoziarniste interfejsy; )
Klasa, o której mowa, jest rodzajem bytu, a interfejsy dotyczą głównie właściwości. Jest ich 113 i tylko cztery metody.
Jonas Elfström,
6
Myślałem, że interfejsy są zasadniczo używane do pisania kaczek w statycznie skompilowanych językach. Jaki jest problem? :)
ashes999
2
tylko pytanie: czy wszystkie 113 właściwości jest wypełnionych dla każdej instancji tego elementu? A może jest to jakiś „wspólny byt” używany w różnych kontekstach, z wypełnionymi tylko niektórymi właściwościami?
David
1
Wróćmy do konceptualnego zrozumienia tego, co stanowi „klasę”. Klasa to jedna rzecz z jasno określoną odpowiedzialnością i rolą. Czy możesz w ten sposób zdefiniować swój obiekt z 23 interfejsami? Czy możesz napisać jedno zdanie (nie Joycean), które odpowiednio podsumuje Twoją klasę? Jeśli tak, to 23 interfejsy są w porządku. Jeśli nie, to jest zbyt duży, zbyt skomplikowany.
Stephen Gross

Odpowiedzi:

36

Rodzina królewska: Nie robią nic szczególnie szalonego, ale mają miliard tytułów i są w jakiś sposób spokrewnieni z większością innych królewskich.

Inżynier świata
źródło
:-)))))))))))))
Emilio Garavaglia
Chciałbym podkreślićsomehow
Wolf
23

Twierdzę, że ten anty-wzór będzie nazywał się Jack of All Trades, a może Too Many Hats.

Mike Partridge
źródło
2
Co powiesz na szwajcarski scyzoryk?
FrustratedWithFormsDesigner
Wygląda na to, że termin ten jest już używany w przypadku interfejsów ze zbyt wieloma metodami :-) Chociaż to też miałoby sens.
Jalayn
1
@FrustratedWithFormsDesigner Szwajcarski scyzoryk nigdy nie miałby złego smaku posiadania interfejsu o nazwie IValue, który zawiera 30 właściwości, z których 20 poprzedza nowe słowo kluczowe modyfikator.
Jonas Elfström
3
+1 za zbyt wiele kapeluszy ... aby właściwie ukryć bóstwo
wielogłowe
1
Szwajcarski scyzoryk to model ponownego użycia. tzn. pojedyncze „ostrze” może wykonać dowolne metody, więc prawdopodobnie jest to zła analogia!
James Anderson,
17

Gdybym musiał nadać temu nazwę, myślę, że nazwałbym to Hydra :

Hydra

W mitologii greckiej Hydra Lernańska (grecki: Λερναία Ὕδρα) była starożytną bezimienną chtoniczną bestią wodną przypominającą węża, o cechach gadzich (jak sama nazwa wskazuje), która posiadała wiele głów - poeci wspominają o większej liczbie głów niż malarze wazonów farby, a na każdą odciętą głowę rosły jeszcze dwa - i jadowity oddech tak zjadliwy, że nawet jej ślady były śmiertelne.

Szczególnie istotny jest fakt, że ma nie tylko wiele głów, ale wciąż rośnie ich liczba i nie można z tego powodu zabić. Takie było moje doświadczenie z tego rodzaju projektami; programiści po prostu wciskają w to coraz więcej interfejsów, dopóki nie ma ich zbyt wiele, aby zmieścić się na ekranie, a do tego czasu jest tak głęboko zakorzeniony w projekcie programu i założeniach, że podzielenie go jest beznadziejną perspektywą (jeśli spróbujesz, faktycznie często potrzebują więcej interfejsów, aby wypełnić lukę).

Jednym z wczesnych oznak zbliżającego się losu z powodu „Hydry” jest częste rzucanie jednego interfejsu na drugi, bez sprawdzania rozsądku i często na cel, który nie ma sensu, jak w:

public void CreateWidget(IPartLocator locator, int widgetTypeId)
{
    var partsNeeded = locator.GetPartsForWidget(widgetTypeId);
    return ((IAssembler)locator).BuildWidget(partsNeeded);
}

Po wyjściu z kontekstu jest oczywiste, że w tym kodzie jest coś podejrzanego, ale prawdopodobieństwo tego dzieje się wraz ze wzrostem liczby „głów” obiektu, ponieważ programiści intuicyjnie wiedzą , że zawsze mają do czynienia z tym samym stworzeniem.

Powodzenia kiedykolwiek robi żadnej konserwacji obiektu, który implementuje 23 interfejsów. Szanse na to, że nie spowodujesz szkód ubocznych w tym procesie, są niewielkie.

Aaronaught
źródło
16

Bóg obiektu przychodzi do głowy; pojedynczy obiekt, który wie, jak zrobić WSZYSTKO. Wynika to z niskiego przestrzegania wymogów „spójności” dwóch głównych metodologii projektowania; jeśli masz obiekt z 23 interfejsami, masz obiekt, który wie, jak być 23 różnymi rzeczami dla jego użytkowników, a te 23 różne rzeczy prawdopodobnie nie są zgodne z jednym zadaniem lub obszarem systemu.

W SOLID, chociaż twórca tego obiektu najwyraźniej starał się przestrzegać zasady segregacji interfejsu, naruszył wcześniejszą regułę; Zasada pojedynczej odpowiedzialności. Jest powód, dla którego jest „SOLID”; S zawsze zajmuje pierwsze miejsce, gdy myśli o designie, i przestrzegane są wszystkie pozostałe zasady.

W GRASP twórca takiej klasy zlekceważył zasadę „wysokiej spójności”; GRASP, w przeciwieństwie do SOLID, uczy, że obiekt NIE MUSI ponosić żadnej odpowiedzialności, ale co najwyżej powinien mieć dwie lub trzy bardzo ściśle powiązane obowiązki.

KeithS
źródło
Nie jestem pewien, czy jest to anty-wzorzec Boskiego Obiektu, ponieważ Bóg nie jest ograniczony żadnymi interfejsami. Ograniczenie przez tak wiele interfejsów może zmniejszyć wszechmoc Boga. ;) Może to jest obiekt Chimera?
FrustratedWithFormsDesigner
Bóg ma wiele twarzy i może być wiele rzeczy dla wielu ludzi. Jeśli funkcjonalność połączonych interfejsów sprawia, że ​​obiekt jest „wszechwiedzący”, tak naprawdę nie ogranicza Boga, prawda?
KeithS,
1
Chyba że to, co stworzyło interfejsy, je zmodyfikuje. W takim razie Bóg może wymagać ponownej implementacji, aby dostosować się do zmian interfejsu.
FrustratedWithFormsDesigner
3
Boli, że myślisz, że to ja stworzyłem klasę.
Jonas Elfström,
Używanie słowa „ty” odnosi się bardziej do liczby mnogiej, odnosząc się do zespołu deweloperów. Ktoś z tego zespołu, dawny lub obecny, stworzył tę klasę. Będę jednak edytować.
KeithS,
8

23 to tylko liczba! W najbardziej prawdopodobnym scenariuszu jest wystarczająco wysoki, aby zaalarmować. Jeśli jednak zapytamy, jaka jest najwyższa liczba metod / interfejsów, zanim będzie mógł zostać wywołany znacznik jako „anty-wzór”? Czy to 5, 10 czy 25? Zdajesz sobie sprawę, że liczba tak naprawdę nie jest odpowiedzią, ponieważ jeśli 10 jest dobre, 11 może być również - a następnie dowolną liczbą całkowitą po tym.

Prawdziwe pytanie dotyczy złożoności. I powinniśmy zauważyć, że długi kod, liczba metod lub duży rozmiar klasy według jakichkolwiek miar NIE jest tak naprawdę definicją złożoności. Tak, coraz większy kod (większa liczba metod) utrudnia czytanie i zrozumienie dla nowego początkującego. Obsługuje również potencjalnie zróżnicowaną funkcjonalność, dużą liczbę wyjątków i dość rozwinięte algorytmy dla różnych scenariuszy. Nie oznacza to, że jest skomplikowany - jest tylko trudny do strawienia.

Z drugiej strony stosunkowo niewielki rozmiar kodu, który można odczytać za kilka godzin, może być nadal złożony. Oto, kiedy myślę, że kod jest (niepotrzebnie) złożony.

Każdy element mądrości projektowania zorientowanego obiektowo może być tutaj użyty do zdefiniowania „złożonego”, ale ograniczyłbym się tutaj, aby pokazać, kiedy „tak wiele metod” jest oznaką złożoności.

  1. Wzajemna wiedza. (inaczej sprzężenie) Wiele razy, gdy rzeczy są zapisywane jako klasy, wszyscy uważamy, że jest to „miły” kod obiektowy. Ale założenie o drugiej klasie w gruncie rzeczy przełamuje rzeczywistą potrzebną enkapsulację. Gdy masz metody, które „wyciekają” szczegółowe informacje o wewnętrznym stanie algorytmów - i aplikacja jest budowana z kluczowym założeniem o wewnętrznym stanie klasy obsługującej.

  2. Zbyt wiele powtórzeń (włamywanie się) Gdy metody o podobnych nazwach działają przeciwstawnie - lub sprzeczne z nazwami o podobnej funkcjonalności. Wielokrotnie ewoluują kody, które obsługują nieco inne interfejsy dla różnych aplikacji.

  3. Zbyt wiele ról Gdy klasa ciągle dodaje funkcje poboczne i rozwija się coraz bardziej, jak ludziom się podoba, tylko po to, aby wiedzieć, że klasa jest teraz dwiema klasami. Niespodziewanie to wszystko zacząć prawdziwawymagania i żadna inna klasa nie istnieje, aby to zrobić. Rozważ to, istnieje klasa Transakcja, która informuje o szczegółach transakcji. Do tej pory wygląda dobrze, teraz ktoś wymaga konwersji formatu w „czasie transakcji” (między UTC i podobnymi), później ludzie dodają reguły, aby sprawdzić, czy pewne rzeczy były w określonych terminach, aby zweryfikować nieważność transakcji. - Nie będę pisać całej historii, ale ostatecznie klasa transakcji buduje w niej cały kalendarz, a następnie ludzie zaczynają używać części „tylko kalendarz”! Jest to bardzo skomplikowane (aby to sobie wyobrazić), dlaczego utworzę instancję „klasy transakcji”, aby mieć funkcjonalność, którą zapewniłby mi „kalendarz”!

  4. (Nie) spójność API Kiedy robię book_a_ticket () - rezerwuję bilet! To bardzo proste, bez względu na to, ile kontroli i procesów musi się odbyć. Teraz staje się skomplikowane, gdy upływ czasu zaczyna na to wpływać. Zazwyczaj zezwala się na „wyszukiwanie” i możliwy stan dostępności / niedostępności, a następnie w celu zmniejszenia liczby zwrotów w przód zaczynasz zapisywać pewne wskaźniki kontekstu wewnątrz biletu, a następnie odsyłać to do rezerwacji biletu. Wyszukiwanie to nie jedyna funkcjonalność - po wielu takich „funkcjach bocznych” sytuacja się pogarsza. W tym znaczeniu znaczenie book_a_ticket () oznacza book_that_ticket ()! i który może być niewiarygodnie skomplikowane.

Prawdopodobnie istnieje wiele takich sytuacji, które można zobaczyć w bardzo rozwiniętym kodzie i jestem pewien, że wiele osób może dodać scenariusze, w których „tak wiele metod” po prostu nie ma sensu lub nie robi tego, co byś oczywiście pomyślał. To jest anty-wzór.

Moje osobiste doświadczenie jest takie, że kiedy projekty, które rozpoczynają się w sposób rozsądny oddolnie, wiele rzeczy, dla których powinny istnieć prawdziwe klasy, pozostaje zakopanych lub gorzej, wciąż pozostaje podzielonych między różne klasy i rośnie sprzężenie. Najczęściej to, co zasłużyłoby na 10 klas, ale ma tylko 4, jest prawdopodobne, że wiele z nich ma wielozadaniowe mylące i dużą liczbę metod. Nazwij to BOGIEM i SMOKIEM, to jest ZŁE.

ALE natkniesz się na BARDZO DUŻE klasy, które są starannie spójne, mają 30 metod - i nadal są bardzo CZYSTE. Mogą być dobre.

Dipan Mehta
źródło
Klasa w pytaniach ma 23 interfejsy, które zawierają w sumie 113 właściwości publicznych i cztery metody. Tylko niektóre z nich są tylko do odczytu. C # ma właściwości automatycznie implementowane, dlatego
różnię
7

Klasy powinny mieć dokładnie odpowiednią liczbę interfejsów; nie więcej nie mniej.

Powiedzenie „zbyt wielu” byłoby niemożliwe bez sprawdzenia, czy wszystkie te interfejsy służą pożytecznemu celowi w tej klasie. Deklaracja, że ​​obiekt implementuje interfejs oznacza, że ​​musisz zaimplementować jego metody, jeśli spodziewasz się, że klasa się skompiluje. (Zakładam, że klasa, na którą się patrzysz.) Jeśli wszystko w interfejsie zostanie zaimplementowane, a te implementacje zrobią coś w stosunku do wewnętrznych elementów klasy, trudno byłoby powiedzieć, że implementacja nie powinna tam być. Jedyny przypadek, w którym mogę wymyślić, gdzie interfejs spełniający te kryteria nie należałby, jest zewnętrzny: kiedy nikt go nie używa.

Niektóre języki umożliwiają „podklasowanie” interfejsów za pomocą mechanizmu takiego jak extendssłowo kluczowe Javy , a ktokolwiek to napisał, mógł tego nie wiedzieć. Może się również zdarzyć, że wszystkie 23 są na tyle odległe, że ich agregacja nie ma sensu.

Blrfl
źródło
Uznałem, że to pytanie jest dość agnostyczne. Rzeczywisty kod znajduje się w C #.
Jonas Elfström,
Nie wykonałem żadnego C #, ale wydaje się, że obsługuje podobną formę dziedziczenia interfejsu.
Blrfl
Tak, interfejs może „odziedziczyć” inne interfejsy i tak dalej.
Jonas Elfström
Interesujące jest dla mnie to, że wiele dyskusji koncentruje się na zajęciach , a nie na instancjach . Robot z różnymi czujnikami obrazowania i audio podłączonymi do wspólnego podwozia powinien być modelowany jako byt, który ma lokalizację i orientację, i który może widzieć, słyszeć i kojarzyć widoki i dźwięki. Cała logika stojąca za tymi funkcjami może dotyczyć innych klas, ale sam robot powinien obsługiwać metody takie jak „sprawdź, czy jakieś obiekty są przed nami”, „nasłuchuj jakichkolwiek odgłosów w okolicy” lub „sprawdź, czy obiekt przed nami wygląda, jakby to był generowanie wykrytych dźwięków ”.
supercat
Może się zdarzyć, że konkretny robot powinien w określony sposób podzielić swoją funkcjonalność na podsystemy, ale w takim zakresie, w jakim praktyczny kod, który używa robota, nie powinien wiedzieć ani przejmować się tym, w jaki sposób funkcjonalność danego robota jest podzielona. Gdyby „oczy” i „uszy” były wystawione na świat zewnętrzny jako osobne obiekty, ale odpowiadające im czujniki znajdowały się na wspólnym wysięgniku, kod zewnętrzny może prosić „uszy”, aby nasłuchiwał dźwięków na poziomie gruntu w tym samym czasie, co poprosił „oczy”, by spojrzały ponad przeszkodę.
supercat
1

Brzmi tak, jakby wydawało się, że mają parę „getter” / „setter” dla każdej właściwości, co jest normalne dla typowego „bean” i promuje wszystkie te metody do interfejsu. A może nazwać to „ ma fasolą ”. Lub bardziej Rabelaisowskie „wzdęcia” po dobrze znanym wpływie zbyt wielu ziaren.

James Anderson
źródło
Jest w C # i ma właściwości automatycznie wdrażane. Te 23 interfejsy zawierają w sumie 113 takich właściwości.
Jonas Elfström
1

Liczba interfejsów często rośnie, gdy obiekt ogólny jest używany w różnych środowiskach. W języku C # warianty IComparable, IEqualityComparer i IComparer pozwalają na sortowanie w różnych konfiguracjach, więc możesz skończyć implementacją wszystkich z nich, niektóre z nich mogą być więcej niż jeden raz, ponieważ możesz zaimplementować ogólne wersje silnie typowane, a także wersje inne niż ogólne , dodatkowo możesz zaimplementować więcej niż jeden rodzaj ogólny.

Weźmy przykładowy scenariusz, powiedzmy sklep internetowy, w którym możesz kupić kredyty, za pomocą których możesz kupić coś innego (witryny z fotografiami często używają tego schematu). Możesz mieć klasę „Valuta” i klasę „Kredyty”, które dziedziczą z tej samej bazy. Valuta ma pewne przeciążenia operatora i procedury porównywania, które pozwalają wykonywać obliczenia bez martwienia się o właściwość „Waluta” (na przykład dodawanie funtów do dolarów). Kredyty są dużo prostsze, ale zachowują się inaczej. Chcąc być w stanie porównać je ze sobą, możesz w końcu wdrożyć IComparable, IComparable i inne warianty interfejsów porównania na obu (nawet jeśli używają wspólnej implementacji, czy to w klasie podstawowej, czy gdzie indziej).

Podczas implementowania serializacji wdrażane jest ISerializable, IDeserializationCallback. Następnie wdrażanie stosów cofania i ponawiania: Dodano IClonable. Funkcjonalność IsDirty: IObservable, INotifyPropertyChanged. Zezwalanie użytkownikom na edycję wartości przy użyciu ciągów: IConvertable ... Lista może się zmieniać i ciągnąć ...

We współczesnych językach widzimy inny trend, który pomaga oddzielić te aspekty i umieścić je we własnych klasach, poza klasą podstawową. Klasa zewnętrzna lub aspekt jest następnie kojarzony z klasą docelową za pomocą adnotacji (atrybutów). Często możliwe jest uczynienie klas aspektów zewnętrznych bardziej lub mniej ogólnymi.

Wykorzystanie atrybutów (adnotacji) podlega refleksji. Wadą jest niewielka (początkowa) utrata wydajności. Wadą (często emocjonalną) jest to, że zasady takie jak enkapsulacja muszą być rozluźnione.

Zawsze są inne rozwiązania, ale dla każdego fajnego rozwiązania występuje kompromis lub haczyk. Na przykład użycie rozwiązania ORM może wymagać uznania wszystkich właściwości za wirtualne. Rozwiązania do serializacji mogą wymagać domyślnych konstruktorów w twoich klasach. Jeśli używasz wstrzykiwania zależności, możesz w końcu wdrożyć 23 interfejsy w jednej klasie.

Moim zdaniem 23 interfejsy z definicji nie muszą być złe. Może kryć się za tym dobrze przemyślany schemat lub pewne zasadnicze określenie, takie jak unikanie refleksji lub ekstremalnych przekonań enkapsulacji.

Za każdym razem, gdy zmieniasz zadanie lub musisz budować na istniejącej architekturze. Radzę najpierw się w pełni zapoznać, nie próbuj wszystko refaktoryzować zbyt szybko. Posłuchaj oryginalnego programisty (jeśli nadal tam jest) i spróbuj dowiedzieć się, co kryje się za tym, co widzisz. Zadając pytania, nie rób tego, aby go rozbić, ale aby się nauczyć ... Tak, każdy ma swoje własne złote młoty, ale im więcej młotków możesz zebrać, tym łatwiej będzie ci dogadać się z innymi kolegami.

Louis Somers
źródło
0

„Zbyt wiele” jest subiektywne: styl programowania? wydajność? zgodność ze standardami? precedensy? po prostu poczucie komfortu / pewności siebie?

Dopóki twój kod zachowuje się poprawnie i nie stwarza problemów z utrzymaniem, 23 może nawet być nową normą. Pewnego dnia mógłbym wtedy powiedzieć: „Możesz wykonać kawał dobrej roboty z 23 interfejsami, patrz: Jonas Elfström”.

Kris
źródło
0

Powiedziałbym, że limit powinien być mniejszy niż 23 - więcej niż 5 lub 7,
jednak nie obejmuje to żadnej liczby interfejsów dziedziczonych przez te interfejsy ani żadnej liczby interfejsów implementowanych przez klasy podstawowe.

(W sumie N + dowolna liczba odziedziczonych interfejsów, gdzie N <= 7.)

Jeśli twoja klasa implementuje zbyt wiele interfejsów, prawdopodobnie jest to klasa boska .

Danny Varod
źródło