Czy zawsze najlepszą praktyką jest pisanie funkcji dla wszystkiego, co trzeba powtórzyć dwukrotnie?


131

Sam nie mogę się doczekać, aby napisać funkcję, gdy muszę zrobić coś więcej niż dwa razy. Ale jeśli chodzi o rzeczy, które pojawiają się tylko dwa razy, jest to nieco trudniejsze.

Dla kodu, który wymaga więcej niż dwóch linii, napiszę funkcję. Ale w obliczu takich rzeczy jak:

print "Hi, Tom"
print "Hi, Mary"

Waham się napisać:

def greeting(name):
    print "Hi, " + name

greeting('Tom')
greeting('Mary')

Drugi wydaje się zbyt duży, prawda?


Ale co, jeśli mamy:

for name in vip_list:
    print name
for name in guest_list:
    print name

A oto alternatywa:

def print_name(name_list):
    for name in name_list:
        print name

print_name(vip_list)
print_name(guest_list)

Sprawy stają się trudne, nie? Trudno teraz zdecydować.

Co o tym sądzisz?


145
Traktuję alwaysi neverjak czerwone flagi. Istnieje coś, co nazywa się „kontekstem”, w którym reguły alwaysi never, nawet jeśli ogólnie są dobre, mogą nie być odpowiednie. Uważaj na twórców oprogramowania, którzy zajmują się absolutami. ;)
asynchronuje

7
W ostatnim przykładzie można zrobić: from itertools import chain; for name in chain(vip_list, guest_list): print(name).
Bakuriu

14
@ user16547 Nazywamy tych twórców oprogramowania sithware!
Brian

6
Z Zen Pytona autorstwa Tim Peters:Special cases aren't special enough to break the rules. Although practicality beats purity.
Zenon

3
Zastanów się także, czy chcesz użyć „wilgotnego kodu” czy „suchego kodu”, patrz stackoverflow.com/questions/6453235/…
Ian

Odpowiedzi:


201

Chociaż jest to jeden z czynników decydujących o rozdzieleniu funkcji, liczba powtórzeń czegoś nie powinna być jedynym czynnikiem. Często sensowne jest utworzenie funkcji dla czegoś, co zostanie wykonane tylko raz . Zasadniczo chcesz podzielić funkcję, gdy:

  • Upraszcza każdą pojedynczą warstwę abstrakcji.
  • Masz dobre, znaczące nazwy dla funkcji podziału, więc zwykle nie musisz przeskakiwać między warstwami abstrakcji, aby zrozumieć, co się dzieje.

Twoje przykłady nie spełniają tych kryteriów. Przechodzisz od jednej linijki do jednej linijki, a nazwy tak naprawdę nie kupują niczego pod względem przejrzystości. To powiedziawszy, proste funkcje są rzadkie poza samouczkami i zadaniami szkolnymi. Większość programistów ma tendencję do zbyt daleko idących błędów.


Tęsknię za odpowiedzią Roberta Harvey'a.
Doc Brown

1
W rzeczy samej. Często dzielę złożone funkcje na wbudowane podprogramy - nie ma perfekcyjnego trafienia, ale jest czyste. Anonimowe zakresy też są dobre.
imallett

3
Istnieje również kwestia zakresu. Prawdopodobnie nie ma sensu pisanie funkcji print_name (nazwa_listy), którą może zobaczyć cała klasa lub cały moduł, ale może (w tym przypadku nadal jest to rozciągnięcie ) sensować utworzenie funkcji lokalnej w funkcji, aby wyczyścić kilka linii powtarzającego się kodu.
Joshua Taylor

6
Kolejną kwestią, którą mógłbym dodać, jest standaryzacja. W pierwszym przykładzie zostało zrobione powitanie, więc warto umieścić to w funkcji, aby mieć pewność, że powitamy obie osoby tak samo. Zwłaszcza biorąc pod uwagę powitanie może się zmienić w przyszłości :)
Svish

1
+1 tylko dla Często ma sens tworzenie funkcji dla czegoś, co jest wykonywane tylko raz. Kiedyś zapytano mnie, jak zaprojektować testy dla funkcji modelującej zachowanie silnika rakietowego. Cyklomatyczna złożoność tej funkcji miała miejsce w latach 90. i nie była to instrukcja switch z 90 przypadkami (brzydka, ale możliwa do przetestowania). Zamiast tego był to zawiły bałagan napisany przez inżynierów i był całkowicie nie do przetestowania. Moja odpowiedź była taka, że ​​był nie do przetestowania i musiał zostać przepisany. Postępowali zgodnie z moją radą!
David Hammen,

104

Tylko jeśli powielanie jest zamierzone, a nie przypadkowe .

Lub, mówiąc inaczej:

Tylko jeśli oczekujesz, że będą ewoluować w przyszłości.


Dlatego:

Czasami dwa kawałki kodu właśnie stało się stać to samo, mimo że nie mają nic wspólnego ze sobą. W takim przypadku musisz oprzeć się pragnieniu ich połączenia, ponieważ następnym razem, gdy ktoś przeprowadzi konserwację jednego z nich, osoba ta nie spodziewa się, że zmiany zostaną przeniesione do nieistniejącego wcześniej dzwoniącego, w wyniku czego funkcja ta może ulec awarii. Dlatego należy rozróżniać kod tylko wtedy, gdy ma to sens, a nie za każdym razem, gdy wydaje się zmniejszać rozmiar kodu.

Praktyczna zasada:

Jeśli kod zwraca tylko nowe dane i nie modyfikuje istniejących danych ani nie wywołuje innych skutków ubocznych, bardzo prawdopodobne jest, że będzie to bezpieczne jako osobna funkcja. (Nie wyobrażam sobie żadnego scenariusza, w którym spowodowałoby to awarię bez całkowitej zmiany zamierzonej semantyki funkcji, w którym to momencie nazwa funkcji lub sygnatura również powinna się zmienić, a ty i tak musisz zachować ostrożność. )


12
+1 Myślę, że jest to najważniejsza uwaga. Nikt nie refaktoryzuje kodu, oczekując, że ich zmiana będzie ostatnia, więc zastanów się, jak twoje zmiany wpłyną na przyszłe zmiany w kodzie.
yoniLavi

2
Jest rubinowy, ale ten screencast na temat przypadkowego powielania autorstwa Avdiego Grimma wciąż dotyczy pytania: youtube.com/watch?v=E4FluFOC1zM
DGM

60

Nie, nie zawsze jest to najlepsza praktyka.

Wszystkie inne rzeczy, które są równe, liniowy kod wiersz po wierszu są łatwiejsze do odczytania niż przeskakiwanie wywołań funkcji. Nietrywialne wywołanie funkcji zawsze przyjmuje parametry, więc musisz to wszystko uporządkować i wykonać mentalne kontekstowe skoki od wywołania funkcji do wywołania funkcji. Zawsze faworyzuj lepszą przejrzystość kodu, chyba że masz dobry powód, aby być niejasnym (np. Uzyskać niezbędne ulepszenia wydajności).

Dlaczego zatem kod jest refaktoryzowany na osobne metody? Aby poprawić modułowość. Aby zebrać znaczącą funkcjonalność za poszczególną metodą i nadać jej sensowną nazwę. Jeśli tego nie osiągniesz, nie potrzebujesz tych osobnych metod.


58
Najważniejsza jest czytelność kodu.
Tom Robinson

27
Poparłbym tę odpowiedź, gdybyś nie zapomniał jednego z głównych powodów tworzenia funkcji: stworzyć abstrakcję , nadając elementowi funkcjonalnemu dobre imię. Gdybyś dodał ten punkt, byłoby jasne, że liniowy kod wiersz po wierszu nie zawsze jest łatwiejszy do odczytania niż kod przy użyciu dobrze sformułowanych abstrakcji.
Doc Brown

17
Zgadzam się z Doc Brown, kod niekoniecznie jest bardziej czytelny wiersz po wierszu, niż gdy jest odpowiednio wyodrębniony. Dobre nazwy funkcji sprawiają, że funkcje wysokiego poziomu BARDZO są łatwe do odczytania (ponieważ czytasz zamiar, a nie implementacja), a funkcje niskiego poziomu łatwiejsze do odczytania, ponieważ wykonujesz jedno zadanie, dokładnie jedno zadanie i tylko to zadanie. Dzięki temu funkcja jest zwięzła i precyzyjna.
Jon Story

19
Why take the performance hit of a function call when you don't get any significant benefit by doing so?Jaka wydajność uderzyła? W ogólnym przypadku małe funkcje zostają wstawione, a narzut jest niewielki w przypadku dużych funkcji. CPython prawdopodobnie nie jest wbudowany, ale nikt nie używa go do wydajności. Ja też line by line code...trochę kwestionuję . Łatwiej jest, jeśli próbujesz symulować wykonanie w mózgu. Ale debugger wykona lepszą robotę, a próba udowodnienia czegoś o pętli poprzez wykonanie jej jest jak próba udowodnienia twierdzenia o liczbach całkowitych przez iterację po nich wszystkich.
Doval

6
@Doval podnosi BARDZO prawidłowy punkt - gdy kod jest kompilowany, funkcja jest umieszczana w wierszu (i duplikowana), a zatem nie ma ŻADNEGO spadku wydajności działania, chociaż w kompilacji jest mały. Nie dotyczy to języków interpretowanych, ale mimo to wywołanie funkcji stanowi niewielką część czasu wykonania.
Jon Story

25

W twoim konkretnym przykładzie wykonanie funkcji może wydawać się przesadą; zamiast tego zadałbym pytanie: czy to możliwe, że to szczególne powitanie zmieni się w przyszłości? Jak to?

Funkcje nie służą jedynie do zawijania funkcjonalności i ułatwiają ponowne użycie, ale także do modyfikacji. Jeśli wymagania zmienią się, należy wkleić i wkleić ręcznie skopiowany kod, a przy pomocy funkcji kod należy zmodyfikować tylko raz.

Twój przykład skorzystałby na tym nie poprzez funkcję - ponieważ logika prawie nie istnieje - ale być może GREET_OPENINGstała. Tę stałą można następnie załadować z pliku, aby program można łatwo dostosować na przykład do różnych języków. Zauważ, że jest to prymitywne rozwiązanie, bardziej złożony program prawdopodobnie potrzebowałby bardziej wyrafinowanego sposobu śledzenia i18n, ale znowu zależy to od wymagań, a nie pracy.

W końcu chodzi o możliwe wymagania i planowanie z wyprzedzeniem, aby ułatwić sobie pracę w przyszłości.


Użycie stałej GREET_OPENING zakłada, że ​​wszystkie możliwe pozdrowienia są w tej samej kolejności. Małżeństwo z kimś, kogo język ojczysty robi prawie wszystko przeciwnie do angielskiego, sprawia, że ​​jestem obojętny na to założenie.
Loren Pechtel

22

Osobiście przyjąłem zasadę trzech - co uzasadnię, dzwoniąc do YAGNI . Jeśli muszę coś zrobić raz, napiszę ten kod, dwa razy mogę po prostu skopiować / wkleić (tak, przyznałem się do kopiowania / wklejania!), Ponieważ nie będę go więcej potrzebował, ale jeśli będę musiał zrobić to samo sprawa ponownie wtedy będę byłaby i wyodrębnić ten klocek do jego własnej metody, mam wykazać sobie, że będzie go trzeba ponownie.

Zgadzam się z tym, co powiedzieli Karl Bielefeldt i Robert Harvey, a moją interpretacją tego, co mówią, jest to, że nadrzędną zasadą jest czytelność. Jeśli dzięki temu mój kod będzie łatwiejszy do odczytania, rozważ utworzenie funkcji, pamiętaj o takich rzeczach, jak DRY i SLAP . Jeśli mogę uniknąć zmiany poziomu abstrakcji w mojej funkcji, to łatwiej mi sobie z tym poradzić, więc nie przeskakiwanie między funkcjami (jeśli nie rozumiem, co robi funkcja po prostu czytając jej nazwę), oznacza mniej przełączników w mojej procesy mentalne.
Podobnie nie konieczności przełączania kontekstu między funkcjami i kodu inline takich jak print "Hi, Tom"pracuje dla mnie, w tym przypadku może wyodrębnić funkcję PrintNames() IFF resztą mojej ogólnej funkcji były głównie wywołania funkcji.


3
Wiki wiki c2 ma dobrą stronę o Regule trzech: c2.com/cgi/wiki?RuleOfThree
pimlottc 13.01.2015

13

Rzadko jest to jednoznaczne, więc musisz zważyć opcje:

  • Termin (napraw palenie serwerowni ASAP)
  • Czytelność kodu (może mieć wpływ na wybór w obu kierunkach)
  • Poziom abstrakcji wspólnej logiki (powiązany z powyższym)
  • Wymaganie ponownego użycia (tj. Ma dokładnie taką samą logikę ważną lub po prostu wygodną w tej chwili)
  • Trudność udostępniania (tutaj świeci python, patrz poniżej)

W C ++ zwykle stosuję zasadę trzech (tj. Za trzecim razem potrzebuję tego samego, przekształcam go w odpowiednio współdzielony kawałek), ale z doświadczeniem łatwiej jest dokonać tego wyboru na początku, ponieważ wiesz więcej o zakresie, oprogramowanie i domena, z którymi pracujesz.

Jednak w Pythonie ponowne użycie, a nie powtórzenie logiki jest dość lekkie. Bardziej niż w wielu innych językach (przynajmniej historycznie) IMO.

Rozważ więc ponowne użycie logiki lokalnie, na przykład poprzez utworzenie listy z lokalnych argumentów:

def foo():
    for name_list in (vip_list, guest_list): # can be list of tuples, for many args
        for name in name_list:
            print name

Możesz użyć krotki krotek i podzielić ją bezpośrednio na pętlę for, jeśli potrzebujesz kilku argumentów:

def foo2():
    for header, name_list in (('vips': vip_list), ('people': guest_list)): 
        print header + ": "
        for name in name_list:
            print name

lub stwórz funkcję lokalną (może to twój drugi przykład), która sprawia, że ​​ponowne użycie logiki jest jawne, ale także wyraźnie pokazuje, że print_name () nie jest używane poza funkcją:

def foo():
    def print_name(name_list):
        for name in name_list:
            print name

    print_name(vip_list)
    print_name(guest_list)

Funkcje są preferowane, zwłaszcza gdy trzeba przerwać logikę na środku (tzn. Użyć return), ponieważ przerwa lub wyjątki mogą niepotrzebnie zagracać rzeczy.

Każda z nich jest lepsza niż powtarzanie tej samej logiki, IMO, a także mniej zaśmiecania niż deklarowanie funkcji globalnej / klasy, która jest używana tylko przez jednego rozmówcę (aczkolwiek dwa razy).


Opublikowałem podobny komentarz na temat zakresu nowej funkcji, zanim zobaczyłem tę odpowiedź. Cieszę się, że istnieje odpowiedź na ten aspekt pytania.
Joshua Taylor

1
+1 - być może nie jest to odpowiedź, jakiej szuka PO, ale przynajmniej jeśli chodzi o pytanie, które zadał, myślę, że jest to najbardziej rozsądna odpowiedź. Jest to coś, co prawdopodobnie zostanie dla ciebie upieczone w języku.
Patrick Collins

@PatrickCollins: Dzięki za głos! :) Dodałem kilka uwag, aby uzupełnić odpowiedź.
Macke,

9

Wszystkie najlepsze praktyki mają szczególny powód, do którego możesz się odwołać, aby odpowiedzieć na takie pytanie. Zawsze należy unikać zmarszczek, gdy jedna potencjalna przyszła zmiana pociąga za sobą również zmiany innych składników.

W twoim przykładzie: jeśli zakładasz, że masz standardowe powitanie i chcesz upewnić się, że jest takie samo dla wszystkich ludzi, napisz

def std_greeting(name):
    print "Hi, " + name

for name in ["Tom", "Mary"]:
    std_greeting(name)   # even the function call should be written only once

W przeciwnym razie musisz zwrócić uwagę i zmienić standardowe powitanie w dwóch miejscach, jeśli tak się stanie.

Jeśli jednak „Cześć” jest takie samo przez przypadek, a zmiana jednego z pozdrowieniami niekoniecznie powoduje zmiany w drugim, zachowaj je oddzielnie. Dlatego trzymaj je oddzielnie, jeśli istnieją logiczne powody, by sądzić, że bardziej prawdopodobna jest następująca zmiana:

print "Hi, Tom"
print "Hello, Mary"

W drugiej części, decyzja o tym, ile „spakować” w funkcje, prawdopodobnie zależy od dwóch czynników:

  • utrzymuj małe bloki dla czytelności
  • utrzymuj bloki wystarczająco duże, aby zmiany następowały tylko w kilku blokach. Nie trzeba zbyt wiele grupować, ponieważ trudno jest śledzić wzorzec połączeń.

Idealnie, gdy nastąpi zmiana, pomyślisz „muszę zmienić większość następującego bloku”, a nie „muszę zmienić kod gdzieś wewnątrz dużego bloku”.


Po prostu głupie dręczenie, biorąc pod uwagę twoją pętlę - jeśli naprawdę musi być tylko garść zakodowanych nazw do iteracji, a nie oczekujemy, że się zmienią, argumentowałbym, że nieco bardziej czytelne jest nie używanie lista. for name in "Tom", "Mary": std_greeting(name)
yoniLavi

Cóż, aby kontynuować, można powiedzieć, że lista zakodowana na stałe nie pojawi się w środku kodu i będzie zmienną :) Ale tak, właściwie to zapomniałem, że można pominąć nawiasy klamrowe.
Gerenuk

5

Najważniejszym aspektem nazwanych stałych i funkcji jest nie tyle to, że zmniejszają one ilość pisania, ale raczej „dołączają” różne miejsca, w których są używane. Jeśli chodzi o twój przykład, rozważ cztery scenariusze:

  1. Konieczna jest zmiana obu powitań w ten sam sposób [np. Na Bonjour, Tomi Bonjour, Mary].

  2. Konieczna jest zmiana jednego powitania, ale pozostawienie takiego, jakie jest [np. Hi, TomI Guten Tag, Mary].

  3. Konieczna jest inna zmiana obu powitań [np. Hello, TomI Howdy, Mary].

  4. Żadne powitanie nigdy nie musi zostać zmienione.

Jeśli zmiana powitania nigdy nie będzie konieczna, tak naprawdę nie będzie miało znaczenia, które podejście zostanie zastosowane. Korzystanie z funkcji wspólnej spowoduje naturalny efekt, że zmiana powitania zmieni je w ten sam sposób. Gdyby wszystkie pozdrowienia zmieniły się w ten sam sposób, byłoby dobrze. Jeśli jednak nie powinni, powitanie każdej osoby będzie musiało zostać zakodowane lub określone osobno; każda praca, która sprawi, że będą korzystać ze wspólnej funkcji lub specyfikacji, będzie musiała zostać cofnięta (i lepiej byłoby, gdyby jej nie wykonano).

Oczywiście nie zawsze można przewidzieć przyszłość, ale jeśli ktoś ma powód, by sądzić, że bardziej prawdopodobne jest, że oba pozdrowienia będą musiały się zmienić razem, byłby to czynnik przemawiający za użyciem wspólnego kodu (lub nazwanej stałej) ; jeśli ktoś ma powód, by sądzić, że bardziej prawdopodobne jest, że jedno lub oba będą musiały zmienić, aby się różniły, byłby to czynnik przeciwko próbowaniu użycia wspólnego kodu.


Podoba mi się ta odpowiedź, ponieważ (przynajmniej dla tego prostego przykładu) możesz prawie obliczyć wybór za pomocą teorii gier.
RubberDuck,

@RubberDuck: Myślę, że ogólnie zbyt mało uwagi poświęca się pytaniu o to, co należy, a czego nie należy „aliasować” - pod wieloma względami, i spodziewałbym się, że dwie najczęstsze przyczyny błędów to (1) przekonanie, że rzeczy są aliasowane / dołączane, gdy nie są, lub (2) zmieniają coś, nie zdając sobie sprawy, że inne rzeczy są z tym związane. Takie problemy powstają przy projektowaniu zarówno kodu, jak i struktur danych, ale nie przypominam sobie, aby kiedykolwiek widziałem, jak wiele uwagi poświęcono tej koncepcji. Aliasing zazwyczaj nie ma znaczenia, gdy jest tylko jedno z nich lub jeśli to się nigdy nie zmieni, ale ...
supercat 15.01.2015

@RubberDuck: ... jeśli są dwa takie same i pasują do siebie, ważne jest, aby wiedzieć, czy zmiana jednego powinna pozostawić wartość drugiego jako stała, czy też powinna pozostać stała relacja (co oznacza, że ​​wartość drugiego by się zmieniła). Często widzę wielką wagę przywilejów utrzymania wartości na stałym poziomie, ale znacznie mniejszą wagę przypisuje się znaczeniu relacji .
supercat

Całkowicie się zgadzam. Właśnie zastanawiałem się nad tym, jak konwencjonalna mądrość mówi, aby to WYSUSZIĆ, ale statystycznie bardziej prawdopodobne jest, że powtarzany kod nie będzie później powtarzany. Zasadniczo masz 2 do 1 kursów.
RubberDuck,

@RubberDuck: Podaję dwa scenariusze, w których odłączenie jest ważne, i jeden, w którym przywiązanie jest lepsze, nie mówi nic o względnych prawdopodobieństwach różnych scenariuszy. Ważne jest rozpoznanie, kiedy dwa fragmenty kodu pasują „przypadkowo” lub dlatego, że mają takie samo podstawowe znaczenie. Ponadto, gdy są więcej niż dwa elementy, powstają dodatkowe scenariusze, w których jeden chce ostatecznie odróżnić jeden lub więcej od pozostałych, ale nadal zmienia dwa lub więcej w identyczny sposób; także, nawet jeśli akcja jest używana tylko w jednym miejscu ...
supercat 15.01.2015

5

Myślę, że powinieneś mieć powód, aby stworzyć procedurę. Istnieje wiele powodów, dla których należy utworzyć procedurę. Najważniejsze, które moim zdaniem są najważniejsze:

  1. abstrakcja
  2. modułowość
  3. nawigacja kodu
  4. zaktualizuj spójność
  5. rekurencja
  6. testowanie

Abstrakcja

Procedurę można wykorzystać do wyodrębnienia ogólnego algorytmu ze szczegółów poszczególnych kroków algorytmu. Jeśli potrafię znaleźć odpowiednio spójną abstrakcję, pomaga mi to ustrukturyzować sposób myślenia o działaniu algorytmu. Na przykład mogę zaprojektować algorytm, który działa na listach bez konieczności rozważania reprezentacji listy.

Modułowość

Procedury mogą służyć do organizowania kodu w moduły. Moduły można często traktować osobno. Na przykład zbudowany i przetestowany osobno.

Dobrze zaprojektowany moduł zazwyczaj przechwytuje pewną spójną, znaczącą jednostkę funkcjonalności. Dlatego często mogą być własnością różnych zespołów, całkowicie zamienione na alternatywne lub ponownie wykorzystane w innym kontekście.

Nawigacja kodu

W dużych systemach znalezienie kodu związanego z określonym zachowaniem systemu może być trudne. Hierarchiczna organizacja kodu w bibliotekach, modułach i procedurach może pomóc w tym wyzwaniu. W szczególności, jeśli spróbujesz a) zorganizować funkcjonalność pod znaczącymi i przewidywalnymi nazwami b) umieść procedury związane z podobną funkcjonalnością blisko siebie.

Zaktualizuj spójność

W dużych systemach znalezienie kodu, który należy zmienić, aby uzyskać określoną zmianę zachowania, może być trudne. Użycie procedur do zorganizowania funkcjonalności programu może to ułatwić. W szczególności, jeśli każdy element funkcjonalności twojego programu pojawia się tylko raz i w spójnej grupie procedur, z mojego doświadczenia mniej prawdopodobne jest, że przegapisz miejsce, w którym powinieneś zaktualizować lub dokonać niespójnej aktualizacji.

Pamiętaj, że powinieneś organizować procedury w oparciu o funkcjonalność i abstrakty w programie. Nie opiera się na tym, czy w tej chwili dwa bity kodu są takie same.

Rekurencja

Korzystanie z rekurencji wymaga utworzenia procedur

Testowanie

Zazwyczaj można testować procedury niezależnie od siebie. Niezależne testowanie różnych części treści procedury jest trudniejsze, ponieważ zazwyczaj pierwszą część procedury należy wykonać przed drugą częścią. Ta obserwacja często ma również zastosowanie do innych podejść do określania / weryfikowania zachowania programów.

Wniosek

Wiele z tych punktów dotyczy zrozumiałości programu. Można powiedzieć, że procedury są sposobem na stworzenie nowego języka, specyficznego dla Twojej domeny, w którym można organizować, pisać i czytać o problemach i procesach w Twojej domenie.


4

Żaden z podanych przypadków nie wydaje się warty refaktoryzacji.

W obu przypadkach wykonujesz operację, która jest wyrażona jasno i zwięźle, i nie ma niebezpieczeństwa, że ​​wprowadzona zostanie niespójna zmiana.

Zalecam, aby zawsze szukać sposobów izolowania kodu, w których:

  1. Istnieje możliwe do zidentyfikowania, znaczące zadanie, które można oddzielić i

  2. Zarówno

    za. zadanie jest złożone do wyrażenia (biorąc pod uwagę dostępne funkcje) lub

    b. zadanie jest wykonywane więcej niż raz i potrzebujesz sposobu, aby upewnić się, że jest zmieniany w ten sam sposób w obu miejscach,

Jeśli warunek 1 nie jest spełniony, nie znajdziesz odpowiedniego interfejsu dla kodu: jeśli spróbujesz go wymusić, skończysz z mnóstwem parametrów, wieloma rzeczami, które chcesz zwrócić, dużą ilością logiki kontekstowej i prawdopodobnie nie mogę znaleźć dobrego imienia. Spróbuj najpierw udokumentować interfejs, o którym myślisz, jest to dobry sposób na przetestowanie hipotezy, że jest to odpowiedni pakiet funkcjonalności i ma dodatkową zaletę, że podczas pisania kodu jest on już sprecyzowany i udokumentowany!

2a jest możliwe bez 2b: czasami wyjąłem kod, nawet gdy wiem, że zużywa się tylko raz, po prostu dlatego, że przeniesienie go w inne miejsce i zastąpienie go jedną linią oznacza, że ​​kontekst, w którym jest wywoływany, jest nagle znacznie bardziej czytelny (szczególnie jeśli jest to prosta koncepcja, którą język utrudnia wdrożenie). Jest to również jasne podczas czytania wyodrębnionej funkcji, gdzie logika zaczyna się i kończy oraz co robi.

2b jest możliwe bez 2a: Sztuką jest wyczucie, jakie zmiany są bardziej prawdopodobne. Czy bardziej prawdopodobne jest, że zasady Twojej firmy zmienią się z powiedzenia „Cześć” wszędzie na „Cześć” przez cały czas lub że powitanie zmieni się na bardziej formalny ton, jeśli wysyłasz żądanie zapłaty lub przeprosiny za przerwę w świadczeniu usług? Czasami może to wynikać z tego, że korzystasz z biblioteki zewnętrznej, której nie jesteś pewien i chcesz móc szybko zamienić ją na inną: implementacja może się zmienić, nawet jeśli funkcjonalność się nie zmieni.

Najczęściej jednak będziesz mieć mieszankę 2a i 2b. Będziesz musiał użyć swojej oceny, biorąc pod uwagę wartość biznesową kodu, częstotliwość, z jakiej się korzysta, czy jest dobrze udokumentowany i zrozumiany, stan zestawu testów itp.

Jeśli zauważysz tę samą logikę, która została użyta więcej niż jeden raz, z pewnością powinieneś rozważyć możliwość refaktoryzacji. Jeśli zaczynasz nowe instrukcje bardziej niż npoziomy wcięcia w większości języków, jest to kolejny, nieco mniej znaczący wyzwalacz (wybierz wartość, nktórą czujesz się komfortowo dla danego języka: na przykład Python może wynosić 6).

Tak długo, jak zastanawiasz się nad tymi rzeczami i eliminujesz duże potwory z kodem spaghetti, powinieneś czuć się dobrze - nie przejmuj się tak długo, jak drobnoziarniste musi być twoje refaktoryzowanie, że nie masz czasu dla takich rzeczy, jak przerabiane testy lub dokumentacja. Jeśli będzie trzeba, czas pokaże.


3

Ogólnie zgadzam się z Robertem Harveyem, ale chciałem dodać przypadek, aby podzielić funkcjonalność na funkcje. Aby poprawić czytelność. Rozważ przypadek:

def doIt(smth,smthElse)
    for x in getDataFromSomething(smth,smthElse):
        if not check(x,smth):
            continue
        process(x,smthElse)
        store(x) 

Mimo że te wywołania funkcji nie są używane nigdzie indziej, istnieje znaczny wzrost czytelności, jeśli wszystkie 3 funkcje są znacznie długie i mają zagnieżdżone pętle itp.


2

Nie ma twardej i szybkiej reguły, którą należy zastosować, będzie ona zależeć od faktycznej funkcji, która zostanie użyta. W przypadku prostego drukowania nazw nie użyłbym funkcji, ale jeśli byłaby to suma matematyczna, byłaby to inna historia. Następnie utworzyłbyś funkcję, nawet jeśli zostanie wywołana tylko dwa razy, aby zapewnić, że suma matematyczna będzie zawsze taka sama, nawet jeśli zostanie zmieniona. W innym przykładzie, jeśli wykonujesz jakąkolwiek formę sprawdzania poprawności, użyłbyś funkcji, więc w twoim przykładzie, jeśli musisz sprawdzić, czy nazwa jest dłuższa niż 5 znaków, użyłbyś funkcji, aby upewnić się, że zawsze wykonywane są te same sprawdzenia.

Myślę więc, że odpowiedziałeś na swoje pytanie, kiedy powiedziałeś „W przypadku kodów, które wymagają więcej niż dwóch linii, będę musiał napisać func”. Zasadniczo używałbyś funkcji, ale musisz także użyć własnej logiki, aby ustalić, czy istnieje jakaś wartość dodana z używania funkcji.


2

Zacząłem wierzyć w coś o refaktoryzacji, o której tu nie wspominałem, wiem, że jest tu już wiele odpowiedzi, ale myślę, że to jest nowe.

Byłem bezwzględnym refaktorem i mocno wierzyłem w SUCHO, zanim jeszcze pojawiły się warunki. Głównie dlatego, że mam problemy z utrzymaniem dużej bazy kodu w mojej głowie, a częściowo dlatego, że lubię kodowanie DRY i nie lubię niczego w kodowaniu C&P, w rzeczywistości jest to dla mnie bolesne i strasznie wolne.

Chodzi o to, że naleganie na DRY dało mi dużo praktyki w niektórych technikach, z których rzadko widuję inne. Wiele osób twierdzi, że Java jest trudna lub niemożliwa do wykonania SUCHEGO, ale tak naprawdę po prostu nie próbują.

Jeden przykład dawno temu, który jest nieco podobny do twojego przykładu. Ludzie myślą, że tworzenie GUI w Javie jest trudne. Oczywiście jest tak, jeśli kodujesz w ten sposób:

Menu m=new Menu("File");
MenuItem save=new MenuItem("Save")
save.addAction(saveAction); // I forget the syntax, but you get the idea
m.add(save);
MenuItem load=new MenuItem("Load")
load.addAction(loadAction)

Każdy, kto uważa, że ​​to jest po prostu szalone, ma absolutną rację, ale to nie wina Javy - kodu nigdy nie należy tak pisać. Te wywołania metod są funkcjami przeznaczonymi do pakowania w inne systemy. Jeśli nie możesz znaleźć takiego systemu, zbuduj go!

Oczywiście nie możesz takiego kodu, więc musisz cofnąć się i spojrzeć na problem, co właściwie robi ten powtarzany kod? Określa niektóre ciągi znaków i ich związek (drzewo) i łączy liście tego drzewa z działaniami. Więc tak naprawdę chcesz powiedzieć:

class Menu {
    @MenuItem("File|Load")
    public void fileLoad(){...}
    @MenuItem("File|Save")
    public void fileSave(){...}
    @MenuItem("Edit|Copy")
    public void editCopy(){...}...

Po zdefiniowaniu relacji w sposób zwięzły i opisowy piszesz metodę, jak sobie z tym poradzić - w tym przypadku iterujesz metody przekazanej klasy i budujesz drzewo, a następnie używasz go do budowania menu i Działania, a także (oczywiście) wyświetlają menu. Nie będziesz mieć powielania, a twoja metoda jest wielokrotnego użytku ... i prawdopodobnie łatwiejsza do napisania niż duża liczba menu, a jeśli naprawdę lubisz programować, masz o wiele więcej zabawy. To nie jest trudne - metoda, którą musisz napisać, to prawdopodobnie mniej linii niż ręczne tworzenie menu!

Chodzi o to, że aby dobrze to zrobić, trzeba dużo ćwiczyć. Musisz dobrze przeanalizować, jakie dokładnie informacje znajdują się w powtarzanych częściach, wyodrębnij te informacje i wymyśl, jak je dobrze wyrazić. Nauka używania narzędzi takich jak parsowanie ciągów i adnotacje bardzo pomaga. Bardzo ważna jest również wiedza na temat zgłaszania błędów i dokumentacji.

Otrzymujesz „darmową” praktykę, po prostu dobrze kodując - są duże szanse, że gdy będziesz w tym dobry, przekonasz się, że kodowanie czegoś SUCHEGO (w tym pisanie narzędzia wielokrotnego użytku) jest szybsze niż kopiowanie i wklejanie, a wszystkie błędy, powielone błędy i trudne zmiany, które powoduje ten rodzaj kodowania.

Nie sądzę, że byłbym w stanie czerpać przyjemność z mojej pracy, gdybym nie ćwiczył technik DRY i budowania narzędzi tak bardzo, jak tylko mogłem. Gdybym musiał wziąć obniżkę wynagrodzenia, aby nie musiałem kopiować i wklejać programowania, wziąłbym to.

Więc moje punkty to:

  • Kopiowanie i wklejanie kosztuje więcej czasu, chyba że nie wiesz, jak odpowiednio refaktoryzować.
  • Nauczysz się dobrze refaktoryzować, robiąc to - kładąc nacisk na DRY, nawet w najtrudniejszych i najbardziej trywialnych przypadkach.
  • Jesteś programistą, jeśli potrzebujesz małego narzędzia, aby twój kod SUSZONY, zbuduj go.

1

Ogólnie rzecz biorąc, dobrym pomysłem jest podzielenie czegoś na funkcję, która poprawia czytelność twojego kodu. Lub, jeśli często powtarzana część jest w jakiś sposób rdzeniem systemu. W powyższym przykładzie, jeśli trzeba przywitać użytkowników w zależności od ustawień regionalnych, warto mieć osobną funkcję powitania.


1

W następstwie komentarza @ DocBrown do @RobertHarvey na temat używania funkcji do tworzenia abstrakcji: jeśli nie możesz wymyślić odpowiednio pouczającej nazwy funkcji, która nie jest znacznie bardziej zwięzła ani bardziej przejrzysta niż kod za nią kryjący, nie pobiera się zbyt wiele . Bez jakiegokolwiek innego dobrego powodu, aby uczynić go funkcją, nie ma takiej potrzeby.

Ponadto nazwa funkcji rzadko przechwytuje pełną semantykę, więc może być konieczne sprawdzenie jej definicji, jeśli nie jest ona wystarczająco często znana. Zwłaszcza w języku innym niż funkcjonalny może być konieczne sprawdzenie, czy ma on skutki uboczne, jakie błędy mogą wystąpić i jak na nie reagować, złożoność czasu, czy przydziela i / lub zwalnia zasoby oraz czy jest wątkowo bezpieczny.

Oczywiście z definicji rozważamy tutaj tylko proste funkcje, ale dzieje się to w obie strony - w takich przypadkach wstawianie raczej nie zwiększy złożoności. Co więcej, czytelnik prawdopodobnie nie zda sobie sprawy, że jest to prosta funkcja, dopóki nie spojrzy. Nawet z IDE, które hiperłącza cię do definicji, wizualne przeskakiwanie jest przeszkodą w zrozumieniu.

Mówiąc ogólnie, rekurencyjnie dzieląc kod na więcej, mniejsze funkcje ostatecznie doprowadzą cię do punktu, w którym funkcje nie mają już niezależnego znaczenia, ponieważ ponieważ fragmenty kodu, z których są wykonane, stają się mniejsze, fragmenty te zyskują większe znaczenie z utworzonego kontekstu przez otaczający kod. Jako analogię, pomyśl o tym jak o zdjęciu: jeśli zbyt mocno powiększysz obraz, nie zobaczysz, na co patrzysz.


1
Nawet jeśli jest to najprostsza instrukcja zawijająca ją w funkcję, powinna uczynić ją bardziej czytelną. Są chwile, kiedy zawijanie go w funkcję jest przesadą, ale jeśli nie możesz wymyślić nazwy funkcji, która jest bardziej zwięzła i wyraźna niż to, co robią instrukcje, jest to prawdopodobnie zły znak. Twoje negatywy dotyczące korzystania z funkcji nie są tak naprawdę wielkim imo. Negatywy dla inklinacji są znacznie większe. Inlining sprawia, że ​​trudniej jest go czytać, trudniej jest utrzymać i trudniej go ponownie użyć.
pllee

@pllee: Przedstawiłem powody, dla których faktoring w funkcje staje się odwrotny do zamierzonego w skrajności (zauważ, że pytanie dotyczy konkretnie skrajności, a nie ogólnego przypadku.) Nie przedstawiłeś żadnych argumentów przeciwnych w tych kwestiach, ale po prostu twierdzą, że „powinno” być inne. Pisanie, że problem z nazewnictwem „prawdopodobnie” jest złym znakiem, nie jest argumentem, że można go uniknąć w rozważanych przypadkach (mój argument oczywiście polega na tym, że jest to przynajmniej ostrzeżenie - że mogłeś dosięgnąć tylko abstrakcji dla samego siebie.)
sdenham

Wspomniałem o 3 przeczeniach, że nie jestem pewien, gdzie, jak twierdzę, „powinno być inaczej”. Mówię tylko, że jeśli nie możesz wymyślić imienia, prawdopodobnie Twój program robi zbyt wiele. Myślę też, że brakuje ci tutaj ważnego punktu na temat nawet drobnych, kiedyś używanych metod. Istnieją małe, dobrze nazwane metody, więc nie musisz wiedzieć, co próbuje zrobić kod (nie musisz przeskakiwać IDE). Na przykład nawet proste zdanie `if (dzień == 0 || dzień == 6)` vs `if (isWeekend (dzień))` staje się łatwiejsze do odczytania i mapowania mentalnego. Teraz, jeśli trzeba powtórzyć, to stwierdzenie isWeekendstaje się oczywiste.
pllee

@pllee Twierdzisz, że jest to „prawdopodobnie zły znak”, jeśli nie możesz znaleźć znacznie krótszej, wyraźniejszej nazwy dla prawie każdego powtarzającego się fragmentu kodu, ale wydaje się to być kwestią wiary - nie podajesz żadnego dodatkowego argumentu (aby uogólnić przypadek, potrzebujesz więcej niż przykładów.) isWeekend ma sensowną nazwę, więc jedynym sposobem, by mógł nie spełnić moich kryteriów, jest to, że nie jest ani znacznie krótszy, ani znacznie jaśniejszy niż jego implementacja. Argumentując, że uważasz, że to jest to drugie, twierdzisz, że nie jest to kontrprzykładem dla mojej pozycji (FWIW, sam użyłem is Weekend).
sdenham

Podałem przykład, w którym nawet instrukcja jednowierszowa może być bardziej czytelna, i podałem negatywy kodu wstawiania. To moja skromna opinia i wszystko, co mówię, i nie, nie spieram się ani nie mówię o „nienazwanej metodzie”. Nie jestem do końca pewien, co jest tak mylącego w tym względzie ani dlaczego, moim zdaniem, potrzebuję „teoretycznego dowodu” na moją opinię. Próbowałem poprawionej czytelności. Utrzymanie jest trudniejsze, ponieważ jeśli fragment kodu zmienia się, musi to być w liczbie N miejsc (czytaj o OSUSZANIU). Ponowne użycie jest oczywiście trudniejsze, chyba że uważasz, że skopiuj wklej, jeśli jest to realny sposób ponownego użycia kodu.
pllee,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.