Czy długie funkcje są dopuszczalne, jeśli mają wewnętrzną strukturę?

23

Podczas pracy ze skomplikowanymi algorytmami w językach z obsługą funkcji zagnieżdżonych (takich jak Python i D) często piszę ogromne funkcje (ponieważ algorytm jest skomplikowany), ale łagodzę to, używając funkcji zagnieżdżonych do struktury skomplikowanego kodu. Czy ogromne (ponad 100 linii) funkcje są nadal uważane za złe, nawet jeśli mają wewnętrzną strukturę dzięki zastosowaniu funkcji zagnieżdżonych?

Edycja: Dla tych, którzy nie znają Python lub D, funkcje zagnieżdżone w tych językach umożliwiają również dostęp do zewnętrznego zakresu funkcji. W D ten dostęp umożliwia mutację zmiennych w zakresie zewnętrznym. W Pythonie pozwala tylko na czytanie. W D można jawnie wyłączyć dostęp do zakresu zewnętrznego w funkcji zagnieżdżonej, deklarując go static.

dsimcha
źródło
5
Regularnie piszę ponad 400 funkcji / metod linii. Muszę gdzieś umieścić tę instrukcję zmiany skrzynki na 500 :)
Callum Rogers
7
@Callum Rogers: W Pythonie zamiast 500 instrukcji przełączania wielkości liter można użyć słownika / mapowania wyszukiwania. Sądzę, że są lepsi niż 500-to-tak-twierdzenie o zamianie. Nadal potrzebujesz 500 wierszy definicji słowników (alternatywnie w Pythonie możesz użyć refleksji, aby je dynamicznie wyliczyć), ale definicja słownika to dane, a nie kod; a definicje dużych danych są znacznie mniej skrupulatne niż definicje dużych funkcji. Ponadto dane są bardziej niezawodne niż kod.
Lie Ryan,
Kulę się za każdym razem, gdy widzę funkcje z dużą ilością LOC, zastanawiam się, jaki jest cel modułowości. Łatwiej jest podążać za logiką i debugować z mniejszymi funkcjami.
Aditya P
Starożytny i znany język Pascal umożliwia również dostęp do zewnętrznego zakresu, podobnie jak rozszerzenie funkcji zagnieżdżonych w GNU C. (Języki te pozwalają jednak tylko na fungusy w dół: to znaczy argumenty, które są funkcjami o zasięgu, mogą być tylko przekazane, a nie zwrócone, co wymagałoby pełnego wsparcia leksykalnego zamknięcia).
Kaz
Dobrym przykładem funkcji, które wydają się być długie, są kombinatory napisane podczas programowania reaktywnego. Łatwo osiągają trzydzieści linii, ale podwoiłyby się, gdyby się rozdzieliły, ponieważ tracisz zamknięcia.
Craig Gidney

Odpowiedzi:

21

Zawsze pamiętaj o regule, funkcja robi jedną rzecz i robi to dobrze! Jeśli możesz to zrobić, unikaj zagnieżdżonych funkcji.

Utrudnia to czytelność i testowanie.

Bryan Harrington
źródło
9
Zawsze nienawidziłem tej reguły, ponieważ funkcja, która wykonuje „jedną rzecz”, gdy oglądana jest na wysokim poziomie abstrakcji, może robić „wiele rzeczy”, gdy oglądana jest na niższym poziomie. W przypadku nieprawidłowego zastosowania zasada „jedna rzecz” może prowadzić do nadmiernie drobnoziarnistych interfejsów API.
dsimcha
1
@dsimcha: Jakiej reguły nie można niewłaściwie zastosować i prowadzić do problemów? Kiedy ktoś stosuje „reguły” / frazesy poza punkt, w którym są przydatne, po prostu przywołaj inną: wszystkie uogólnienia są fałszywe.
2
@Roger: Uzasadniona skarga, z tą różnicą, że IMHO zasada jest często niewłaściwie stosowana w praktyce, aby uzasadnić nadmiernie drobnoziarniste, nadindywidualizowane interfejsy API. Ma to konkretne koszty, ponieważ interfejs API staje się trudniejszy i bardziej szczegółowy w prostych, typowych przypadkach użycia.
dsimcha
2
„Tak, zasada jest niewłaściwie zastosowana, ale zasada jest niewłaściwie zastosowana!”? : P
1
Moja funkcja robi jedną rzecz i robi to bardzo dobrze: mianowicie zajmuje 27 screenów. :)
Kaz
12

Niektórzy twierdzą, że krótkie funkcje mogą być bardziej podatne na błędy niż długie .

Card and Glass (1990) wskazują, że złożoność projektu naprawdę obejmuje dwa aspekty: złożoność każdego komponentu i złożoność relacji między komponentami.

Osobiście odkryłem, że dobrze skomentowany kod liniowy jest łatwiejszy do naśladowania (zwłaszcza gdy nie byłeś tym, który go pierwotnie napisał), niż gdy jest podzielony na wiele funkcji, które nigdy nie są używane gdzie indziej. Ale to naprawdę zależy od sytuacji.

Myślę, że głównym problemem jest to, że kiedy dzielisz blok kodu, zamieniasz jeden rodzaj złożoności na inny. Prawdopodobnie jest gdzieś pośrodku.

Jonathan Tran
źródło
Czy przeczytałeś „Wyczyść kod”? Omówiono to szczegółowo. amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/…
Martin Wickman
9

Idealnie byłoby, gdyby cała funkcja była widoczna bez konieczności przewijania. Czasami nie jest to możliwe. Ale jeśli uda ci się go rozbić na części, znacznie ułatwi to czytanie kodu.

Wiem, że jak tylko popycham Page Up / Down lub przechodzę do innej sekcji kodu, pamiętam tylko 7 +/- 2 rzeczy z poprzedniej strony. I niestety, niektóre z tych lokalizacji zostaną wykorzystane podczas czytania nowego kodu.

Zawsze lubię myśleć o mojej pamięci krótkotrwałej jak o rejestrach komputerowych (CISC, a nie RISC). Jeśli masz całą funkcję na tej samej stronie, możesz przejść do pamięci podręcznej, aby uzyskać wymagane informacje z innej sekcji programu. Jeśli cała funkcja nie mieści się na stronie, byłoby to równoznaczne z wypychaniem pamięci na dysk po każdej operacji.

jsternberg
źródło
3
Wystarczy wydrukować go na drukarce matrycowej - patrz 10 metrów kodu naraz :)
Lub uzyskaj monitor o wyższej rozdzielczości
frogstarr78
I używaj go w orientacji pionowej.
Calmarius
4

Dlaczego warto korzystać z funkcji zagnieżdżonych zamiast zwykłych funkcji zewnętrznych?

Nawet jeśli funkcje zewnętrzne były kiedykolwiek używane tylko w jednej, dawniej dużej funkcji, nadal sprawia, że ​​cały bałagan jest łatwiejszy do odczytania:

DoThing(int x){
    x += ;1
    int y = FirstThing(x);
    x = SecondThing(x, y);
    while(spoo > fleem){
        x = ThirdThing(x+y);
    }
}

FirstThing(int x){...}
SecondThing(int x, int y){...}
ThirdThing(int z){...}
Fishtoaster
źródło
2
Dwa możliwe powody: zaśmiecenie przestrzeni nazw i to, co nazywam „bliskością leksykalną”. Mogą to być dobre lub złe powody, w zależności od języka.
Frank Shearar,
Jednak funkcje zagnieżdżone mogą być mniej wydajne, ponieważ muszą obsługiwać funkcje spadkowe i być może w pełni zamknięte leksykalne. Jeśli język jest słabo zoptymalizowany, może wykonać dodatkową pracę, aby stworzyć dynamiczne środowisko do przekazywania funkcji potomnych.
Kaz
3

W tej chwili nie mam przed sobą książki (zacytować), ale według Code Complete „sweetspot” dla długości funkcji wynosił około 25-50 linii kodu według jego badań.

Są chwile, w których dobrze jest mieć długie funkcje:

  • Gdy cyklomatyczność złożoności funkcji jest niska. Twoi koledzy programiści mogą być nieco sfrustrowani, jeśli będą musieli spojrzeć na funkcję zawierającą gigantyczną instrukcję if i instrukcję else, jeśli nie jest wyświetlana na ekranie w tym samym czasie.

Czasy, w których długie funkcje nie są w porządku:

  • Masz funkcję z głęboko zagnieżdżonymi warunkami warunkowymi. Zrób coś innym swoim czytelnikom kodów, popraw czytelność, rozkładając funkcję. Funkcja informuje czytelnika, że ​​„To blok kodu, który robi jedną rzecz”. Zadaj sobie również pytanie, czy długość funkcji wskazuje, że robi ona zbyt wiele i należy ją rozdzielić na inną klasę.

Najważniejsze jest to, że łatwość konserwacji powinna być jednym z najwyższych priorytetów na liście. Jeśli inny programista nie może spojrzeć na twój kod i uzyskać „sedna” tego, co robi kod w czasie krótszym niż 5 sekund, Twój kod nie zapewnia wystarczającej liczby „metadanych”, aby powiedzieć, co robi. Inni deweloperzy powinni być w stanie powiedzieć, co robi Twoja klasa, patrząc na przeglądarkę obiektów w wybranym IDE zamiast czytać ponad 100 linii kodu.

Mniejsze funkcje mają następujące zalety:

  • Przenośność: O wiele łatwiej jest przenosić funkcjonalność (albo w klasie po refaktoryzacji do innej)
  • Debugowanie: Kiedy patrzysz na stacktrace, znacznie szybciej jest zlokalizować błąd, jeśli patrzysz na funkcję z 25 liniami kodu zamiast 100.
  • Czytelność - nazwa funkcji mówi, co robi cały blok kodu. Dev w twoim zespole może nie chcieć czytać tego bloku, jeśli nie pracuje z nim. Ponadto w większości współczesnych IDE inny programista może lepiej zrozumieć, co robi twoja klasa, czytając nazwy funkcji w przeglądarce obiektów.
  • Nawigacja - większość IDE pozwala wyszukiwać według nazw funkcji. Ponadto większość współczesnych IDE ma możliwość przeglądania źródła funkcji w innym oknie, co pozwala innym deweloperom spojrzeć na twoją długą funkcję na 2 ekranach (jeśli na wielu monitorach) zamiast przewijać je.

I tak dalej.....

Korbin
źródło
2

Odpowiedź brzmi: to zależy, jednak prawdopodobnie powinieneś zmienić to w klasę.

Lie Ryan
źródło
Chyba że nie piszesz w OOPL, w takim razie może nie :)
Frank Shearar
dsimcha wspomniał, że używają D i Python.
frogstarr78
Zajęcia są zbyt często kultywowane dla osób, które nigdy nie słyszały o takich rzeczach jak leksykalne zamknięcia.
Kaz
2

Nie lubię większości zagnieżdżonych funkcji. Jagnięta należą do tej kategorii, ale zwykle mnie nie oflagują, chyba że mają więcej niż 30-40 znaków.

Podstawowym powodem jest to, że staje się wysoce lokalnie gęstą funkcją z wewnętrzną rekurencją semantyczną, co oznacza , że trudno mi się owijać wokół mózgu, a po prostu łatwiej jest wypchnąć niektóre rzeczy do funkcji pomocniczej, która nie zaśmieca przestrzeni kodu .

Uważam, że funkcja powinna robić swoje. Robienie innych rzeczy jest tym, co robią inne funkcje. Jeśli więc masz 200-liniową funkcję Robi swoje rzeczy i wszystko płynie, to jest OK.

Paul Nathan
źródło
Czasami musisz przekazać tyle kontekstu do funkcji zewnętrznej (do której może ona mieć wygodny dostęp jako funkcja lokalna), że jest więcej komplikacji w sposobie wywoływania funkcji niż w jej działaniu.
Kaz
1

Czy jest to akceptowane? To naprawdę pytanie, na które tylko Ty możesz odpowiedzieć. Czy funkcja osiąga to, czego potrzebuje? Czy można to utrzymać? Czy jest to „akceptowalne” dla innych członków twojego zespołu? Jeśli tak, to właśnie to ma znaczenie.

Edycja: Nie widziałem rzeczy o zagnieżdżonych funkcjach. Osobiście nie używałbym ich. Zamiast tego użyłbym zwykłych funkcji.

Grandmaster B.
źródło
1

Długa funkcja „linii prostej” może być prostym sposobem na określenie długiej sekwencji kroków, które zawsze występują w określonej sekwencji.

Jednak, jak wspomnieli inni, ta forma jest podatna na lokalne obszary złożoności, w których ogólny przepływ jest mniej widoczny. Umieszczenie tej lokalnej złożoności w funkcji zagnieżdżonej (tj. Zdefiniowanej gdzie indziej w funkcji długiej, być może na górze lub na dole) może przywrócić przejrzystość przepływu linii głównej.

Drugim ważnym zagadnieniem jest kontrolowanie zakresu zmiennych, które mają być używane tylko w lokalnym odcinku długiej funkcji. Wymagana jest czujność, aby uniknąć posiadania zmiennej, która została wprowadzona w jednej sekcji kodu nieświadomie przywołanej gdzie indziej (na przykład po cyklach edycji), ponieważ ten rodzaj błędu nie pojawi się jako błąd kompilacji lub środowiska wykonawczego.

W niektórych językach tego problemu można łatwo uniknąć: lokalny odcinek kodu można owinąć we własnym bloku, na przykład za pomocą „{...}”, w którym wszelkie nowo wprowadzone zmienne są widoczne tylko dla tego bloku. Niektóre języki, takie jak Python, nie mają tej funkcji, w którym to przypadku funkcje lokalne mogą być przydatne do wymuszenia regionów o mniejszym zasięgu.

gwideman
źródło
1

Nie, funkcje wielostronicowe nie są pożądane i nie powinny przejść przeglądu kodu. Pisałem też długie funkcje, ale po przeczytaniu Refaktoryzacji Martina Fowlera przestałem. Długie funkcje są trudne do napisania poprawnie, trudne do zrozumienia i trudne do przetestowania. Nigdy nie widziałem funkcji nawet 50 linii, która nie byłaby łatwiejsza do zrozumienia i przetestowania, gdyby została podzielona na zestaw mniejszych funkcji. W funkcji wielostronicowej prawie na pewno istnieją całe klasy, które należy uwzględnić. Trudno jest być bardziej konkretnym. Może powinieneś opublikować jedną ze swoich długich funkcji w Code Review, a ktoś (może ja) pokaże ci, jak ją poprawić.

Kevin Cline
źródło
0

Gdy programuję w Pythonie, lubię cofać się po napisaniu funkcji i zadać sobie pytanie, czy jest ona zgodna z „Zen of Python” (wpisz „import this” w interpreterie Pythona):

Piękne jest lepsze niż brzydkie.
Jawne jest lepsze niż niejawne.
Prosty jest lepszy niż złożony.
Kompleks jest lepszy niż skomplikowany.
Mieszkanie jest lepsze niż zagnieżdżone.
Rzadki jest lepszy niż gęsty.
Liczy się czytelność.
Przypadki specjalne nie są wystarczająco wyjątkowe, aby złamać zasady.
Chociaż praktyczność przewyższa czystość.
Błędy nigdy nie powinny przejść bezgłośnie.
Chyba że wyraźnie uciszone.
W obliczu dwuznaczności odmów pokusy zgadywania.
Powinien być jeden - a najlepiej tylko jeden - oczywisty sposób na zrobienie tego.
Chociaż na początku taki sposób może nie być oczywisty, chyba że jesteś Holendrem.
Teraz jest lepiej niż nigdy.
Chociaż nigdy nie jest często lepsze niż właściweteraz.
Jeśli implementacja jest trudna do wyjaśnienia, to zły pomysł.
Jeśli implementacja jest łatwa do wyjaśnienia, może to być dobry pomysł.
Przestrzenie nazw to jeden świetny pomysł - zróbmy ich więcej!


źródło
1
Jeśli jawne jest lepsze niż niejawne, powinniśmy kodować w języku maszynowym. Nawet w asemblerze; robi paskudne domniemane rzeczy, takie jak automatyczne wypełnianie przedziałów opóźnienia gałęzi instrukcjami, obliczanie względnych przesunięć gałęzi i rozwiązywanie odniesień symbolicznych między globałami. Człowieku, ci głupi użytkownicy Pythona i ich mała religia ...
Kaz
0

Umieść je w osobnym module.

Zakładając, że twoje rozwiązanie nie jest bardziej rozdęte niż potrzeba, nie masz zbyt wielu opcji. Już podzieliłeś funkcje na różne podfunkcje, więc pytanie brzmi, gdzie powinieneś to umieścić:

  1. Umieść je w module z tylko jedną funkcją „publiczną”.
  2. Umieść je w klasie z tylko jedną funkcją „publiczną” (statyczną).
  3. Zagnieżdż je w funkcji (jak opisano).

Teraz zarówno druga, jak i trzecia alternatywa są w pewnym sensie zagnieżdżone, ale druga alternatywa nie wydaje się być zła. Jeśli nie wykluczysz drugiej alternatywy, nie widzę zbyt wiele powodów, aby wykluczyć trzecią.

Król nieba
źródło