Parametr kontrolujący, czy zgłosić wyjątek, czy zwrócić wartość null - dobra praktyka?

24

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_failureparametrem.

/**
 * @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.

Don Kichot
źródło
5
Chociaż muszę pracować w PHP i jestem dość biegły, jest to po prostu zły język, a jego standardowa biblioteka jest wszędzie. Przeczytaj eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design w celach informacyjnych. Nic więc dziwnego, że niektórzy programiści PHP przyjmują złe nawyki. Ale jeszcze nie widziałem tego szczególnego zapachu.
Polygnome,
W udzielonych odpowiedziach brakuje mi istotnego punktu: kiedy użyłbyś jednego z nich. Użyłbyś metody wyjątku na wypadek, gdyby nie znaleziono elementu, który byłby nieoczekiwany i uznany za błąd (czas na panikę). Użyłbyś metody Try ... w przypadku, gdy brakujący element nie jest mało prawdopodobny i na pewno nie jest błędem, ale tylko możliwością uwzględnienia w normalnej kontroli przepływu.
Martin Maat,
1
@Polygnome Masz rację, standardowe funkcje biblioteczne są wszędzie, istnieje wiele złych kodów i istnieje problem jednego procesu na żądanie. Ale z każdą wersją jest coraz lepiej. Typy i model obiektowy wykonuje wszystkie lub większość rzeczy, których można się po nim spodziewać. Chcę zobaczyć ogólne, ko-i sprzeczności, typy połączeń zwrotnych, które określają podpis itp. Wiele z nich jest omawianych lub jest w trakcie implementacji. Myślę, że ogólny kierunek jest dobry, nawet jeśli standardowe dziwactwa w bibliotece nie znikną łatwo.
donquixote
4
Jedna sugestia: zamiast loadItemOrNull(id)zastanowić się, czy loadItemOr(id, defaultItem)ma to dla ciebie sens. Jeśli element jest ciągiem lub liczbą, często tak jest.
user949300,

Odpowiedzi:

35

Masz rację: dwie metody są o wiele lepsze z kilku powodów:

  1. 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.

  2. 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.

  3. 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 ( nullodpowiedź i wyjątek ), co znacznie utrudnia kod. Ponieważ wyboru nie dokonuje się w czasie wykonywania, ale podczas pisania kodu, dwie metody mają idealny sens.

  4. 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): intzgłasza wyjątek, podczas gdy int.TryParse(string, out int): boolnie. Ta konwencja nazewnictwa jest bardzo silna w społeczności .NET i należy jej przestrzegać, ilekroć kod pasuje do opisanej sytuacji.

Arseni Mourzenko
źródło
1
Dziękuję, to była odpowiedź, której szukałem! Celem było rozpoczęcie dyskusji na ten temat dla Drupal CMS, drupal.org/project/drupal/issues/2932772 , i nie chcę, żeby chodziło o moje osobiste preferencje, ale popieram je opiniami innych.
donquixote
4
W rzeczywistości od dawna tradycja przynajmniej .NET polega na tym, że NIE należy używać dwóch metod, ale zamiast tego wybrać właściwy wybór dla właściwego zadania. Wyjątek stanowią wyjątkowe okoliczności, które nie obsługują oczekiwanego przepływu kontroli. Nie można przeanalizować ciągu znaków jako liczby całkowitej? Nie jest to bardzo nieoczekiwana lub wyjątkowa okoliczność. int.Parse()jest uważana za naprawdę złą decyzję projektową, i właśnie dlatego zespół .NET zaczął wdrażać TryParse.
Voo,
1
Podanie dwóch metod zamiast myślenia o tym, z jakim przypadkiem użycia mamy do czynienia, jest łatwym wyjściem: nie myśl o tym, co robisz, zamiast tego ponosimy odpowiedzialność za wszystkich użytkowników interfejsu API. Jasne, że działa, ale to nie jest cechą dobrego projektu API (ani żadnego innego projektu w tej sprawie. Przeczytaj np. Podejście Joela w tej sprawie .
Voo
@ Voo Ważny punkt. Rzeczywiście, podanie dwóch metod stawia osobę dzwoniącą. Być może dla niektórych wartości parametrów element może istnieć, podczas gdy dla innych wartości parametrów nieistniejący element jest uważany za zwykły przypadek. Być może komponent jest używany w jednej aplikacji, w której zawsze oczekuje się, że elementy istnieją, oraz w innej aplikacji, w której nieistniejące elementy są uważane za normalne. Być może dzwoniący dzwoni ->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.
donquixote
1
Powód 1b: Niektóre języki (Haskell, Swift) wymagają obecności zerowania w podpisie. W szczególności Haskell ma zupełnie inny typ wartości zerowych ( data Maybe a = Just a | Nothing).
John Dvorak
9

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ę.

gnasher729
źródło
2

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:

//throws exceptions when error occurs
Item LoadItem(int id);

//returns false when error occurs
bool TryLoadItem(int id, out Item item)

przykładowe użycie (out oznacza przekazanie przez odwołanie niezainicjowane)

Item i;
if(TryLoadItem(3, i))
{
    Print(i.Name);
}
else
{
    Print("unable to load item :(");
}
Ewan
źródło
Przepraszam, zgubiłeś mnie tutaj. Jak wyglądałoby twoje „ bool TryMethodName(out returnValue)podejście” w oryginalnym przykładzie?
donquixote
edytowane w celu dodania przykładu
Ewan
1
Więc używasz parametru referencyjnego zamiast wartości zwracanej, więc masz wartość zwracaną za darmo dla sukcesu bool? Czy istnieje język programowania, który obsługuje to słowo kluczowe „out”?
donquixote
2
tak, przykro mi, nie zdawałem sobie sprawy, że „out” jest specyficzne dla c #
Ewan
1
nie zawsze nie. Ale co jeśli pozycja 3 jest zerowa? nie wiedziałbyś, czy wystąpił błąd, czy nie
Ewan
2

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ą.

Max
źródło
1
Nie wykluczałbym tego z reguły bez dodawania kwalifikacji lub niuansów. Usunięcie argumentu boolean jako może zmusić cię do napisania głupka, jeśli / else w innym miejscu, a zatem kodujesz dane w swoim pisanym kodzie, a nie w zmiennych.
Gerard
@ Gerard, czy możesz podać mi przykład?
Maks.
@Max Gdy masz zmienną boolowską 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.
BlackJack
@BlackJack no tak, mogę się z tobą zgodzić. Pomyślałem o przykładzie, if (myGrandmaMadeCookies) eatThem() else makeFood()który tak mógłbym zawrzeć w innej takiej funkcji checkIfShouldCook(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.
Maks.
1
Niuans, o którym mowa w moim komentarzu, odnosi się do „powinieneś zawsze”. Być może sformułuj to inaczej: „jeśli a, b i c mają zastosowanie, to [...]”. Wspomniana „reguła” jest świetnym paradygmatem, ale bardzo odmiennym w przypadku użycia.
Gerard,
1

Clean Code zaleca unikanie argumentów flagi logicznej, ponieważ ich znaczenie jest nieprzejrzyste w witrynie wywołania:

loadItem(id, true) // does this throw or not? We need to check the docs ...

podczas gdy osobne metody mogą używać wyrazistych nazw:

loadItemOrNull(id); // meaning is obvious; no need to refer to docs
meriton - podczas strajku
źródło
1

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ływanaOptional , 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:

Optional<Item> loadItem(int id);

W swojej metodzie albo zwrócisz, Optional.of(item)jeśli znajdziesz przedmiot, albo wrócisz Optional.empty(), jeśli nie. Nigdy nie rzucasz 2 i nigdy nie wracasz null.

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 z Optionaljawnej akcji, wymagana jest:

  • loadItem(1).get() rzuci NoSuchElementException lub zwróci znaleziony przedmiot.
  • Można również loadItem(1).orElseThrow(() -> new MyException("No item 1"))użyć niestandardowego wyjątku, jeśli zwrócony Optionaljest pusty.
  • loadItem(1).orElse(defaultItem)zwraca znaleziony lub przekazany element defaultItem(który może być również null) bez zgłaszania wyjątku.
  • loadItem(1).map(Item::getName) ponownie zwróci Optional z nazwą przedmiotu, jeśli jest obecny.
  • Jest jeszcze kilka metod, zobacz JavadocOptional .

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.

siegi
źródło
0

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.

public Widget GetWidgetOrNull(int id) {
    // All the lines to get your item, returning null if not found
}

public Widget GetWidget(int id) {
    var widget = GetWidgetOrNull(id);

    if (widget == null) {
        throw new WidgetNotFoundException();
    }

    return widget;
}
krillgar
źródło
0

Wolę dwie metody, w ten sposób nie tylko powiesz mi, że zwracasz wartość, ale dokładnie tę wartość.

public int GetValue(); // If you can't get the value, you'll throw me an exception
public int GetValueOrDefault(); // If you can't get the value, you'll return me 0, since it's the default for ints

TryDoSomething () również nie jest złym wzorcem.

public bool TryParse(string value, out int parsedValue); // If you return me false, I won't bother checking parsedValue.

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.

A Bravo Dev
źródło
-1

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:

Thing getThing(string x);
Thing tryGetThing (string x);

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:

Thing getThingWithBrackets(string x)
{
  getThing("["+x+"]");
}
Thing tryGetThingWithBrackets (string x);
{
  return tryGetThing("["+x+"]");
}

Jeśli istnieje argument za tym, jak obsługiwać błędy, potrzebny byłby tylko jeden pakiet:

Thing getThingWithBrackets(string x, bool returnNullIfFailure)
{
  return getThing("["+x+"]", returnNullIfFailure);
}

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:

Thing getThingWithBrackets(string x)
{
   return getThingWithBrackets(x, false);
}
Thing tryGetThingWithBrackets(string x)
{
   return tryGetThingWithBrackets(x, true);
}

całą „logikę biznesową” można zachować w jednej metodzie - tej, która akceptuje argument wskazujący, co zrobić w przypadku awarii.

supercat
źródło
Masz rację: Dekoratorzy i opakowania, a nawet regularne wdrażanie, będą miały pewne duplikacje kodu przy użyciu dwóch metod.
donquixote
Czy na pewno umieścisz wersje z wyjątkami i bez generowania wyjątków w osobnych pakietach i sprawisz, że opakowania będą ogólne?
Will Crawford,
-1

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 Vectorklasę, 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 sprawdzenieif (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:

// Development version - exceptions        // Embeded version - return 0s
Vector right = forward.cross(up);          Vector right = forward.cross(up);
Vector localUp = right.cross(forward);     Vector localUp = right.cross(forward);
Vector forwardHat = forward.unit();        Vector forwardHat = forward.tryUnit();
Vector rightHat = right.unit();            Vector rightHat = right.tryUnit();
Vector localUpHat = localUp.unit()         Vector localUpHat = localUp.tryUnit();

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!

Cort Ammon - Przywróć Monikę
źródło