Najlepsza praktyka - Zawijanie w pobliżu wywołania funkcji vs Dodanie wcześniejszego wyjścia, jeśli wartownik działa

9

Wiem, że może to być bardzo specyficzne dla każdego przypadku użycia, ale zbyt często zastanawiam się nad tym. Czy istnieje ogólnie preferowana składnia.

Nie pytam, jakie jest najlepsze podejście do funkcji, pytam, czy powinienem wyjść wcześniej, czy nie powinienem wywoływać funkcji.

Zawiń, jeśli wokół wywołania funkcji


if (shouldThisRun) {
  runFunction();
}

Mają if ( wartownik ) w funkcji

runFunction() {
  if (!shouldThisRun) return;
}

Ta ostatnia opcja oczywiście może zmniejszyć duplikację kodu, jeśli ta funkcja jest wywoływana wiele razy, ale czasami dodanie jej tutaj jest błędne, ponieważ może to oznaczać, że funkcja traci jedną odpowiedzialność .


Oto przykład

Jeśli mam funkcję updateStatus (), która po prostu aktualizuje status czegoś. Chcę tylko zaktualizować status, jeśli status się zmienił. Znam miejsca w moim kodzie, w których status może się zmienić, i znam inne miejsca, w których zdecydowanie się zmienił.

Nie jestem pewien, czy to tylko ja, ale czuję się trochę brudnie, aby sprawdzić tę funkcję wewnętrzną, ponieważ chcę zachować tę funkcję tak czystą, jak to możliwe - jeśli ją wywołam, oczekuję, że status zostanie zaktualizowany. Ale nie wiem, czy lepiej zawrzeć połączenie w kilku miejscach, w których wiem, że może się nie zmienić.

Matthew Mullin
źródło
3
@gnat Nie, to pytanie brzmi: „Jaka jest preferowana składnia przy wczesnym wyjściu”, podczas gdy moje brzmi: „Czy powinienem wyjść wcześniej, czy powinienem po prostu nie wywoływać funkcji”
Matthew Mullin
4
nie możesz ufać programistom, nawet sobie , że poprawnie sprawdzają warunki wstępne funkcji wszędzie tam, gdzie jest wywoływana. z tego powodu sugerowałbym, aby funkcja wewnętrznie zweryfikowała wszelkie niezbędne warunki, jeśli ma taką możliwość.
TZHX
„Chcę tylko aktualizować status, jeśli status się zmienił” - chcesz zaktualizować status (= zmieniony), jeśli zmienił się ten sam status? Brzmi dość okrągłe. Czy możesz wyjaśnić, co przez to rozumiesz, abym mógł dodać sensowny przykład do mojej odpowiedzi w tej sprawie?
Doc Brown
@DocBrown Pozwala na przykład powiedzieć, że chcę zachować synchronizację właściwości statusu dwóch różnych obiektów. Kiedy któryś z obiektów się zmienia, wywołuję syncStatuses () - ale może to zostać wywołane dla wielu różnych zmian pola (nie tylko pola statusu).
Matthew Mullin,

Odpowiedzi:

15

Zawijanie if wokół wywołania funkcji:
To decyduje, czy funkcja powinna być w ogóle wywoływana i jest częścią procesu decyzyjnego twojego programu.

Klauzula ochronna w funkcji (wczesny powrót):
Ma to na celu ochronę przed wywołaniem z niepoprawnymi parametrami

Użyta w ten sposób klauzula ochronna utrzymuje funkcję „czystą” (twój termin). Istnieje tylko po to, aby zapewnić, że funkcja nie zepsuje się przy błędnych danych wejściowych.

Logika, czy w ogóle wywoływać funkcję, jest na wyższym poziomie abstrakcji, nawet jeśli tylko. Powinien istnieć poza samą funkcją. Jak mówi DocBrown, możesz mieć funkcję pośrednią, która wykonuje tę kontrolę, aby uprościć kod.

To dobre pytanie, które należy zadać i mieści się w zestawie pytań, które prowadzą do rozpoznania poziomów abstrakcji. Każda funkcja powinna działać na jednym poziomie abstrakcji, a posiadanie zarówno logiki programu, jak i logiki funkcji w funkcji jest dla ciebie złe - dzieje się tak, ponieważ znajdują się na różnych poziomach abstrakcji. Sama funkcja jest niższym poziomem.

Utrzymując je osobno, masz pewność, że Twój kod będzie łatwiej pisać, czytać i utrzymywać.

Baldrickk
źródło
Świetna odpowiedź. Podoba mi się fakt, że daje mi to jasny sposób myślenia o tym. If powinien być na zewnątrz, ponieważ jest „częścią procesu decyzyjnego”, czy funkcja powinna być wywołana w pierwszej kolejności. I z natury nie ma nic wspólnego z samą funkcją. Dziwnie jest oznaczyć odpowiedź opinii jako poprawną, ale za kilka godzin jeszcze raz ją zobaczę.
Matthew Mullin,
Jeśli to pomaga, nie uważam tego za odpowiedź na „opinię”. Zauważam, że „czuje się” źle, ale dzieje się tak, ponieważ różne poziomy abstrakcji nie są oddzielne. Z twojego pytania wynika, że ​​widać, że to nie jest „właściwe”, ale ponieważ nie myślisz o poziomach abstrakcji, trudno jest je określić ilościowo, dlatego trudno ci wyrazić to słowami.
Baldrickk
7

Możesz mieć jedno i drugie - funkcję, która nie sprawdza parametrów, i drugą, która tak robi (może zwracać informacje o tym, czy wywołanie zostało wykonane):

bool tryRunFunction(...)
{
    bool shouldThisRun = /* some logic using data not available inside "runFunction"*/;
    if (shouldThisRun)
        runFunction();
    return shouldThisRun;
}

W ten sposób można uniknąć zduplikowanej logiki, zapewniając możliwość wielokrotnego użytku tryRunFunctioni nadal posiadając oryginalną (być może czystą) funkcję, która nie powoduje sprawdzenia w środku.

Zauważ, że czasami będziesz potrzebować funkcji takiej jak wyłącznie tryRunFunctionze zintegrowanym czekiem, abyś mógł zintegrować czek z runFunction. Lub nie musisz ponownie używać czeku w dowolnym miejscu w programie, w którym to przypadku możesz pozwolić mu pozostać w funkcji wywoływania.

Staraj się jednak, aby wywołujący był przejrzysty, co się dzieje, nadając swoim funkcjom odpowiednie nazwy. Osoby wywołujące nie muszą więc zgadywać ani sprawdzać implementacji, jeśli muszą same sprawdzić lub jeśli wywołana funkcja już to robi. Prosty prefiks, taki jak, trymoże często być do tego wystarczający.

Doktor Brown
źródło
1
Muszę przyznać, że idiom „tryXXX ()” zawsze wydawał się nieco odbiegający od normy i tutaj jest nieodpowiedni. Nie próbujesz zrobić czegoś oczekującego prawdopodobnego błędu. Aktualizujesz, jeśli jest brudny.
user949300,
@ user949300: wybranie dobrego imienia lub schematu nazewnictwa zależy od rzeczywistego przypadku użycia, rzeczywistych nazw funkcji, a nie od jakiejś wymyślonej nazwy runFunction. Funkcji podobnej updateStatus()może towarzyszyć inna funkcja podobna updateIfStatusHasChanged(). Jest to jednak zależne w 100% od przypadku, nie ma na to „uniwersalnego rozwiązania”, więc zgadzam się, idiom „wypróbowania” nie zawsze jest dobrym wyborem.
Doc Brown
Nie ma czegoś takiego jak „dryRun”? Mniej więcej jest to regularne wykonywanie bez skutków ubocznych. Jak wyłączyć efekty uboczne to inna historia
Laiv
3

Jeśli chodzi o to, kto decyduje, czy uruchomić, odpowiedź brzmi: od GRASP , który jest „ekspertem od informacji”, który wie.

Po podjęciu takiej decyzji rozważ zmianę nazwy funkcji dla zachowania przejrzystości.

Coś takiego, jeśli funkcja decyduje:

 ensureUpdated()
 updateIfDirty()

Lub, jeśli dzwoniący ma zdecydować:

 writeStatus()
użytkownik949300
źródło
2

Chciałbym rozwinąć odpowiedź @ Baldrickk.

Nie ma ogólnej odpowiedzi na twoje pytanie. Zależy to od znaczenia (kontraktu) funkcji do wywołania i charakteru warunku.

Omówmy to w kontekście twojego przykładowego wywołania updateStatus(). Umowa prawdopodobnie polega na aktualizacji jakiegoś statusu, ponieważ wydarzyło się coś, co miało wpływ na status. Spodziewałbym się, że wywołania tej metody będą dozwolone, nawet jeśli nie nastąpi prawdziwa zmiana statusu, i będą konieczne, jeśli nastąpi prawdziwa zmiana.

Tak więc strona wywołująca może pominąć połączenie, updateStatus()jeśli wie, że (w ramach swojej domeny) nic istotnego się nie zmieniło. W takich sytuacjach połączenie powinno być otoczone odpowiednią ifkonstrukcją.

Wewnątrz updateStatus()funkcji mogą wystąpić sytuacje, w których ta funkcja wykrywa (na podstawie danych z jej horyzontu domenowego), że nie ma nic do zrobienia i właśnie tam powinna wrócić wcześniej.

Tak więc pytania są następujące:

  • Kiedy z zewnątrz jest dozwolone / wymagane wywoływanie funkcji, biorąc pod uwagę kontrakt funkcji?
  • Czy wewnątrz funkcji są sytuacje, w których może wrócić wcześniej bez żadnej pracy?
  • Czy warunek, czy wywołać funkcję / czy wrócić wcześniej, należy do wewnętrznej domeny funkcji czy na zewnątrz?

W przypadku tej updateStatus()funkcji spodziewałbym się, że zobaczę oba, wzywając strony, które nie wiedzą, że nic się nie zmieniło, pomijam połączenie, a implementacja sprawdza wcześniej sytuacje „nic się nie zmieniło”, nawet jeśli w ten sposób zostanie sprawdzony dwukrotnie ten sam warunek, oba wewnątrz i na zewnątrz.

Ralf Kleberhoff
źródło
2

Istnieje wiele dobrych wyjaśnień. Ale chcę wyglądać w nietypowy sposób: Załóżmy, że używasz w ten sposób:

if (shouldThisRun) {
   runFunction();
}

runFunction() {
   if (!shouldThisRun) return;
}

I musisz wywołać inną funkcję w runFunctionmetodzie takiej jak ta:

runFunction() {
   if (!shouldThisRun) return;
   someOtherfunction();
}

Co zrobisz? Czy kopiujesz wszystkie walidacje od góry do dołu?

someOtherfunction() {
   if (!shouldThisRun) return;
}

Nie wydaje mi się Dlatego zazwyczaj stosuję to samo podejście: sprawdzaj poprawność danych wejściowych i sprawdzaj warunki w publicmetodzie. Metody publiczne powinny przeprowadzać własne weryfikacje i sprawdzać wymagane warunki, nawet w przypadku osób dzwoniących. Ale pozwól, aby prywatne metody po prostu działały na własny rachunek . Niektóre inne funkcje mogą wywoływać runFunctionbez sprawdzania poprawności lub sprawdzania jakichkolwiek warunków.

public someFunction() {
   if (shouldThisRun) {
      runFunction();
   }
}

private runFunction() {
 // do your business.
}
Engineert
źródło