Często spotykam metody / funkcje, które mają dodatkowy parametr boolowski, który kontroluje, czy wyjątek jest zgłaszany w przypadku niepowodzenia, czy zwracany jest null.
Trwają już dyskusje na temat tego, który z nich jest lepszym wyborem, więc nie skupmy się na tym tutaj. Zobacz np. Zwróć wartość magiczną, wyrzuć wyjątek lub zwróć false w przypadku niepowodzenia?
Zamiast tego załóżmy, że istnieje dobry powód, dla którego chcemy wspierać obie strony.
Osobiście uważam, że taką metodę należy raczej podzielić na dwie części: jedną, która generuje wyjątek w przypadku niepowodzenia, drugą, która zwraca wartość null w przypadku niepowodzenia.
Więc co jest lepsze?
Odp .: Jedna metoda z $exception_on_failure
parametrem.
/**
* @param int $id
* @param bool $exception_on_failure
*
* @return Item|null
* The item, or null if not found and $exception_on_failure is false.
* @throws NoSuchItemException
* Thrown if item not found, and $exception_on_failure is true.
*/
function loadItem(int $id, bool $exception_on_failure): ?Item;
B: Dwie różne metody.
/**
* @param int $id
*
* @return Item|null
* The item, or null if not found.
*/
function loadItemOrNull(int $id): ?Item;
/**
* @param int $id
*
* @return Item
* The item, if found (exception otherwise).
*
* @throws NoSuchItemException
* Thrown if item not found.
*/
function loadItem(int $id): Item;
EDYCJA: C: Coś jeszcze?
Wiele osób sugerowało inne opcje lub twierdziło, że zarówno A, jak i B są wadliwe. Takie sugestie lub opinie są mile widziane, istotne i przydatne. Pełna odpowiedź może zawierać takie dodatkowe informacje, ale także odniesie się do głównego pytania, czy parametr zmiany podpisu / zachowania jest dobrym pomysłem.
Notatki
W przypadku, gdy ktoś się zastanawia: przykłady są w języku PHP. Myślę jednak, że pytanie dotyczy wszystkich języków, o ile są one nieco podobne do PHP lub Java.
źródło
loadItemOrNull(id)
zastanowić się, czyloadItemOr(id, defaultItem)
ma to dla ciebie sens. Jeśli element jest ciągiem lub liczbą, często tak jest.Odpowiedzi:
Masz rację: dwie metody są o wiele lepsze z kilku powodów:
W Javie podpis metody, która potencjalnie zgłasza wyjątek, będzie zawierał ten wyjątek; inna metoda nie. Dzięki temu wyraźnie widać, czego można oczekiwać od jednego i drugiego.
W językach takich jak C #, w których podpis metody nic nie mówi o wyjątkach, metody publiczne powinny być nadal dokumentowane, a taka dokumentacja obejmuje wyjątki. Udokumentowanie pojedynczej metody nie byłoby łatwe.
Twój przykład jest doskonały: komentarze w drugim fragmencie kodu wyglądają na znacznie wyraźniejsze, a ja nawet skróciłbym „Element, jeśli został znaleziony (wyjątek inaczej).” Aż do „Elementu” - obecność potencjalnego wyjątku i opis, który mu podałeś, jest oczywisty.
W przypadku pojedynczej metody istnieje kilka przypadków, w których chciałbyś zmienić wartość parametru boolean w czasie wykonywania , a jeśli to zrobisz, oznaczałoby to, że osoba dzwoniąca będzie musiała obsłużyć oba przypadki (
null
odpowiedź i wyjątek ), co znacznie utrudnia kod. Ponieważ wyboru nie dokonuje się w czasie wykonywania, ale podczas pisania kodu, dwie metody mają idealny sens.Niektóre frameworki, takie jak .NET Framework, ustanowiły konwencje dla tej sytuacji i rozwiązują je dwiema metodami, tak jak sugerowałeś. Jedyną różnicą jest to, że używają wzoru wyjaśnionego przez Ewana w jego odpowiedzi , więc
int.Parse(string): int
zgłasza wyjątek, podczas gdyint.TryParse(string, out int): bool
nie. Ta konwencja nazewnictwa jest bardzo silna w społeczności .NET i należy jej przestrzegać, ilekroć kod pasuje do opisanej sytuacji.źródło
int.Parse()
jest uważana za naprawdę złą decyzję projektową, i właśnie dlatego zespół .NET zaczął wdrażaćTryParse
.->itemExists($id)
przed nawiązaniem połączenia->loadItem($id)
. A może jest to po prostu wybór dokonany przez innych ludzi w przeszłości i musimy brać to za pewnik.data Maybe a = Just a | Nothing
).Ciekawą odmianą jest język Swift. W Swift deklarujesz funkcję jako „rzucanie”, co oznacza, że dozwolone jest zgłaszanie błędów (podobne, ale nie takie same jak w przypadku wyjątków Java). Dzwoniący może wywołać tę funkcję na cztery sposoby:
za. W instrukcji try / catch łapanie i obsługa wyjątku.
b. Jeśli sam dzwoniący jest zadeklarowany jako rzucający, po prostu zadzwoń, a każdy zgłoszony błąd zamieni się w błąd zgłoszony przez dzwoniącego.
do. Zaznaczenie wywołania funkcji,
try!
co oznacza „Jestem pewien, że to wywołanie nie zostanie rzucone”. Jeżeli wywoływana funkcja wykonuje rzut aplikacja jest gwarantowana do katastrofy.re. Oznaczenie wywołania funkcji
try?
oznacza: „Wiem, że funkcja może rzucać, ale nie dbam o to, który błąd zostanie zgłoszony”. Zmienia to wartość zwracaną przez funkcję z typu „T
” na typ „optional<T>
”, a zgłoszony wyjątek zmienia się w zwracany zero.(a) i (d) są sprawami, o które pytasz. Co jeśli funkcja może zwrócić zero i może generować wyjątki? W takim przypadku typ zwracanej wartości to „
optional<R>
” dla niektórych typów R, a (d) zmieni to na „optional<optional<R>>
”, co jest nieco tajemnicze i trudne do zrozumienia, ale całkowicie w porządku. A funkcja Swift może powrócićoptional<Void>
, więc (d) może być użyte do rzucania funkcji zwracających Pustkę.źródło
Lubię to
bool TryMethodName(out returnValue)
podejście.Daje to jednoznaczne wskazanie, czy metoda się powiodła, czy też nie, a nazewnictwo pasuje do zawijania metody zgłaszania wyjątku w bloku try catch.
Jeśli po prostu zwrócisz null, nie wiesz, czy to się nie powiodło, czy wartość zwracana była prawnie zerowa.
na przykład:
przykładowe użycie (out oznacza przekazanie przez odwołanie niezainicjowane)
źródło
bool TryMethodName(out returnValue)
podejście” w oryginalnym przykładzie?Zwykle parametr boolowski wskazuje, że funkcję można podzielić na dwie, i z reguły należy ją zawsze dzielić, a nie przekazywać wartość logiczną.
źródło
b
: Zamiast wywoływaćfoo(…, b);
, musisz pisaćif (b) then foo_x(…) else foo_y(…);
i powtarzać inne argumenty, lub coś w rodzaju,((b) ? foo_x : foo_y)(…)
jeśli język ma trójskładnikowego operatora i funkcje / metody pierwszej klasy. I to może nawet powtórzyć z kilku różnych miejsc w programie.if (myGrandmaMadeCookies) eatThem() else makeFood()
który tak mógłbym zawrzeć w innej takiej funkcjicheckIfShouldCook(myGrandmaMadeCookies)
. Zastanawiam się, czy niuans, o którym wspomniałeś, ma związek z tym, czy przekazujemy wartość logiczną, jak chcemy zachować funkcję, jak w pierwotnym pytaniu, czy przekazujemy informacje o naszym modelu, tak jak w przykład babci, który właśnie podałem.Clean Code zaleca unikanie argumentów flagi logicznej, ponieważ ich znaczenie jest nieprzejrzyste w witrynie wywołania:
podczas gdy osobne metody mogą używać wyrazistych nazw:
źródło
Gdybym musiał użyć A lub B, wówczas użyłbym B, dwóch oddzielnych metod, z powodów określonych w użyłbym odpowiedzi Arseni Mourzenkos .
Istnieje jednak inna metoda 1 : w Javie jest ona wywoływana
Optional
, ale możesz zastosować tę samą koncepcję do innych języków, w których możesz zdefiniować własne typy, jeśli język nie zapewnia już podobnej klasy.Swoją metodę definiujesz w następujący sposób:
W swojej metodzie albo zwrócisz,
Optional.of(item)
jeśli znajdziesz przedmiot, albo wróciszOptional.empty()
, jeśli nie. Nigdy nie rzucasz 2 i nigdy nie wracasznull
.Dla klienta twojej metody jest to coś podobnego
null
, ale z tą dużą różnicą, że zmusza do myślenia o przypadku brakujących elementów. Użytkownik nie może po prostu zignorować faktu, że może nie być rezultatu. Aby wyciągnąć element zOptional
jawnej akcji, wymagana jest:loadItem(1).get()
rzuciNoSuchElementException
lub zwróci znaleziony przedmiot.loadItem(1).orElseThrow(() -> new MyException("No item 1"))
użyć niestandardowego wyjątku, jeśli zwróconyOptional
jest pusty.loadItem(1).orElse(defaultItem)
zwraca znaleziony lub przekazany elementdefaultItem
(który może być równieżnull
) bez zgłaszania wyjątku.loadItem(1).map(Item::getName)
ponownie zwróciOptional
z nazwą przedmiotu, jeśli jest obecny.Optional
.Zaletą jest to, że teraz to od kodu klienta zależy, co powinno się stać, jeśli nie ma elementu i nadal wystarczy podać jedną metodę .
1 Myślę, że jest to coś
try?
w rodzaju odpowiedzi w gnasher729, ale nie jest to dla mnie całkowicie jasne, ponieważ nie znam Swift i wydaje się, że jest to funkcja językowa, więc jest specyficzna dla Swift.2 Możesz zgłaszać wyjątki z innych powodów, np. Jeśli nie wiesz, czy istnieje element, czy nie, ponieważ miałeś problemy z komunikacją z bazą danych.
źródło
Kolejny punkt, w którym zgadzają się dwie metody, może być bardzo łatwy do wdrożenia. Napiszę mój przykład w języku C #, ponieważ nie znam składni PHP.
źródło
Wolę dwie metody, w ten sposób nie tylko powiesz mi, że zwracasz wartość, ale dokładnie tę wartość.
TryDoSomething () również nie jest złym wzorcem.
Jestem bardziej przyzwyczajony do tego, aby zobaczyć ten pierwszy w interakcjach z bazą danych, podczas gdy ten drugi jest bardziej wykorzystywany do analizowania.
Jak już powiedzieli inni, parametry flagi są zwykle zapachem kodu (zapachy kodu nie oznaczają, że robisz coś źle, tylko istnieje prawdopodobieństwo, że robisz to źle). Choć nazywasz to precyzyjnie, czasami zdarzają się niuanse, które utrudniają umiejętność posługiwania się tą flagą, i w rezultacie zaglądasz do wnętrza metody.
źródło
Podczas gdy inni sugerują inaczej, sugerowałbym, że posiadanie metody z parametrem wskazującym, jak należy obsługiwać błędy, jest lepsze niż wymaganie użycia osobnych metod dla dwóch scenariuszy użycia. Chociaż może być pożądane, aby również mieć różne punkty dostępu do błędów „generuje” w stosunku do „wskazanie błędu zwraca komunikat o błędzie” zastosowań, o punkt wejścia, które mogą służyć zarówno wzory użytkowania pozwala uniknąć powielania kodu w owinięciu metod. Jako prosty przykład, jeśli ktoś ma funkcje:
a ktoś chce funkcji, które zachowują się podobnie, ale z x zawiniętymi w nawiasy, konieczne byłoby napisanie dwóch zestawów funkcji otoki:
Jeśli istnieje argument za tym, jak obsługiwać błędy, potrzebny byłby tylko jeden pakiet:
Jeśli opakowanie jest wystarczająco proste, duplikacja kodu może nie być taka zła, ale jeśli kiedykolwiek będzie trzeba go zmienić, duplikowanie kodu będzie z kolei wymagało powielenia wszelkich zmian. Nawet jeśli ktoś zdecyduje się dodać punkty wejścia, które nie wykorzystują dodatkowego argumentu:
całą „logikę biznesową” można zachować w jednej metodzie - tej, która akceptuje argument wskazujący, co zrobić w przypadku awarii.
źródło
Myślę, że wszystkie odpowiedzi, które wyjaśniają, że nie powinieneś mieć flagi kontrolującej, czy wyjątek jest zgłaszany, czy nie są dobre. Ich rada byłaby moją radą dla każdego, kto odczuwa potrzebę zadania tego pytania.
Chciałem jednak zaznaczyć, że istnieje znaczący wyjątek od tej zasady. Gdy jesteś wystarczająco zaawansowany, aby rozpocząć tworzenie interfejsów API, które będą używane przez setki innych osób, są przypadki, w których możesz chcieć podać taką flagę. Kiedy piszesz API, nie piszesz tylko dla purystów. Piszesz do prawdziwych klientów, którzy mają prawdziwe pragnienia. Częścią twojego zadania jest uszczęśliwienie ich interfejsem API. Staje się to problemem społecznym, a nie programowym, a czasem problemy społeczne dyktują rozwiązania, które byłyby mniej niż idealne jako samodzielne rozwiązania programistyczne.
Może się okazać, że masz dwóch różnych użytkowników interfejsu API, z których jeden chce wyjątków, a drugi nie. Przykładem, w którym może się to zdarzyć, jest biblioteka używana zarówno w programowaniu, jak i systemie wbudowanym. W środowiskach programistycznych użytkownicy prawdopodobnie będą chcieli mieć wyjątki wszędzie. Wyjątki są bardzo popularne w przypadku nieoczekiwanych sytuacji. Są one jednak zabronione w wielu osadzonych sytuacjach, ponieważ są zbyt trudne do przeanalizowania ograniczeń w czasie rzeczywistym. Kiedy zależy ci nie tylko na średnim czasie wykonywania funkcji, ale także na czasie pojedynczej zabawy, pomysł odwrócenia stosu w dowolnym dowolnym miejscu jest bardzo niepożądany.
Przykład z życia: biblioteka matematyczna. Jeśli twoja biblioteka matematyczna ma
Vector
klasę, która obsługuje uzyskanie wektora jednostki w tym samym kierunku, musisz być w stanie podzielić przez wielkość. Jeśli wartość wynosi 0, masz sytuację dzielenia przez zero. W rozwoju chcesz je złapać . Naprawdę nie chcesz zaskakującego podziału przez kręcenie się 0. Zgłoszenie wyjątku jest bardzo popularnym rozwiązaniem tego problemu. To prawie nigdy się nie zdarza (jest to naprawdę wyjątkowe zachowanie), ale kiedy to się dzieje, chcesz to wiedzieć.Na platformie wbudowanej nie chcesz tych wyjątków. Bardziej sensowne byłoby sprawdzenie
if (this->mag() == 0) return Vector(0, 0, 0);
. W rzeczywistości widzę to w prawdziwym kodzie.Teraz pomyśl z perspektywy biznesu. Możesz spróbować nauczyć dwóch różnych sposobów korzystania z interfejsu API:
Jest to zgodne z opiniami większości odpowiedzi tutaj, ale z punktu widzenia firmy jest to niepożądane. Wbudowani programiści będą musieli nauczyć się jednego stylu kodowania, a programiści będą musieli nauczyć się innego. Może to być dość nieznośne i zmusza ludzi do jednego sposobu myślenia. Potencjalnie gorzej: jak udowadniasz, że wersja osadzona nie zawiera kodu zgłaszającego wyjątek? Wersja do rzucania może łatwo wtapiać się, ukrywając się gdzieś w kodzie, dopóki nie znajdzie go droga tablica oceny awarii.
Z drugiej strony, jeśli masz flagę, która włącza lub wyłącza obsługę wyjątków, obie grupy mogą nauczyć się dokładnie tego samego stylu kodowania. W rzeczywistości w wielu przypadkach możesz nawet użyć kodu jednej grupy w projektach drugiej grupy. W przypadku użycia tego kodu
unit()
, jeśli faktycznie zależy Ci na tym, co dzieje się w przypadku dzielenia przez 0, powinieneś sam napisać test. Tak więc, jeśli przeniesiesz go do systemu osadzonego, w którym po prostu uzyskasz złe wyniki, takie zachowanie będzie „rozsądne”. Teraz z perspektywy biznesowej raz szkolę programistów, którzy używają tych samych interfejsów API i tych samych stylów we wszystkich obszarach mojej działalności.W zdecydowanej większości przypadków chcesz postępować zgodnie z radami innych: używaj idiomów podobnych
tryGetUnit()
lub podobnych dla funkcji, które nie zgłaszają wyjątków, i zamiast tego zwracaj wartość wartownika, np. Null. Jeśli uważasz, że użytkownicy mogą chcieć korzystać zarówno z obsługi wyjątków, jak i obsługi wyjątków obok siebie w ramach jednego programu, trzymaj siętryGetUnit()
notacji. Istnieje jednak przypadek narożny, w którym rzeczywistość biznesowa może sprawić, że lepiej będzie użyć flagi. Jeśli znajdziesz się w tej narożnej skrzynce, nie bój się podrzucić „zasad” na marginesie. Do tego służą reguły!źródło