Czy istnieje nazwa (anty-) wzorca przekazywania parametrów, który będzie używany tylko kilka poziomów w łańcuchu połączeń?

208

Próbowałem znaleźć alternatywy dla użycia zmiennej globalnej w jakimś starszym kodzie. Ale to pytanie nie dotyczy alternatyw technicznych, martwię się głównie terminologią .

Oczywistym rozwiązaniem jest przekazanie parametru do funkcji zamiast użycia globalnego. W tej starej bazie kodu oznaczałoby to, że muszę zmienić wszystkie funkcje w długim łańcuchu wywołań między punktem, w którym wartość zostanie ostatecznie użyta, a funkcją, która odbierze parametr jako pierwszy.

higherlevel(newParam)->level1(newParam)->level2(newParam)->level3(newParam)

gdzie newParampoprzednio była zmienną globalną w moim przykładzie, ale zamiast tego mogła być wcześniej zakodowaną wartością. Chodzi o to, że teraz wartość newParam jest uzyskiwana na higherlevel()i musi „podróżować” aż do level3().

Zastanawiałem się, czy istnieją nazwy dla tego rodzaju sytuacji / wzorca, w którym należy dodać parametr do wielu funkcji, które po prostu „przekazują” wartość niezmodyfikowaną.

Mam nadzieję, że zastosowanie właściwej terminologii pozwoli mi znaleźć więcej zasobów na temat rozwiązań dotyczących przeprojektowania i opisać tę sytuację kolegom.

ecerulm
źródło
94
Jest to poprawa w stosunku do używania zmiennych globalnych. Wyjaśnia dokładnie, od jakiego stanu zależy każda funkcja (i stanowi jeden krok na drodze do czystych funkcji). Słyszałem, że nazywa się to „wątkiem” parametru, ale nie wiem, jak powszechna jest ta terminologia.
ogrodnik
8
Jest to rodzaj zbyt szerokiego spektrum, aby uzyskać konkretną odpowiedź. Na tym poziomie nazwałbym to po prostu „kodowaniem”.
Machado,
38
Myślę, że „problemem” jest po prostu prostota. Jest to w zasadzie wstrzyknięcie zależności. Sądzę, że mogą istnieć mechanizmy, które mogłyby automatycznie wstrzykiwać zależność przez łańcuch, jeśli ma ją jakiś głębiej zagnieżdżony element, bez rozszerzania list parametrów parametrów funkcji. Być może spojrzenie na strategie wstrzykiwania zależności o różnych poziomach zaawansowania może prowadzić do poszukiwanej terminologii, jeśli taka istnieje.
null
7
Chociaż doceniam dyskusję na temat tego, czy jest to dobry wzorzec / antipattern / koncepcja / rozwiązanie, tak naprawdę chciałem wiedzieć, czy istnieje na to nazwa.
ecerulm
3
Słyszałem też, że nazywa gwintowania najczęściej, ale także hydraulika , jak w obniżaniu pionu całym stosie wywołań.
wchargin

Odpowiedzi:

202

Same dane nazywane są „danymi trampowymi” . Jest to „zapach kodu”, wskazujący, że jeden fragment kodu komunikuje się z innym fragmentem kodu na odległość, za pośrednictwem pośredników.

  • Zwiększa sztywność kodu, szczególnie w łańcuchu połączeń. Jesteś znacznie bardziej ograniczony w sposobie refaktoryzacji dowolnej metody w łańcuchu połączeń.
  • Dystrybuuje wiedzę o danych / metodach / architekturze do miejsc, które w najmniejszym stopniu nie dbają o to. Jeśli musisz zadeklarować dane, które właśnie przechodzą, a deklaracja wymaga nowego importu, zanieczyściłeś przestrzeń nazw.

Refaktoryzacja w celu usunięcia zmiennych globalnych jest trudna, a dane trampowe są jedną z metod, a często najtańszą. Ma swoje koszty.

BobDalgleish
źródło
73
Szukając „danych trampowych” udało mi się znaleźć książkę „Code Complete” w mojej subskrypcji Safari. W książce znajduje się rozdział zatytułowany „Powody korzystania z danych globalnych”, a jednym z powodów jest „Korzystanie z danych globalnych może wyeliminować dane trampowe”. :) Mam wrażenie, że „dane trampowe” pozwolą mi znaleźć więcej literatury na temat radzenia sobie z globalnymi. Dzięki!
ecerulm
9
@JimmyJames, te funkcje oczywiście działają. Po prostu nie z tym konkretnym nowym parametrem, który wcześniej był tylko globalny.
ecerulm
174
Przez 20 lat programowania dosłownie nigdy wcześniej nie słyszałem tego terminu, ani nie byłoby od razu oczywiste, co to znaczy. Nie narzekam na odpowiedź, sugeruję tylko, że termin ten nie jest tak powszechnie używany / znany. Może to tylko ja.
Derek Elkins,
6
Niektóre dane globalne są w porządku. Zamiast nazywać to „danymi globalnymi”, można to nazwać „środowiskiem” - bo tak właśnie jest. Środowisko może zawierać na przykład ścieżkę ciągu dla appdata (w systemie Windows) lub w moim bieżącym projekcie cały zestaw pędzli GDI +, pisaków, czcionek itp. Używanych przez wszystkie komponenty.
Robinson,
7
@Robinson Niezupełnie. Na przykład, czy naprawdę chcesz, aby kod do zapisywania obrazów dotykał% AppData%, czy wolałbyś, aby wziął argument za tym, gdzie pisać? To jest różnica między stanem globalnym a argumentem. „Środowisko” może być równie łatwo wstrzykiwaną zależnością, obecne tylko dla tych, którzy są odpowiedzialni za interakcję ze środowiskiem. Pędzle GDI + itp. Są bardziej rozsądne, ale tak naprawdę jest to bardziej przypadek zarządzania zasobami w środowisku, które nie jest w stanie tego zrobić dla ciebie - prawie brak podstawowych API i / lub twojego języka / bibliotek / środowiska uruchomieniowego.
Luaan,
101

Nie sądzę, że to samo w sobie jest anty-wzorem. Myślę, że problem polega na tym, że myślisz o funkcjach jako łańcuchu, podczas gdy naprawdę powinieneś myśleć o każdej z nich jako o niezależnej czarnej skrzynce ( UWAGA : metody rekurencyjne są godnym uwagi wyjątkiem od tej porady).

Załóżmy na przykład, że muszę obliczyć liczbę dni między dwiema datami kalendarzowymi, aby utworzyć funkcję:

int daysBetween(Day a, Day b)

Aby to zrobić, tworzę nową funkcję:

int daysSinceEpoch(Day day)

Wtedy moja pierwsza funkcja staje się po prostu:

int daysBetween(Day a, Day b)
{
    return daysSinceEpoch(b) - daysSinceEpoch(a);
}

Nie ma w tym nic antyzapachowego. Parametry metody daysBetween są przekazywane do innej metody i nigdy nie są w żaden sposób przywoływane w metodzie, ale nadal są potrzebne, aby ta metoda mogła wykonać to, co musi zrobić.

Polecam przyjrzeć się każdej funkcji i zacząć od kilku pytań:

  • Czy ta funkcja ma jasny i ukierunkowany cel, czy może jest to metoda „robienia pewnych rzeczy”? Zwykle pomaga tutaj nazwa funkcji, a jeśli jest w niej coś, co nie jest opisane przez nazwę, jest to czerwona flaga.
  • Czy jest za dużo parametrów? Czasami metoda może wymagać wielu danych wejściowych, ale posiadanie tak wielu parametrów sprawia, że ​​korzystanie z niej lub jej zrozumienie jest uciążliwe.

Jeśli patrzysz na zbieraninę kodu bez jednego celu zawartego w metodzie, powinieneś zacząć od rozwiązania tego. To może być nudne. Zacznij od najprostszych rzeczy do wyciągnięcia i przejdź do oddzielnej metody i powtarzaj, aż uzyskasz coś spójnego.

Jeśli masz po prostu zbyt wiele parametrów, rozważ Refaktoryzację metody do obiektu .

JimmyJames
źródło
2
Cóż, nie chciałem być kontrowersyjny wobec (anty-). Nadal jednak zastanawiam się, czy istnieje nazwa „sytuacji” polegającej na konieczności aktualizacji wielu podpisów funkcji. Wydaje mi się, że jest bardziej „zapachem kodu” niż antypatternem. Mówi mi, że w tym starszym kodzie jest coś do naprawienia, jeśli muszę zaktualizować sygnaturę 6 funkcji, aby umożliwić wyeliminowanie globalnego. Ale wydaje mi się, że przekazywanie parametrów jest zwykle w porządku i doceniam porady, jak rozwiązać podstawowy problem.
ecerulm
4
@ ecerulm Nie znam żadnego z nich, ale powiem, że moje doświadczenie mówi mi, że konwersja globałów na parametry jest absolutnie właściwym sposobem na ich wyeliminowanie. Eliminuje to stan udostępniania, dzięki czemu można dalej refaktoryzować. Domyślam się, że jest więcej problemów z tym kodem, ale w twoim opisie nie ma wystarczająco dużo, aby wiedzieć, co to jest.
JimmyJames
2
Zazwyczaj też podążam za tym podejściem i prawdopodobnie tak też się stanie. Chciałem tylko poprawić moje słownictwo / terminologię w tym zakresie, aby móc badać więcej na ten temat i robić lepsze, bardziej ukierunkowane pytania w przyszłości.
ecerulm
3
@ ecerulm Nie sądzę, że jest na to jedna nazwa. To jak objaw powszechny w wielu chorobach, a także w stanach innych niż choroby, np. „Suchość w ustach”. Jeśli dopracujesz opis struktury kodu, może to wskazywać na coś konkretnego.
JimmyJames
@ ecerulm Mówi ci, że jest coś do naprawienia - teraz jest o wiele bardziej oczywiste, że coś należy naprawić niż wtedy, gdy była to zmienna globalna.
immibis
61

BobDalgleish zauważył już, że ten (anty-) wzorzec nazywany jest „ danymi trampowymi ”.

Z mojego doświadczenia wynika, że ​​najczęstszą przyczyną nadmiernej ilości danych trampowych jest wiązka zmiennych stanu połączonych, które powinny być naprawdę zamknięte w obiekcie lub strukturze danych. Czasami może być nawet konieczne zagnieżdżenie wielu obiektów w celu prawidłowej organizacji danych.

Dla prostego przykładu rozważmy grę, która posiada konfigurowalny player charakter, o właściwościach podobnych playerName, playerEyeColori tak dalej. Oczywiście gracz ma również fizyczną pozycję na mapie gry i różne inne właściwości, takie jak, powiedzmy, aktualny i maksymalny poziom zdrowia i tak dalej.

Podczas pierwszej iteracji takiej gry rozsądnym wyborem może być przekształcenie wszystkich tych właściwości w zmienne globalne - w końcu jest tylko jeden gracz i prawie wszystko w grze w jakiś sposób dotyczy gracza. Więc twój stan globalny może zawierać zmienne takie jak:

playerName = "Bob"
playerEyeColor = GREEN
playerXPosition = -8
playerYPosition = 136
playerHealth = 100
playerMaxHealth = 100

Ale w pewnym momencie może się okazać, że musisz zmienić ten projekt, być może dlatego, że chcesz dodać do gry tryb wieloosobowy. Jako pierwszą próbę możesz spróbować ustawić wszystkie te zmienne lokalnie i przekazać je do funkcji, które ich potrzebują. Może się jednak okazać, że określone działanie w grze może obejmować łańcuch wywołań funkcji, na przykład:

mainGameLoop()
 -> processInputEvent()
     -> doPlayerAction()
         -> movePlayer()
             -> checkCollision()
                 -> interactWithNPC()
                     -> interactWithShopkeeper()

... a interactWithShopkeeper()funkcja ma adres sprzedawcy do gracza po imieniu, więc teraz musisz nagle przekazać playerNamedane trampowe przez wszystkie te funkcje. I oczywiście, jeśli sprzedawca myśli, że niebieskoocy gracze są naiwni, i będzie pobierać za nich wyższe ceny, wówczas musisz przejść playerEyeColorprzez cały łańcuch funkcji i tak dalej.

Właściwe rozwiązanie, w tym przypadku, to oczywiście do zdefiniowania obiektu odtwarzacza, która zamyka nazwa, kolor oczu, stanowisko, zdrowia i innych właściwości postać gracza. W ten sposób wystarczy przekazać ten pojedynczy obiekt wszystkim funkcjom, które w jakiś sposób dotyczą odtwarzacza.

Ponadto niektóre z powyższych funkcji można naturalnie przekształcić w metody tego obiektu gracza, co automatycznie zapewniłoby im dostęp do właściwości gracza. W pewnym sensie jest to po prostu cukier składniowy, ponieważ wywołanie metody na obiekcie skutecznie przekazuje instancję obiektu jako parametr ukryty do metody, ale sprawia, że ​​kod wygląda na wyraźniejszy i bardziej naturalny, jeśli jest właściwie stosowany.

Oczywiście, typowa gra miałaby znacznie bardziej „globalny” stan niż tylko gracz; na przykład, prawie na pewno miałbyś jakąś mapę, na której gra się toczy, oraz listę postaci niebędących graczami poruszającymi się po mapie, a może umieszczone na niej przedmioty i tak dalej. Możesz przekazać je wszystkie jako obiekty trampowe, ale to znowu zaśmieci twoje argumenty metody.

Zamiast tego rozwiązaniem jest, aby obiekty przechowywały odniesienia do wszelkich innych obiektów, z którymi mają stałe lub tymczasowe relacje. Na przykład obiekt gracza (i prawdopodobnie także wszelkie obiekty NPC) prawdopodobnie powinien przechowywać odniesienie do obiektu „świata gry”, który miałby odniesienie do bieżącego poziomu / mapy, aby metoda player.moveTo(x, y)taka nie musiała otrzymać jawnie mapę jako parametr.

Podobnie, gdyby nasza postać gracza miała, powiedzmy, psa, który za nimi podążał, naturalnie pogrupowalibyśmy wszystkie zmienne stanu opisujące psa w jeden obiekt i nadaliśmy obiektowi gracza odniesienie do psa (aby gracz mógł powiedzmy, nazwij psa po imieniu) i odwrotnie (aby pies wiedział, gdzie jest gracz). I oczywiście chcielibyśmy, aby gracz i pies sprzeciwiali się obu podklasom bardziej ogólnego obiektu „aktora”, abyśmy mogli ponownie użyć tego samego kodu do, powiedzmy, poruszania się po mapie.

Ps. Mimo że użyłem gry jako przykładu, istnieją inne rodzaje programów, w których pojawiają się takie problemy. Z mojego doświadczenia wynika jednak, że podstawowy problem jest zawsze taki sam: masz kilka oddzielnych zmiennych (lokalnych lub globalnych), które naprawdę chcą być połączone w jeden lub więcej powiązanych ze sobą obiektów. Niezależnie od tego, czy „dane trampowe” wtrącające się w twoje funkcje obejmują ustawienia „globalne”, buforowane zapytania do bazy danych lub wektory stanu w symulacji numerycznej, rozwiązaniem jest niezmiennie określenie naturalnego kontekstu , do którego należą dane, i przekształcenie go w obiekt (lub jakikolwiek najbliższy odpowiednik w wybranym języku).

Ilmari Karonen
źródło
1
Ta odpowiedź zawiera rozwiązania szeregu problemów, które mogą wystąpić. Mogą wystąpić sytuacje, w których zastosowano globały, co wskazywałoby na inne rozwiązanie. Mam problem z tym, że włączenie metod do klasy gracza jest równoważne z przekazywaniem obiektów do metod. Ignoruje to polimorfizm, który nie jest łatwo replikowany w ten sposób. Na przykład, jeśli chcę stworzyć różne typy graczy, które mają różne reguły dotyczące ruchów i różne typy właściwości, po prostu przekazanie tych obiektów do implementacji jednej metody będzie wymagało dużo logiki warunkowej.
JimmyJames,
6
@JimmyJames: Twoja uwaga na temat polimorfizmu jest słuszna i zastanawiałem się nad tym, by zrobić to sam, ale pominąłem ją, aby nie dopuścić do wydłużenia się odpowiedzi. Chodziło mi o to (być może słabo), że chociaż czysto pod względem przepływu danych jest niewielka różnica między, foo.method(bar, baz)i method(foo, bar, baz)istnieją inne powody (w tym polimorfizm, enkapsulacja, lokalizacja itp.), Aby preferować to pierwsze.
Ilmari Karonen,
@IlmariKaronen: również bardzo oczywista korzyść, że w przyszłości zabezpiecza prototypy funkcji przed wszelkimi przyszłymi zmianami / uzupełnieniami / usunięciami / refaktoryzacjami w obiektach (np. PlayerAge). To samo jest bezcenne.
smci
34

Nie znam konkretnej nazwy tego, ale myślę, że warto wspomnieć, że problem, który opisujesz, jest po prostu problemem znalezienia najlepszego kompromisu dla zakresu takiego parametru:

  • jako zmienna globalna zakres jest zbyt duży, gdy program osiągnie określony rozmiar

  • jako parametr lokalny zakres może być zbyt mały, gdy prowadzi do wielu list powtarzalnych parametrów w łańcuchach wywołań

  • więc jako kompromis często można uczynić taki parametr zmienną składową w jednej lub więcej klasach, i to właśnie nazwałbym właściwym projektem klasy .

Doktor Brown
źródło
10
+1 za właściwy projekt klasy. Brzmi to jak klasyczny problem czekający na rozwiązanie OO.
l0b0
21

Wierzę, że opisany przez ciebie wzór jest dokładnie zastrzykiem zależności . Kilku komentujących twierdziło, że jest to wzorzec , a nie anty-wzorzec , i chciałbym się zgodzić.

Zgadzam się również z odpowiedzią @ JimmyJames, w której twierdzi on, że dobrą praktyką programistyczną jest traktowanie każdej funkcji jako czarnej skrzynki, która przyjmuje wszystkie dane wejściowe jako jawne parametry. Oznacza to, że jeśli piszesz funkcję, która tworzy masło orzechowe i galaretkową kanapkę, możesz to zapisać jako

Sandwich make_sandwich() {
    PeanutButter pb = get_peanut_butter();
    Jelly j = get_jelly();
    return pb + j;
}
extern PhysicalRefrigerator g_refrigerator;
PeanutButter get_peanut_butter() {
    return g_refrigerator.get("peanut butter");
}
Jelly get_jelly() {
    return g_refrigerator.get("jelly");
}

ale lepszym rozwiązaniem byłoby zastosowanie wstrzyknięcia zależności i napisanie go w ten sposób:

Sandwich make_sandwich(Refrigerator& r) {
    PeanutButter pb = get_peanut_butter(r);
    Jelly j = get_jelly(r);
    return pb + j;
}
PeanutButter get_peanut_butter(Refrigerator& r) {
    return r.get("peanut butter");
}
Jelly get_jelly(Refrigerator& r) {
    return r.get("jelly");
}

Teraz masz funkcję, która wyraźnie dokumentuje wszystkie swoje zależności w sygnaturze funkcji, co jest świetne dla czytelności. W końcu prawdą jest, że aby make_sandwichuzyskać dostęp do Refrigerator; więc sygnatura starej funkcji była zasadniczo nieuczciwa, ponieważ nie brała lodówki jako części jej danych wejściowych.

Jako bonus, jeśli dobrze przestrzegasz swojej hierarchii klas, unikaj krojenia itp., Możesz nawet przetestować make_sandwichfunkcję, przekazując MockRefrigerator! (Może być konieczne przetestowanie go w ten sposób, ponieważ środowisko testowe może nie mieć dostępu do żadnych PhysicalRefrigerators.)

Rozumiem, że nie wszystkie zastosowania wstrzykiwania zależności wymagają umieszczenia parametru o podobnej nazwie na wielu poziomach w dół stosu wywołań, więc nie odpowiadam dokładnie na zadane pytanie ... ale jeśli szukasz dalszej lektury na ten temat, „wstrzykiwanie zależności” jest zdecydowanie trafnym słowem kluczowym dla Ciebie.

Quuxplusone
źródło
10
Jest to bardzo wyraźnie anty- wzór. Absolutnie nie ma potrzeby przekazywania lodówki. Teraz przekazanie ogólnego składnika IngredientSource może działać, ale co, jeśli weźmiesz chleb z kosza na chleb, tuńczyka ze spiżarni, sera z lodówki ... poprzez wstrzyknięcie zależności od źródła składników do niezwiązanej operacji tworzenia tych składników składników w kanapkę, naruszyłeś separację obaw i sprawiłeś, że zapach zaczął się rozchodzić.
Dewi Morgan
8
@DewiMorgan: Oczywiście można jeszcze bardziej przebudować, aby uogólnić Refrigeratorna IngredientSource, a nawet uogólnić pojęcie „kanapki” na template<typename... Fillings> StackedElementConstruction<Fillings...> make_sandwich(ElementSource&); to się nazywa „programowanie ogólne” i jest dość potężne, ale z pewnością jest o wiele bardziej tajemnicze, niż OP naprawdę chce się teraz dostać. Zachęcamy do otwarcia nowego pytania o odpowiedni poziom abstrakcji dla programów typu sandwich. ;)
Quuxplusone
11
Nie pomyl się, nieuprzywilejowany użytkownik nie powinien mieć dostępu do make_sandwich().
dotancohen,
2
@Dewi - link
Gavin Lock
19
Najpoważniejszym błędem w kodzie jest to, że trzymasz masło orzechowe w lodówce.
Malvolio,
15

Jest to właściwie podręcznikowa definicja sprzężenia , jeden moduł ma zależność, która głęboko wpływa na inny, i który powoduje efekt falowania po zmianie. Pozostałe komentarze i odpowiedzi są poprawne, że jest to poprawa w stosunku do globalnej, ponieważ sprzężenie jest teraz bardziej wyraźne i łatwiejsze dla programisty, niż wywrotowe. To nie znaczy, że nie należy tego naprawiać. Powinieneś być w stanie refaktoryzować, aby usunąć lub zmniejszyć sprzęgło, chociaż jeśli już tam było, może być bolesne.

Karl Bielefeldt
źródło
3
W razie level3()potrzeby jest newParamto z pewnością połączenie, ale w jakiś sposób różne części kodu muszą się ze sobą komunikować. Niekoniecznie nazwałbym parametr funkcji złym sprzężeniem, jeśli funkcja ta korzysta z parametru. Myślę, że problematyczny aspekt łańcucha jest dodatkowe sprzęgło wprowadzony level1()i level2()które nie mają zastosowania do newParamwyjątkiem przekazać ją dalej. Dobra odpowiedź, +1 za połączenie.
null
6
@ null Jeśli tak naprawdę nie mieli do tego celu, mogliby nadrobić wartość zamiast odbierać od swojego rozmówcy.
Random832
3

Chociaż ta odpowiedź nie odpowiada bezpośrednio na twoje pytanie, wydaje mi się, że odmówiłbym udzielenia odpowiedzi, nie wspominając o tym, jak ją poprawić (ponieważ, jak mówisz, może to być anty-wzór). Mam nadzieję, że Ty i inni czytelnicy możecie uzyskać wartość z tego dodatkowego komentarza na temat unikania „danych o włóczęgach” (jak Bob Dalgleish tak chętnie nazwał to dla nas).

Zgadzam się z odpowiedziami, które sugerują zrobienie czegoś więcej, aby uniknąć tego problemu. Jednak innym sposobem na głębokie ograniczenie przekazywania argumentów bez przeskakiwania do „ po prostu przekazania klasy, w której przekazano wiele argumentów! ” Jest refaktoryzacja, aby niektóre etapy procesu przebiegały na wyższym poziomie zamiast na niższym poziomie jeden. Na przykład, oto kilka przed kodem:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   FilterAndReportStuff(stuffs, desiredName);
}

public void FilterAndReportStuff(IEnumerable<Stuff> stuffs, string desiredName) {
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   ReportStuff(stuffs.Filter(filter));
}

public void ReportStuff(IEnumerable<Stuff> stuffs) {
   stuffs.Report();
}

Zauważ, że staje się jeszcze gorzej, im więcej rzeczy trzeba zrobić ReportStuff. Być może będziesz musiał przejść do instancji Reportera, którego chcesz użyć. I wszelkiego rodzaju zależności, które należy przekazać, od funkcji do funkcji zagnieżdżonej.

Moją sugestią jest przeniesienie tego wszystkiego na wyższy poziom, gdzie znajomość kroków wymaga życia w jednej metodzie, zamiast być rozłożonym na łańcuch wywołań metod. Oczywiście w prawdziwym kodzie byłoby to bardziej skomplikowane, ale daje to pomysł:

public void PerformReporting(StuffRepository repo, string desiredName) {
   var stuffs = repo.GetStuff(DateTime.Now());
   var filter = CreateStuffFilter(FilterTypes.Name, desiredName);
   var filteredStuffs = stuffs.Filter(filter)
   filteredStuffs.Report();
}

Zauważ, że duża różnica polega na tym, że nie musisz przepuszczać zależności przez długi łańcuch. Nawet jeśli spłaszczysz nie tylko jeden poziom, ale kilka poziomów w głąb, jeśli te poziomy również osiągną pewne „spłaszczenie”, dzięki czemu proces będzie postrzegany jako seria kroków na tym poziomie, wprowadzisz poprawę.

Chociaż jest to nadal proceduralne i nic nie zostało jeszcze przekształcone w obiekt, jest to dobry krok w kierunku podjęcia decyzji, jaki rodzaj enkapsulacji można osiągnąć, zmieniając coś w klasę. Głęboko powiązane łańcuchy wywołań metod w scenariuszu przed ukrywają szczegóły tego, co faktycznie się dzieje i mogą bardzo utrudniać zrozumienie kodu. Chociaż możesz przesadzić i skończyć na tym, że kod wyższego poziomu wie o rzeczach, których nie powinien, lub stworzyć metodę, która robi zbyt wiele rzeczy, naruszając w ten sposób zasadę pojedynczej odpowiedzialności, ogólnie zauważyłem, że spłaszczanie rzeczy trochę pomaga w jasności i wprowadzaniu stopniowych zmian w kierunku lepszego kodu.

Zauważ, że robiąc to wszystko, powinieneś rozważyć testowalność. Powiązanej metoda połączeń rzeczywiście zrobić testy jednostkowe trudniejsze , ponieważ nie mają dobry punkt wejścia i punkt wyjścia w zespole do plasterka, który chcesz przetestować. Zauważ, że dzięki temu spłaszczeniu, ponieważ twoje metody nie przyjmują już tak wielu zależności, łatwiej je przetestować, nie wymagając tylu próbnych prób!

Niedawno próbowałem dodać testy jednostkowe do klasy (której nie napisałem), która wymagała około 17 zależności, z których wszystkie musiały zostać wyszydzone! Nie udało mi się jeszcze tego wszystkiego wypracować, ale podzieliłem klasę na trzy klasy, z których każda dotyczyła jednego z osobnych rzeczowników, o które chodziło, i zmniejszyłem listę zależności do 12 dla najgorszego i około 8 dla najgorszego najlepszy.

Testowalność zmusi cię do napisania lepszego kodu. Powinieneś pisać testy jednostkowe, ponieważ przekonasz się, że sprawia to, że myślisz o swoim kodzie inaczej i od samego początku będziesz pisać lepszy kod, niezależnie od tego, ile błędów mogłeś mieć przed napisaniem testów jednostkowych.

ErikE
źródło
2

Nie dosłownie łamiesz Prawo Demetera, ale twój problem jest podobny do tego pod pewnymi względami. Ponieważ celem twojego pytania jest znalezienie zasobów, proponuję przeczytać o prawie demetera i zobaczyć, ile z tych porad odnosi się do twojej sytuacji.

karma dla kotów
źródło
1
Trochę słaby w szczegółach, co prawdopodobnie tłumaczy opinie negatywne. Jednak w duchu ta odpowiedź jest dokładnie na miejscu: OP powinien przeczytać o prawie Demetera - jest to odpowiedni termin.
Konrad Rudolph
4
FWIW, nie sądzę, żeby Prawo Demetera (inaczej „najmniejszy przywilej”) było w ogóle istotne. Przypadek OP jest taki, że jego funkcja nie byłaby w stanie wykonać swojej pracy, gdyby nie posiadała danych trampowych (ponieważ potrzebny jest następny facet na stosie wywołań, ponieważ następny facet potrzebuje tego i tak dalej). Najmniejszy przywilej / Prawo Demetera jest istotne tylko wtedy, gdy parametr jest naprawdę nieużywany , aw takim przypadku poprawka jest oczywista: usuń nieużywany parametr!
Quuxplusone
2
Sytuacja tego pytania nie ma dokładnie nic wspólnego z prawem Demetera ... Istnieje powierzchowne podobieństwo w łańcuchu wywołań metod, ale poza tym jest zupełnie inaczej.
Eric King,
@Quuxplusone Możliwe, choć w tym przypadku opis jest dość mylący, ponieważ połączenia łańcuchowe nie mają w tym przypadku sensu: powinny być zagnieżdżone .
Konrad Rudolph
1
Problem jest bardzo podobny do naruszeń LoD, ponieważ zwykłe refaktoryzowanie sugerowane w przypadku naruszeń LoD polega na wprowadzeniu danych trampowych. IMHO to dobry punkt wyjścia do zmniejszenia sprzężenia, ale nie jest wystarczający.
Jørgen Fogh
1

Są przypadki, w których najlepiej (pod względem wydajności, łatwości konserwacji i łatwości implementacji) mieć pewne zmienne jako globalne, a nie narzut związany z ciągłym przekazywaniem wszystkiego (powiedzmy, że masz około 15 zmiennych, które muszą się utrzymywać). Dlatego sensowne jest znalezienie języka programowania, który lepiej obsługuje określanie zakresu (jako prywatne zmienne statyczne C ++), aby złagodzić potencjalny bałagan (przestrzeni nazw i manipulacji). Oczywiście to tylko powszechna wiedza.

Ale podejście określone przez PO jest bardzo przydatne, jeśli wykonuje się programowanie funkcjonalne.

kozner
źródło
0

Nie ma tu żadnego anty-wzorca, ponieważ dzwoniący nie wie o wszystkich tych poziomach poniżej i nie obchodzi go to.

Ktoś dzwoni na wyższy poziom (parametry) i oczekuje, że wyższy poziom wykona swoje zadanie. To, co wyższyLevel robi z parametrami, nie jest sprawą firmy dzwoniącej. wyższy poziom rozwiązuje problem w najlepszy możliwy sposób, w tym przypadku przekazując parametry na poziom 1 (parametry). To absolutnie OK.

Widzisz łańcuch połączeń - ale nie ma łańcucha połączeń. Na górze znajduje się funkcja, która wykonuje swoją pracę najlepiej, jak potrafi. I są inne funkcje. Każda funkcja może zostać zastąpiona w dowolnym momencie.

gnasher729
źródło