Funkcje, które wywołują tylko inne funkcje. Czy to dobra praktyka?

13

Obecnie pracuję nad zestawem raportów, które zawierają wiele różnych sekcji (wszystkie wymagają innego formatowania) i próbuję znaleźć najlepszy sposób na ustrukturyzowanie mojego kodu. Podobne raporty, które zrobiliśmy w przeszłości, mają bardzo duże (ponad 200 linii) funkcje, które wykonują wszystkie operacje na danych i formatują raport, dzięki czemu przepływ pracy wygląda mniej więcej tak:

DataTable reportTable = new DataTable();
void RunReport()
{
    reportTable = DataClass.getReportData();
    largeReportProcessingFunction();
    outputReportToUser();
}

Chciałbym móc podzielić te duże funkcje na mniejsze części, ale obawiam się, że skończę z dziesiątkami funkcji, których nie można ponownie użyć, i podobną funkcją „rób wszystko tutaj”, której jedynym zadaniem jest wywołać wszystkie te mniejsze funkcje, takie jak:

void largeReportProcessingFunction()
{
    processSection1HeaderData();
    calculateSection1HeaderAverages();
    formatSection1HeaderDisplay();
    processSection1SummaryTableData();
    calculateSection1SummaryTableTotalRow();
    formatSection1SummaryTableDisplay();
    processSection1FooterData();
    getSection1FooterSummaryTotals();
    formatSection1FooterDisplay();

    processSection2HeaderData();
    calculateSection1HeaderAverages();
    formatSection1HeaderDisplay();
    calculateSection1HeaderAverages();
    ...
}

Lub jeśli pójdziemy o krok dalej:

void largeReportProcessingFunction()
{
    callAllSection1Functions();

    callAllSection2Functions();

    callAllSection3Functions();
    ...        
}

Czy to naprawdę lepsze rozwiązanie? Z organizacyjnego punktu widzenia przypuszczam, że tak (tzn. Wszystko jest o wiele lepiej zorganizowane, niż mogłoby być inaczej), ale nie jestem pewien co do czytelności kodu (potencjalnie duże łańcuchy funkcji, które wywołują tylko inne funkcje).

Myśli?

Eric C.
źródło
Na koniec dnia ... czy jest bardziej czytelny? Tak, więc jest to lepsze rozwiązanie.
sylvanaar

Odpowiedzi:

18

Jest to powszechnie określane jako rozkład funkcjonalny i ogólnie jest dobrym rozwiązaniem, jeśli jest wykonane poprawnie.

Ponadto implementacja funkcji powinna mieścić się w obrębie jednego poziomu abstrakcji. Jeśli weźmiesz largeReportProcessingFunction, to jego rolą jest określenie, jakie różne kroki przetwarzania należy podjąć w jakiej kolejności. Implementacja każdego z tych kroków znajduje się na poniższej warstwie abstrakcji i largeReportProcessingFunctionnie powinna zależeć od niego bezpośrednio.

Pamiętaj jednak, że to zły wybór nazywania:

void largeReportProcessingFunction() {
    callAllSection1Functions();
    callAllSection2Functions();
    callAllSection3Functions();
    ...        
}

Widzisz, callAllSection1Functionsto nazwa, która w rzeczywistości nie zapewnia abstrakcji, ponieważ tak naprawdę nie mówi, co robi, ale jak to robi. processSection1Zamiast tego należy go wywołać lub cokolwiek innego, co ma znaczenie w kontekście.

back2dos
źródło
6
Zasadniczo zgadzam się z tą odpowiedzią, z tym wyjątkiem, że nie widzę, jak „processSection1” jest znaczącą nazwą. Jest to praktycznie równoważne „callAllSection1Functions”.
Eric King
2
@EricKing: callAllSection1Functionswycieki szczegóły dotyczące implementacji. Z zewnętrznego punktu widzenia nie ma znaczenia, czy processSection1odracza swoją pracę do „wszystkich funkcji sekcji 1”, czy też robi to bezpośrednio, czy też używa pixie-pyłu do przywołania jednorożca, który wykona rzeczywistą pracę. Wszystko to naprawdę sprawa jest taka, że po wywołaniu processSection1, sekcja1 można uznać przetwarzane . Zgadzam się, że przetwarzanie jest ogólną nazwą, ale jeśli masz wysoką spójność (do której zawsze powinieneś dążyć), twój kontekst jest zwykle wystarczająco wąski, aby go jednoznacznie określić.
back2dos
2
Chodziło mi tylko o to, że metody zwane „processXXX” są prawie zawsze okropnymi, bezużytecznymi nazwami. Wszystkie metody „przetwarzają” rzeczy, więc nazywanie go „processXXX” jest tak samo przydatne, jak nazywanie go „doSomethingWithXXX” lub „manipulujXXX” i tym podobne. Prawie zawsze istnieje lepsza nazwa dla metod o nazwie „processXXX”. Z praktycznego punktu widzenia jest tak przydatny (bezużyteczny) jak „callAllSection1Functions”.
Eric King,
4

Powiedziałeś, że używasz C #, więc zastanawiam się, czy możesz zbudować strukturalną hierarchię klas, wykorzystując fakt, że używasz języka obiektowego? Na przykład, jeśli sensowne jest podzielenie raportu na sekcje, należy mieć Sectionklasę i rozszerzyć ją o specyficzne wymagania klienta.

Może mieć sens myśleć o tym, jakbyś tworzył nieinteraktywny GUI. Pomyśl o obiektach, które możesz stworzyć tak, jakby były różnymi komponentami GUI. Zadaj sobie pytanie, jak wyglądałby podstawowy raport? A może skomplikowany? Gdzie powinny być punkty rozszerzenia?

Jeremy Heiler
źródło
3

To zależy. Jeśli wszystkie funkcje, które tworzysz, są naprawdę pojedynczą jednostką pracy, to dobrze, jeśli są to tylko wiersze 1-15, 16-30, 31-45 ich funkcji wywoływania, która jest zła. Długie funkcje nie są złe, jeśli robią jedną rzecz, jeśli masz 20 filtrów do raportu, a funkcja sprawdzania poprawności, która sprawdza, że ​​wszystkie dwadzieścia będzie długa. W porządku, dzielenie go według filtrów lub grup filtrów to tylko dodatkowa złożoność bez rzeczywistych korzyści.

Posiadanie wielu prywatnych funkcji utrudnia również automatyczne testowanie jednostek, więc niektórzy z tego powodu będą starali się tego uniknąć, ale moim zdaniem jest to raczej słabe rozumowanie, ponieważ ma tendencję do tworzenia większego, bardziej złożonego projektu do dostosowania do testów.

Ryathal
źródło
3
Z mojego doświadczenia wynika, że ​​długie funkcje złe.
Bernard
Gdyby twój przykład był w języku OO, stworzyłbym klasę filtrów bazowych, a każdy z 20 filtrów byłby w różnych klasach pochodnych. Instrukcja switch zostałaby zastąpiona wywołaniem tworzenia, aby uzyskać odpowiedni filtr. Każda funkcja robi jedną rzecz i wszystkie są małe.
DXM
3

Ponieważ używasz C #, spróbuj pomyśleć więcej w obiektach. Wygląda na to, że nadal kodujesz proceduralnie, mając zmienne klasy „globalnej”, od których zależą kolejne funkcje.

Przełamanie logiki w oddzielnych funkcjach jest zdecydowanie lepsze niż posiadanie całej logiki w jednej funkcji. Założę się, że możesz pójść dalej, oddzielając dane, na których działają funkcje, dzieląc je na kilka obiektów.

Zastanów się nad klasą ReportBuilder, w której możesz przekazać dane raportu. Jeśli możesz podzielić ReportBuilder na osobne klasy, zrób to również.

koder
źródło
2
Procedury pełnią idealnie dobrą funkcję.
DeadMG
1

Tak.

Jeśli masz segmenty kodu, które muszą być używane w wielu miejscach, własna funkcja. W przeciwnym razie, jeśli musisz zmienić funkcjonalność, będziesz musiał dokonać zmiany w wielu miejscach. Zwiększa to nie tylko pracę, ale także zwiększa ryzyko pojawienia się błędów, gdy kod zmienia się w jednym miejscu, ale nie w innym, lub w miejscu, w którym wprowadza się znak „ale”, ale nie w drugim.

Pomaga również uczynić metodę bardziej czytelną, jeśli zostaną użyte opisowe nazwy metod.

SoylentGray
źródło
1

Fakt, że używasz numeracji do rozróżniania funkcji, sugeruje, że istnieją podstawowe problemy z powielaniem lub zbyt dużą klasą. Podział dużej funkcji na wiele mniejszych funkcji może być przydatnym krokiem w refaktoryzacji duplikacji, ale nie powinien być ostatnim. Na przykład tworzysz dwie funkcje:

processSection1HeaderData();
processSection2HeaderData();

Czy w kodzie używanym do przetwarzania nagłówków sekcji nie ma żadnych podobieństw? Czy możesz wyodrębnić wspólną funkcję dla obu z nich poprzez sparametryzowanie różnic?

Garrett Hall
źródło
W rzeczywistości nie jest to kod produkcyjny, więc nazwy funkcji są tylko przykładami (w produkcji nazwy funkcji są bardziej opisowe). Zasadniczo nie, nie ma podobieństw między różnymi sekcjami (chociaż gdy są podobieństwa, staram się jak najlepiej wykorzystywać kod).
Eric C.
1

Podział funkcji na logiczne podfunkcje jest pomocny, nawet jeśli podfunkcje nie są ponownie używane - dzieli je na krótkie, łatwe do zrozumienia bloki. Ale musi to być zrobione przez jednostki logiczne, a nie tylko n # linii. To, jak powinieneś to rozbić, będzie zależeć od twojego kodu, wspólne tematy to pętle, warunki, operacje na tym samym obiekcie; w zasadzie wszystko, co można opisać jako zadanie dyskretne.

Tak, tak, to dobry styl - może to zabrzmieć dziwnie, ale biorąc pod uwagę twoje ograniczone wykorzystanie, polecam dokładnie przemyśleć PRÓBĘ ponownego użycia. Upewnij się, że użycie jest naprawdę identyczne, a nie tylko przypadkowo takie samo. Próba obsłużenia wszystkich przypadków w tym samym bloku jest często sposobem, w jaki kończysz się z dużą niezgrabną funkcją w pierwszym miejscu.

jmoreno
źródło
0

Myślę, że to głównie osobiste preferencje. Jeśli twoje funkcje miałyby być ponownie użyte (i czy jesteś pewien, że nie możesz uczynić ich bardziej ogólnymi i wielokrotnego użytku?) Zdecydowanie powiedziałbym, że je rozdzielisz. Słyszałem również, że dobrą zasadą jest, aby żadna funkcja ani metoda nie zawierała więcej niż 2-3 ekrany kodu. Jeśli więc twoja duża funkcja będzie się powtarzać, kod może być bardziej czytelny, aby ją podzielić.

Nie wspominasz, jakiej technologii używasz do tworzenia tych raportów. Wygląda na to, że używasz C #. Czy to jest poprawne? Jeśli tak, możesz również rozważyć dyrektywę #region jako alternatywę, jeśli nie chcesz dzielić funkcji na mniejsze.

Joshua Carmody
źródło
Tak, pracuję w C #. Możliwe, że będę mógł ponownie użyć niektórych funkcji, ale w przypadku wielu z tych raportów różne sekcje mają bardzo różne dane i układ (wymaganie klienta).
Eric C.
1
+1 z wyjątkiem sugerowanych regionów. Nie użyłbym regionów.
Bernard
@Bernard - jakiś konkretny powód? Zakładam, że pytający rozważyłby użycie #regions tylko, jeśli już wykluczył podział funkcji.
Joshua Carmody
2
Regiony mają tendencję do ukrywania kodu i mogą być nadużywane (tj. Regiony zagnieżdżone). Lubię widzieć cały kod na raz w pliku kodu. Jeśli muszę wizualnie oddzielić kod w pliku, używam linii ukośników.
Bernard
2
Regiony są zwykle znakiem, że kod wymaga refaktoryzacji
koder