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?
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?
Odpowiedzi:
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.
somehow
Twierdzę, że ten anty-wzór będzie nazywał się Jack of All Trades, a może Too Many Hats.
Gdybym musiał nadać temu nazwę, myślę, że nazwałbym to 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.
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.
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.
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.
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.
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”!
(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.
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 extends
sł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.
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.
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.
„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”.
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 .