Kiedy dzielę duże metody (lub procedury lub funkcje - to pytanie nie jest specyficzne dla OOP, ale ponieważ pracuję w językach OOP w 99% przypadków, to terminologia, z którą czuję się najlepiej) na wiele małych , Często jestem niezadowolony z wyników. Trudniej jest rozumować te małe metody niż wtedy, gdy były to tylko bloki kodu w dużej, ponieważ gdy je wydobywam, tracę wiele podstawowych założeń, które wynikają z kontekstu wywołującego.
Później, kiedy patrzę na ten kod i widzę poszczególne metody, nie od razu wiem, skąd są one wywoływane, i myślę o nich jako o zwykłych metodach prywatnych, które można wywoływać z dowolnego miejsca w pliku. Wyobraźmy sobie na przykład metodę inicjowania (konstruktora lub inną) podzieloną na szereg małych: w kontekście samej metody wyraźnie wiesz, że stan obiektu jest nadal nieprawidłowy, ale w zwykłej prywatnej metodzie prawdopodobnie wychodzisz z założenia, że obiekt jest już zainicjowany i jest w poprawnym stanie.
Jedynym rozwiązaniem, jakie widziałem w tym przypadku, jest where
klauzula w Haskell, która pozwala zdefiniować małe funkcje, które są używane tylko w funkcji „nadrzędnej”. Zasadniczo wygląda to tak:
len x y = sqrt $ (sq x) + (sq y)
where sq a = a * a
Ale inne języki, których używam, nie mają czegoś takiego - najbliższą rzeczą jest zdefiniowanie lambdy w zasięgu lokalnym, co prawdopodobnie jest jeszcze bardziej mylące.
Moje pytanie brzmi: czy napotykasz to i czy w ogóle widzisz, że to problem? Jeśli tak, to jak zazwyczaj to rozwiązujesz, szczególnie w „głównych” językach OOP, takich jak Java / C # / C ++?
Edytuj o duplikatach: jak zauważyli inni, są już pytania dotyczące metod podziału i drobne pytania, które są jednowierszowe. Przeczytałem je i nie dyskutują na temat podstawowych założeń, które można wyprowadzić z kontekstu dzwoniącego (na przykład powyżej, obiekt jest inicjowany). To jest sens mojego pytania i dlatego moje pytanie jest inne.
Aktualizacja: Jeśli podążyłeś za tym pytaniem i dyskusją poniżej, możesz polubić ten artykuł Johna Carmacka na ten temat , w szczególności:
Oprócz wiedzy o wykonywanym kodzie, funkcje wstawiania mają tę zaletę, że nie umożliwiają wywołania funkcji z innych miejsc. Brzmi to absurdalnie, ale ma to sens. Ponieważ baza kodów rośnie z biegiem lat, pojawi się wiele okazji, aby skorzystać ze skrótu i po prostu wywołać funkcję, która wykonuje tylko pracę, którą Twoim zdaniem należy wykonać. Może istnieć funkcja FullUpdate (), która wywołuje PartialUpdateA () i PartialUpdateB (), ale w niektórych szczególnych przypadkach możesz zdać sobie sprawę (lub pomyśleć), że potrzebujesz tylko PartialUpdateB () i jesteś wydajny, unikając innych praca. Wynika z tego mnóstwo błędów. Większość błędów wynika z faktu, że stan wykonania nie jest dokładnie taki, jak myślisz.
Odpowiedzi:
Twoje obawy są uzasadnione. Jest inne rozwiązanie.
Zrób krok wstecz. Jaki jest zasadniczo cel metody? Metody wykonują tylko jedną z dwóch rzeczy:
Lub niestety oba. Staram się unikać metod, które robią obie rzeczy, ale wiele robi. Powiedzmy, że wytworzony efekt lub wytworzona wartość jest „wynikiem” metody.
Zauważ, że metody są wywoływane w „kontekście”. Co to za kontekst?
Zasadniczo zwracasz uwagę: poprawność wyniku metody zależy od kontekstu, w którym jest ona wywoływana .
Nazywamy te warunki wymagane przed organem metoda zaczyna do sposobu wytwarzania prawidłowy wynik jego warunki wstępne , a my nazywamy warunków, które zostaną wytworzone po ciało metoda zwraca jego postconditions .
Zasadniczo, na co zwracasz uwagę: kiedy wyodrębniam blok kodu do jego własnej metody, tracę informacje kontekstowe na temat warunków wstępnych i końcowych .
Rozwiązaniem tego problemu jest wyraźne określenie warunków wstępnych i dodatkowych w programie . Na przykład w języku C # można użyć
Debug.Assert
lub Kod umów, aby wyrazić warunki wstępne i dodatkowe.Na przykład: Kiedyś pracowałem na kompilatorze, który przechodził przez kilka „etapów” kompilacji. Najpierw kod byłby leksykowany, następnie analizowany, następnie typy byłyby rozwiązywane, następnie sprawdzane były hierarchie dziedziczenia pod kątem cykli i tak dalej. Każdy fragment kodu był bardzo wrażliwy na jego kontekst; na przykład katastrofalne byłoby pytanie „czy można przekształcić ten typ w ten typ?” jeśli wykres typów bazowych nie był jeszcze znany jako acykliczny! Dlatego każdy fragment kodu wyraźnie udokumentował swoje warunki wstępne. Pragniemy
assert
w sposób, który sprawdził dla typu wymienialności że już przeszły „typy bazowe Acylic” czek, a potem stało się jasne dla czytelnika, gdy sposób można nazwać i gdzie nie można było nazywane.Oczywiście istnieje wiele sposobów, w jaki dobry projekt metody łagodzi zidentyfikowany problem:
źródło
string
i zapisuje ją w bazie danych, istnieje ryzyko wstrzyknięcia SQL, jeśli zapomnisz ją wyczyścić. Z drugiej strony, jeśli twoja funkcja wymaga aSanitisedString
, a jedynym sposobem na uzyskanie aSantisiedString
jest wywołanieSanitise
, to wykluczyłeś błędy iniekcji SQL ze względu na budowę. Coraz częściej szukam sposobów, aby kompilator odrzucił niepoprawny kod.Często to widzę i zgadzam się, że to problem. Zwykle rozwiązuję to, tworząc obiekt metody : nową wyspecjalizowaną klasę, której członkami są zmienne lokalne z oryginalnej, zbyt dużej metody.
Nowa klasa ma zwykle nazwę „Eksporter” lub „Tabulacja” i otrzymuje wszelkie informacje niezbędne do wykonania tego konkretnego zadania z szerszego kontekstu. Następnie można zdefiniować jeszcze mniejsze fragmenty kodu pomocnika, które nie są narażone na użycie do niczego poza tabelowaniem lub eksportowaniem.
źródło
Wiele języków pozwala zagnieżdżać funkcje takie jak Haskell. Java / C # / C ++ są w rzeczywistości względnymi wartościami odstającymi. Niestety są tak popularne, że ludzie zaczynają myśleć: „To musi być zły pomysł, w przeciwnym razie mój ulubiony język„ głównego nurtu ”na to pozwala”.
Java / C # / C ++ zasadniczo uważa, że klasa powinna być jedyną grupą metod, jakiej kiedykolwiek potrzebujesz. Jeśli masz tak wiele metod, że nie możesz określić ich kontekstów, możesz zastosować dwa ogólne podejścia: posortuj je według kontekstu lub podziel je według kontekstu.
Sortowanie według kontekstu to jedno zalecenie sformułowane w Czystym kodzie , w którym autor opisuje wzorzec „akapitów DO”. Zasadniczo polega to na umieszczeniu funkcji pomocniczych bezpośrednio po funkcji, która je wywołuje, dzięki czemu można je czytać jak akapity w artykule w gazecie, a więcej szczegółów można uzyskać w miarę czytania. Myślę, że w swoich filmach nawet je wcina.
Drugim podejściem jest podzielenie klas. Nie można tego posunąć zbyt daleko, ponieważ denerwująca jest potrzeba tworzenia instancji obiektów, zanim będzie można wywołać na nich dowolną metodę, oraz nieodłączne problemy z decyzją, która z kilku małych klas powinna być właścicielem każdej części danych. Jeśli jednak zidentyfikowałeś już kilka metod, które tak naprawdę pasują tylko do jednego kontekstu, prawdopodobnie są dobrym kandydatem do rozważenia umieszczenia ich we własnej klasie. Na przykład, złożoną inicjalizację można wykonać w kreacyjnym wzorcu, takim jak builder.
źródło
Myślę, że w większości przypadków odpowiedzią jest kontekst. Jako programista piszący kod powinieneś założyć, że Twój kod zostanie w przyszłości zmieniony. Klasa może być zintegrowana z inną klasą, może zastąpić wewnętrzny algorytm lub zostać podzielona na kilka klas w celu utworzenia abstrakcji. Są to rzeczy, które początkujący programiści zwykle nie biorą pod uwagę, co powoduje konieczność niepotrzebnych obejść lub późniejszych przeglądów.
Wyodrębnianie metod jest dobre, ale do pewnego stopnia. Zawsze próbuję zadać sobie następujące pytania podczas inspekcji lub przed napisaniem kodu:
W każdym razie zawsze myśl o pojedynczej odpowiedzialności. Klasa powinna ponosić jedną odpowiedzialność, jej funkcje powinny służyć jednej stałej usłudze, a jeśli wykonują wiele akcji, akcje te powinny mieć swoje własne funkcje, więc łatwo je rozróżnić lub zmienić później.
źródło
Nie zdawałem sobie sprawy z tego, jak duży to był problem, dopóki nie przyjąłem ECS, który zachęcał do większych, pętlowych funkcji systemowych (przy czym jedyne systemy mają funkcje) i zależności płynących w kierunku surowych danych , a nie abstrakcji.
To, ku mojemu zaskoczeniu, dało bazę kodów o wiele łatwiejszą do uzasadnienia i utrzymania w porównaniu do baz kodów, w których pracowałem w przeszłości, gdzie podczas debugowania trzeba było prześledzić wszystkie rodzaje drobnych małych funkcji, często poprzez abstrakcyjne wywołania funkcji przez czyste interfejsy prowadzące do tego, kto wie, dokąd się do niego nie prześledzisz, tylko po to, by spawnować pewną kaskadę zdarzeń, które prowadzą do miejsc, o których nie myślałeś, że kod powinien kiedykolwiek prowadzić.
W przeciwieństwie do Johna Carmacka, moim największym problemem z tymi bazami kodów nie była wydajność, ponieważ nigdy nie miałem tak bardzo ciasnego zapotrzebowania na silniki gier AAA na opóźnienia i większość naszych problemów z wydajnością dotyczyła bardziej przepustowości. Oczywiście możesz również zacząć utrudniać optymalizację hotspotów, gdy pracujesz w węższych i węższych ramach funkcji i klas Teeniera i Teeniera bez przeszkadzania tej strukturze (wymagając, abyś stopił wszystkie te małe elementy z powrotem na coś większego, zanim będzie można zacząć skutecznie sobie z tym radzić).
Jednak największym problemem dla mnie było to, że pomimo wszystkich testów nie mogłem w sposób pewny uzasadnić ogólnej poprawności systemu. Miałem zbyt wiele do zrozumienia i zrozumienia, ponieważ ten rodzaj systemu nie pozwolił ci o tym myśleć bez uwzględnienia wszystkich tych drobnych szczegółów i niekończących się interakcji między drobnymi funkcjami i obiektami, które działały wszędzie. Było zbyt wiele „co jeśli?”, Zbyt wiele rzeczy, które należało wywołać we właściwym czasie, zbyt wiele pytań o to, co by się stało, gdyby nazwano ich niewłaściwym czasem (które zaczynają się wznosić do punktu paranoi, kiedy mieć jedno zdarzenie wyzwalające inne zdarzenie wywołujące inne, prowadzące do różnego rodzaju nieprzewidywalnych miejsc) itp.
Teraz podoba mi się mój duży tyłek, 80-liniowy, tu i tam, o ile nadal wykonują pojedynczą i wyraźną odpowiedzialność i nie mają 8 poziomów zagnieżdżonych bloków. Powodują wrażenie, że w systemie jest mniej rzeczy do przetestowania i zrozumienia, nawet jeśli mniejsze, pokrojone w kostkę wersje tych większych funkcji były tylko prywatnymi szczegółami implementacji, których nikt inny nie mógł wywołać ... wciąż, jakoś, wydaje się, że w całym systemie dzieje się mniej interakcji. Lubię nawet bardzo skromne powielanie kodu, o ile nie jest to skomplikowana logika (powiedzmy tylko 2-3 wiersze kodu), jeśli oznacza to mniej funkcji. Podoba mi się rozumowanie Carmacka dotyczące wbudowania, które uniemożliwia wywołanie tej funkcji w innym miejscu pliku źródłowego. Tam'
Prostota nie zawsze zmniejsza złożoność na poziomie dużego obrazu, jeśli opcja jest pomiędzy jedną mięsistą funkcją a 12 bardzo prostymi, które wywołują się nawzajem za pomocą złożonego wykresu zależności. Pod koniec dnia często musisz zastanowić się nad tym, co dzieje się poza funkcją, musisz dowiedzieć się, do czego ostatecznie przyczyniają się te funkcje, i trudniej jest zobaczyć ten duży obraz, jeśli musisz wywnioskować z tego najmniejsze puzzle.
Oczywiście bardzo dobrze przetestowany kod typu biblioteki ogólnego przeznaczenia może zostać zwolniony z tej reguły, ponieważ taki kod ogólnego przeznaczenia często działa i sam sobie radzi. Jest również mniejszy w porównaniu z kodem nieco bliższym do domeny twojej aplikacji (tysiące wierszy kodu, a nie milionów) i jest tak szeroko stosowany, że zaczyna być częścią codziennego słownictwa. Ale z czymś bardziej specyficznym dla twojej aplikacji, w której niezmienniki systemowe, które musisz utrzymywać, wykraczają daleko poza jedną funkcję lub klasę, wydaje mi się, że to pomaga mieć pełniejsze funkcje z jakiegokolwiek powodu. O wiele łatwiej jest mi pracować z większymi puzzlami, próbując dowiedzieć się, co się dzieje z dużym obrazem.
źródło
Nie sądzę, że to duży problem, ale zgadzam się, że to kłopotliwe. Zwykle umieszczam pomocnika bezpośrednio za jego beneficjentem i dodam przyrostek „Pomocnik”. To plus specyfikator
private
dostępu powinien jasno określić swoją rolę. Jeśli istnieje jakiś niezmiennik, który nie działa, gdy wywoływany jest pomocnik, dodaję komentarz w pomocniku.To rozwiązanie ma tę niefortunną wadę, że nie uchwyca zakresu funkcji, którą pomaga. Idealnie twoje funkcje są małe, więc mam nadzieję, że nie spowoduje to zbyt wielu parametrów. Zwykle rozwiązujesz ten problem, definiując nowe struktury lub klasy w celu powiązania parametrów, ale wymagana do tego ilość płyty kotłowej może być z łatwością dłuższa niż sam pomocnik, a następnie wracasz tam, gdzie zacząłeś bez oczywistego sposobu skojarzenia struct z funkcją.
Wspomniałeś już o innym rozwiązaniu - zdefiniuj pomocnika wewnątrz głównej funkcji. W niektórych językach może to być dość nietypowy idiom, ale nie sądzę, że byłoby to mylące (chyba że twoi rówieśnicy są zdezorientowani przez lambdas w ogóle). Działa to tylko wtedy, gdy można łatwo zdefiniować funkcje lub obiekty podobne do funkcji. Nie próbowałbym tego na przykład w Javie 7, ponieważ anonimowa klasa wymaga wprowadzenia 2 poziomów zagnieżdżenia nawet dla najmniejszej „funkcji”. To jest tak blisko
let
lubwhere
klauzuli, jak można dostać; możesz odwoływać się do zmiennych lokalnych przed definicją, a pomocnika nie można używać poza tym zakresem.źródło