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 newParam
poprzednio 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.
Odpowiedzi:
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.
Refaktoryzacja w celu usunięcia zmiennych globalnych jest trudna, a dane trampowe są jedną z metod, a często najtańszą. Ma swoje koszty.
źródło
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ę:
Aby to zrobić, tworzę nową funkcję:
Wtedy moja pierwsza funkcja staje się po prostu:
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ń:
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 .
źródło
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
,playerEyeColor
i 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:
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:
... a
interactWithShopkeeper()
funkcja ma adres sprzedawcy do gracza po imieniu, więc teraz musisz nagle przekazaćplayerName
dane 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śćplayerEyeColor
przez 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).
źródło
foo.method(bar, baz)
imethod(foo, bar, baz)
istnieją inne powody (w tym polimorfizm, enkapsulacja, lokalizacja itp.), Aby preferować to pierwsze.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 .
źródło
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
ale lepszym rozwiązaniem byłoby zastosowanie wstrzyknięcia zależności i napisanie go w ten sposób:
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_sandwich
uzyskać dostęp doRefrigerator
; 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_sandwich
funkcję, przekazującMockRefrigerator
! (Może być konieczne przetestowanie go w ten sposób, ponieważ środowisko testowe może nie mieć dostępu do żadnychPhysicalRefrigerator
s.)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.
źródło
Refrigerator
naIngredientSource
, a nawet uogólnić pojęcie „kanapki” natemplate<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. ;)make_sandwich()
.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.
źródło
level3()
potrzeby jestnewParam
to 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 wprowadzonylevel1()
ilevel2()
które nie mają zastosowania donewParam
wyjątkiem przekazać ją dalej. Dobra odpowiedź, +1 za połączenie.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:
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ł:
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.
źródło
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.
źródło
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.
źródło
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.
źródło