Czy powinienem wyodrębnić określoną funkcję do funkcji i dlaczego?

29

Mam dużą metodę, która wykonuje 3 zadania, z których każde można wyodrębnić do osobnej funkcji. Jeśli stworzę dodatkowe funkcje dla każdego z tych zadań, czy poprawi to lub pogorszy mój kod i dlaczego?

Oczywiście spowoduje to zmniejszenie liczby wierszy kodu w funkcji głównej, ale pojawią się dodatkowe deklaracje funkcji, więc moja klasa będzie miała dodatkowe metody, które moim zdaniem nie są dobre, ponieważ sprawią, że klasa będzie bardziej złożona.

Czy powinienem to zrobić, zanim napisałem cały kod, czy powinienem go zostawić, aż wszystko się skończy, a następnie wyodrębnić funkcje?

dhblah
źródło
19
„Zostawiam to, dopóki wszystko się nie skończy” jest zwykle równoznaczne z „Nigdy nie będzie zrobione”.
Euforia
2
Jest to generalnie prawda, ale pamiętaj także o przeciwnej zasadzie YAGNI (która nie ma zastosowania w tym przypadku, ponieważ już jej potrzebujesz).
jhocking
Chciałem tylko podkreślić, nie skupiaj się tak bardzo na zmniejszaniu linii kodu. Zamiast tego spróbuj myśleć w kategoriach abstrakcji. Każda funkcja powinna mieć tylko jedno zadanie. Jeśli okaże się, że twoje funkcje wykonują więcej niż jedno zadanie, zazwyczaj powinieneś refaktoryzować metodę. Jeśli zastosujesz się do tych wskazówek, zbyt długie funkcje powinny być prawie niemożliwe.
Adrian

Odpowiedzi:

35

Jest to książka, do której często prowadzę odnośniki, ale tutaj ponownie: Robert C Martin's Clean Code , rozdział 3, „Funkcje”.

Oczywiście spowoduje to zmniejszenie liczby wierszy kodu w funkcji głównej, ale pojawią się dodatkowe deklaracje funkcji, więc moja klasa będzie miała dodatkowe metody, które moim zdaniem nie są dobre, ponieważ sprawią, że klasa będzie bardziej złożona.

Czy wolisz czytać funkcję z liniami +150, czy funkcję wywołującą 3 funkcje linii +50? Myślę, że wolę drugą opcję.

Tak , poprawi Twój kod w tym sensie, że będzie bardziej „czytelny”. Twórz funkcje, które wykonują jedną i tylko jedną rzecz, będą łatwiejsze w utrzymaniu i stworzeniu przypadku testowego.

Bardzo ważna rzecz, której nauczyłem się ze wspomnianej książki: wybierz dobre i precyzyjne nazwy dla swoich funkcji. Im ważniejsza jest ta funkcja, tym bardziej precyzyjna powinna być nazwa. Nie przejmuj się długością nazwy, jeśli trzeba ją nazwać FunctionThatDoesThisOneParticularThingOnly, nazwij ją w ten sposób.

Przed wykonaniem refaktora napisz jeden lub więcej przypadków testowych. Upewnij się, że działają. Po zakończeniu refaktoryzacji będziesz mógł uruchomić te przypadki testowe, aby upewnić się, że nowy kod działa poprawnie. Możesz napisać dodatkowe „mniejsze” testy, aby upewnić się, że nowe funkcje działają dobrze oddzielnie.

Wreszcie, i to nie jest sprzeczne z tym, co właśnie napisałem, zadaj sobie pytanie, czy naprawdę musisz dokonać tego refaktoryzacji, sprawdź odpowiedzi na „ Kiedy refaktoryzować ?” (również szukaj SO pytania dotyczące „refaktoryzacji”, jest ich więcej i odpowiedzi są interesujące do przeczytania)

Czy powinienem to zrobić przed napisaniem całego kodu, czy powinienem go zostawić, aż wszystko się skończy, a następnie wyodrębnić funkcje?

Jeśli kod już istnieje i działa, a brakuje ci czasu na następne wydanie, nie dotykaj go. W przeciwnym razie uważam, że w miarę możliwości należy wykonywać małe funkcje i jako takie refaktoryzować, gdy tylko będzie trochę czasu, upewniając się, że wszystko działa jak wcześniej (przypadki testowe).

Jalayn
źródło
10
W rzeczywistości Bob Martin kilkakrotnie pokazał, że preferuje 7 funkcji z 2 do 3 linii zamiast jednej funkcji z 15 liniami (patrz tutaj sites.google.com/site/unclebobconsultingllc/… ). I właśnie tam opiera się wielu nawet doświadczonych deweloperów. Osobiście uważam, że wielu z tych „doświadczonych deweloperów” po prostu ma problem z zaakceptowaniem faktu, że mogą ulepszyć tak podstawową rzecz jak budowanie abstrakcji za pomocą funkcji po> 10 latach kodowania.
Doc Brown
+1 tylko za odniesienie do książki, która według mojej skromnej opinii powinna znajdować się na półkach każdej firmy programistycznej.
Fabio Marcolini,
3
Być może parafrazuję tutaj, ale zdanie z tej książki, które odbija się echem w mojej głowie prawie każdego dnia, brzmi: „każda funkcja powinna robić tylko jedną rzecz i robić to dobrze”. Wydaje się to szczególnie istotne, ponieważ PO powiedział: „moja główna funkcja wykonuje trzy rzeczy”
wakjah
Masz całkowitą rację!
Jalayn
Zależy, jak bardzo te trzy oddzielne funkcje są ze sobą powiązane. Łatwiej jest śledzić blok kodu, który znajduje się w jednym miejscu, niż trzy bloki kodu, które wielokrotnie na sobie polegają.
user253751
13

Tak, oczywiście. Jeśli łatwo jest zobaczyć i oddzielić różne „zadania” jednej funkcji.

  1. Czytelność - funkcje o dobrych nazwach wyraźnie pokazują, co robi kod, bez konieczności czytania tego kodu.
  2. Wielokrotnego użytku - Łatwiej jest używać funkcji, która wykonuje jedną rzecz w wielu miejscach, niż funkcji, która robi rzeczy, których nie potrzebujesz.
  3. Testowalność - Łatwiej jest przetestować funkcję, która ma jedną zdefiniowaną „funkcję”, która ma wiele z nich

Ale mogą być z tym problemy:

  • Nie jest łatwo zobaczyć, jak oddzielić tę funkcję. Może to wymagać uprzedniego refaktoryzacji wnętrza funkcji, zanim przejdziesz do separacji.
  • Funkcja ma ogromny stan wewnętrzny, który jest przekazywany. Zwykle wymaga to pewnego rodzaju rozwiązania OOP.
  • Trudno powiedzieć, jaką funkcję powinna pełnić. Testuj urządzenie i refaktoryzuj, aż się dowiesz.
Euforyk
źródło
5

Problem, który stawiasz, nie jest problemem kodowania, konwencji lub praktyki kodowania, a raczej problemem czytelności i sposobu, w jaki redaktorzy tekstowi pokazują napisany kod. Ten sam problem pojawia się również w poście:

Czy podzielenie długich funkcji i metod na mniejsze jest w porządku, mimo że nie zostaną wywołane przez nic innego?

Podział funkcji na podfunkcje ma sens przy wdrażaniu dużego systemu z zamiarem kapsułkowania różnych funkcjonalności, z których będzie się składał. Niemniej jednak wcześniej czy później znajdziesz wiele dużych funkcji. Niektóre z nich są niereagowalne i trudne do utrzymania, nawet jeśli utrzymujesz je jako pojedyncze długie funkcje lub dzielisz je na mniejsze funkcje. Jest to szczególnie prawdziwe w przypadku funkcji, w których wykonywane operacje nie są konieczne w żadnym innym miejscu systemu. Umożliwia pobranie jednej z tak długich funkcji i rozważenie jej w szerszym widoku.

Zawodowiec:

  • Po przeczytaniu masz pełny pomysł na temat wszystkich funkcji, jakie wykonuje ta funkcja (możesz to przeczytać jako książkę);
  • Jeśli chcesz go debugować, możesz wykonać go krok po kroku bez przeskakiwania do innego pliku / części pliku;
  • Masz swobodę dostępu / używania dowolnej zmiennej zadeklarowanej na dowolnym etapie funkcji;
  • Algorytm, który funkcja implementuje w pełni zawarty w funkcji (enkapsulowany);

Contra:

  • Zajmuje wiele stron ekranu;
  • Przeczytanie go zajmuje dużo czasu;
  • Nie jest łatwo zapamiętać wszystkie etapy;

Teraz wyobraźmy sobie, że możemy podzielić długą funkcję na kilka podfunkcji i spojrzeć na nie z szerszej perspektywy.

Zawodowiec:

  • Z wyjątkiem funkcji urlopu każda funkcja opisuje słowami (nazwy podfunkcji) różne wykonane kroki;
  • Odczytywanie każdej pojedynczej funkcji / podfunkcji zajmuje bardzo krótko;
  • Oczywiste jest, jakie parametry i zmienne mają wpływ na każdą podfunkcję (rozdzielenie obaw);

Contra:

  • Łatwo jest sobie wyobrazić, jak działa funkcja taka jak „sin ()”, ale nie tak łatwo sobie wyobrazić, co robią nasze podfunkcje;
  • Algorytm jest teraz zniknięty, jest teraz rozpowszechniany w podfunkcjach maj (bez przeglądu);
  • Podczas debugowania krok po kroku łatwo jest zapomnieć wywołanie funkcji poziomu głębi, z którego pochodzisz (przeskakując tu i tam w plikach projektu);
  • Możesz łatwo stracić kontekst podczas czytania różnych podfunkcji;

Oba rozwiązania mają zalety i kontra. Najlepszym rozwiązaniem byłoby posiadanie edytorów, które pozwalają rozszerzyć, wbudować i na całą głębokość, każde wywołanie funkcji do jej zawartości. Co sprawiłoby, że dzielenie funkcji na podfunkcje byłoby jedynym najlepszym rozwiązaniem.

Antonello Ceravola
źródło
2

Dla mnie istnieją cztery powody, aby wyodrębnić bloki kodu do funkcji:

  • Ponownie go używasz: właśnie skopiowałeś blok kodu do schowka. Zamiast wklejać go, umieść go w funkcji i zamień blok na wywołanie funkcji po obu stronach. Dlatego za każdym razem, gdy trzeba zmienić ten blok kodu, wystarczy zmienić tylko jedną funkcję zamiast zmieniać kod w wielu miejscach. Dlatego za każdym razem, gdy kopiujesz blok kodu, musisz wykonać funkcję.

  • Jest to wywołanie zwrotne : jest to program obsługi zdarzeń lub kod użytkownika wywołany przez bibliotekę lub środowisko. (Trudno mi to sobie wyobrazić bez wykonywania funkcji).

  • Uważasz, że zostanie on ponownie wykorzystany w bieżącym projekcie, a może gdzieś indziej: właśnie napisałeś blok, który oblicza najdłuższy wspólny podsekwencja dwóch tablic. Nawet jeśli twój program wywoła tę funkcję tylko raz, uważam, że w końcu będę potrzebował tej funkcji również w innych projektach.

  • Chcesz kodu samokontrującego : Zamiast pisać wiersz komentarza nad blokiem kodu podsumowującego jego działanie, wyodrębniasz całość do funkcji i nazywasz to, co napiszesz w komentarzu. Chociaż nie jestem tego fanem, ponieważ lubię zapisywać nazwę używanego algorytmu, powód, dla którego wybrałem ten algorytm itp. Nazwy funkcji byłyby wtedy zbyt długie ...

Calmarius
źródło
1

Jestem pewien, że słyszałeś radę, aby zakres zmiennych był jak najściślejszy i mam nadzieję, że się z tym zgadzasz. Funkcje są kontenerami zakresu, a w mniejszych funkcjach zakres zmiennych lokalnych jest mniejszy. Jest o wiele jaśniej jak i kiedy mają być używane, a trudniej jest używać ich w niewłaściwej kolejności lub przed ich inicjalizacją.

Funkcje są również kontenerami przepływu logicznego. Istnieje tylko jedno wejście, wyjścia są wyraźnie oznaczone, a jeśli funkcja jest wystarczająco krótka, przepływy wewnętrzne powinny być oczywiste. Powoduje to zmniejszenie złożoności cyklicznej, co jest niezawodnym sposobem na zmniejszenie liczby wad.

John Wu
źródło
0

Na bok: Napisałem to w odpowiedzi na pytanie Dallina (teraz zamknięte), ale nadal uważam, że to może być komuś pomocne, więc proszę bardzo


Myślę, że powód atomizacji funkcji jest 2-krotny, a jak wspomina @jozefg, zależy od używanego języka.

Rozdzielenie obaw

Głównym powodem tego jest oddzielenie różnych fragmentów kodu, więc każdy blok kodu, który nie przyczynia się bezpośrednio do pożądanego wyniku / zamiaru funkcji, stanowi osobną kwestię i może zostać wyodrębniony.

Załóżmy, że masz zadanie w tle, które również aktualizuje pasek postępu, aktualizacja paska postępu nie jest bezpośrednio związana z zadaniem długo działającym, więc należy go wyodrębnić, nawet jeśli jest to jedyny fragment kodu, który korzysta z paska postępu.

Powiedz w JavaScript, że masz funkcję getMyData (), która 1) buduje komunikat mydła na podstawie parametrów, 2) inicjuje odwołanie do usługi, 3) wywołuje usługę z komunikatem mydła, 4) analizuje wynik, 5) zwraca wynik. Wydaje się rozsądne, napisałem tę dokładną funkcję wiele razy - ale tak naprawdę można ją podzielić na 3 funkcje prywatne, w tym kod dla 3 i 5 (jeśli to), ponieważ żaden inny kod nie jest bezpośrednio odpowiedzialny za pobieranie danych z usługi .

Ulepszone działanie debugowania

Jeśli masz funkcje całkowicie atomowe, ślad stosu staje się listą zadań, zawierającą wszystkie pomyślnie wykonane kody, tj .:

  • Uzyskaj moje dane
    • Zbuduj wiadomość z mydłem
    • Zainicjuj odniesienie do usługi
    • Odpowiedź przeanalizowanej usługi - BŁĄD

byłoby bardziej interesujące niż stwierdzenie, że wystąpił błąd podczas pobierania danych. Ale niektóre narzędzia są jeszcze bardziej przydatne do debugowania szczegółowych drzew połączeń, na przykład na przykład Płótno debugowania Microsofts .

Rozumiem również twoje obawy, że przestrzeganie kodu napisanego w ten sposób może być trudne, ponieważ pod koniec dnia musisz wybrać kolejność funkcji w jednym pliku, gdzie jako drzewo wywołań byłoby dużo bardziej skomplikowane niż to. . Ale jeśli funkcje są dobrze nazwane (intellisense pozwala mi używać 3-4 wyrazów wielbłąda w dowolnej funkcji, która mi się podoba, bez spowalniania mnie) i ma strukturę z interfejsem publicznym na górze pliku, twój kod będzie czytał jak pseudo-kod, który jest zdecydowanie najprostszym sposobem na uzyskanie wysokiego poziomu zrozumienia bazy kodu.

Do waszej informacji - to jedna z tych rzeczy „róbcie, co mówię nie tak, jak robię”, utrzymywanie kodu atomowego jest bezcelowe, chyba że jesteście bezwzględnie z nim zgodni IMHO, czego nie jestem.

Dead.Rabit
źródło