Czy metoda powinna wybaczać z przekazanymi argumentami? [Zamknięte]

21

Załóżmy, że mamy metodę, foo(String bar)która działa tylko na ciągach, które spełniają określone kryteria; na przykład musi być małymi literami, nie może być pusty lub mieć tylko białe znaki i musi pasować do wzorca [a-z0-9-_./@]+. Dokumentacja metody podaje te kryteria.

Czy metoda powinna odrzucić wszelkie odchylenia od tych kryteriów, czy też powinna bardziej wybaczać niektóre kryteria? Na przykład, jeśli początkową metodą jest

public void foo(String bar) {
    if (bar == null) {
        throw new IllegalArgumentException("bar must not be null");
    }
    if (!bar.matches(BAR_PATTERN_STRING)) {
        throw new IllegalArgumentException("bar must match pattern: " + BAR_PATTERN_STRING);
    }
    this.bar = bar;
}

A drugą metodą wybaczania jest

public void foo(String bar) {
    if (bar == null) {
        throw new IllegalArgumentException("bar must not be null");
    }
    if (!bar.matches(BAR_PATTERN_STRING)) {
        bar = bar.toLowerCase().trim().replaceAll(" ", "_");
        if (!bar.matches(BAR_PATTERN_STRING) {
            throw new IllegalArgumentException("bar must match pattern: " + BAR_PATTERN_STRING);
        }
    }
    this.bar = bar;
}

Czy dokumentacja powinna zostać zmieniona, aby oświadczyć, że zostanie przekształcona i ustawiona na przekształconą wartość, jeśli to możliwe, czy też metoda powinna być tak prosta, jak to możliwe i odrzucać wszelkie odchylenia? W takim przypadku barmoże zostać ustawiony przez użytkownika aplikacji.

Podstawowym przypadkiem użycia tego są użytkownicy uzyskujący dostęp do obiektów z repozytorium za pomocą określonego identyfikatora ciągu. Każdy obiekt w repozytorium powinien mieć unikalny ciąg identyfikujący go. Te repozytoria mogą przechowywać obiekty na różne sposoby (serwer sql, json, xml, binarny itp.), Więc próbowałem zidentyfikować najniższy wspólny mianownik, który pasowałby do większości konwencji nazewnictwa.

Zymus
źródło
1
Prawdopodobnie zależy to w dużej mierze od przypadku użycia. Każdy z nich może być rozsądny, a nawet widziałem klasy, które zapewniają obie metody i sprawiają, że użytkownik decyduje. Czy mógłbyś wyjaśnić, co ma zrobić ta metoda / klasa / dziedzina, abyśmy mogli udzielić prawdziwej porady?
Ixrec,
1
Czy znasz wszystkich, którzy wywołują tę metodę? Podobnie jak w przypadku jej zmiany, czy możesz w wiarygodny sposób zidentyfikować wszystkich klientów? Jeśli tak, wybrałbym tak tolerancyjne i wybaczające, na ile pozwalają na to problemy z wydajnością. Mogę również usunąć dokumentację. Jeśli nie, i jest to część biblioteki API, upewnię się, że kod zaimplementował dokładnie reklamowany interfejs API, ponieważ w przeciwnym razie zmiana kodu w celu dopasowania go do dokumentacji może generować raporty o błędach.
Jon Chesterfield
7
Możesz argumentować, że Separation of Concerns mówi, że jeśli to konieczne, powinieneś mieć ścisłą foofunkcję, która jest rygorystyczna pod względem akceptowanych argumentów, i mieć drugą funkcję pomocniczą, która może próbować „wyczyścić” argument, z którego chcesz skorzystać foo. W ten sposób każda metoda ma mniej do zrobienia sama, a ich zarządzanie i integracja są czystsze. Idąc tą drogą, prawdopodobnie pomocne byłoby również odejście od projektu ciężkiego od wyjątków; możesz użyć czegoś takiego jak Optionalzamiast, a następnie mieć funkcje, które zużywają foowyjątki rzucania, jeśli to konieczne.
gntskn
1
To jest jak pytanie „ktoś mnie skrzywdził, czy powinienem mu wybaczyć?” Oczywiście są okoliczności, w których jedno lub drugie jest odpowiednie. Programowanie może nie być tak skomplikowane jak relacje międzyludzkie, ale z pewnością jest na tyle skomplikowane, że taka powszechna recepta nie zadziała.
Kilian Foth,
2
@Boggin Chciałbym również zwrócić uwagę na ponownie rozważoną zasadę solidności . Trudność pojawia się, gdy trzeba rozszerzyć implementację, a wybaczająca implementacja prowadzi do dwuznacznego przypadku rozszerzonej implementacji.

Odpowiedzi:

47

Twoja metoda powinna robić to, co mówi.

Zapobiega to błędom, zarówno w użyciu, jak i późniejszym zmienianiu zachowania przez opiekunów. Oszczędza czas, ponieważ opiekunowie nie muszą poświęcać tyle czasu na zastanawianie się, co się dzieje.

To powiedziawszy, jeśli zdefiniowana logika nie jest przyjazna dla użytkownika, być może powinna zostać poprawiona.

Telastyn
źródło
8
To jest klucz. Jeśli twoja metoda robi dokładnie to, co mówi, koder wykorzystujący tę metodę zrekompensuje określony przypadek użycia. Nigdy nie rób czegoś nieudokumentowanego tą metodą tylko dlatego, że uważasz ją za przydatną. Jeśli chcesz to zmienić, napisz kontener lub zmień dokumentację.
Nelson
Dodam do komentarza @ Nelson, że metody nie należy projektować w próżni. Jeśli koderzy twierdzą, że będą go używać, ale będą kompensować, a ich kompensacje mają wartość ogólnego przeznaczenia, rozważ włączenie ich do klasy. (Np. Mają fooi fooForUncleanStringmetody, w których ten drugi dokonuje poprawek przed przekazaniem go do pierwszego.)
Blrfl
20

Jest kilka punktów:

  1. Twoje wdrożenie musi robić to, co stanowi udokumentowana umowa i nie powinno robić nic więcej.
  2. Prostota jest ważna, zarówno dla kontraktu, jak i dla realizacji, choć więcej dla tych pierwszych.
  3. Próba poprawienia błędnych danych wejściowych zwiększa złożoność, sprzeczną z intuicją, nie tylko w przypadku kontraktowania i wdrażania, ale także użytkowania.
  4. Błędy należy wychwytywać wcześnie, tylko jeśli poprawi to debuggowanie i nie wpłynie zbytnio na wydajność.
    Pamiętaj, że w trybie debugowania znajdują się stwierdzenia dotyczące debugowania, które w większości łagodzą wszelkie problemy z wydajnością.
  5. Wydajność, o ile pozwala na to dostępny czas i pieniądze bez nadmiernego kompromisu w zakresie prostoty, jest zawsze celem.

Jeśli wdrożysz interfejs użytkownika, przyjazne komunikaty o błędach (w tym sugestie i inna pomoc) są częścią dobrego projektu.
Pamiętaj jednak, że interfejsy API są przeznaczone dla programistów, a nie użytkowników końcowych.


Prawdziwym eksperymentem polegającym na rozmyciu i pobłażaniu przy wprowadzaniu danych jest HTML.
W rezultacie wszyscy robili to nieco inaczej, a specyfikacja, teraz udokumentowana, jest gigantyczną książką pełną specjalnych przypadków.
Zobacz prawo Postela („ Bądź konserwatywny w tym, co robisz, bądź liberalny w tym, co akceptujesz od innych. ”) I krytyk dotykający tego ( Lub o wiele lepszy, o którym MichaelT mnie uświadomił ).

Deduplikator
źródło
Kolejny krytyczny fragment autora sendmaila:
15

Zachowanie metody powinno być jasne, intuicyjne, przewidywalne i proste. Ogólnie rzecz biorąc, powinniśmy bardzo wahać się, czy wykonać dodatkowe przetwarzanie danych wejściowych osoby dzwoniącej. Takie domysły na temat tego, co zamierzał dzwoniący, niezmiennie mają wiele przypadków skrajnych, które powodują niepożądane zachowanie. Rozważ operację tak prostą jak połączenie ścieżki pliku. Wiele (a może nawet większość) funkcji łączenia ścieżek plików po cichu odrzuca wszystkie poprzednie ścieżki, jeśli jedna z łączonych ścieżek wydaje się być zrootowana! Na przykład /abc/xyzpołączone z /evilspowoduje tylko /evil. Prawie nigdy nie zamierzam tego robić, dołączając ścieżki plików, ale ponieważ nie ma interfejsu, który nie zachowałby się w ten sposób, jestem zmuszony albo mieć błędy, albo napisać dodatkowy kod opisujący te przypadki.

To powiedziawszy, zdarzają się rzadkie sytuacje, w których sensowna jest metoda „wybaczania”, ale osoba dzwoniąca powinna zawsze decydować, kiedy i czy te etapy przetwarzania mają zastosowanie do ich sytuacji. Kiedy więc zidentyfikujesz wspólny krok przetwarzania wstępnego, który chcesz zastosować do argumentów w różnych sytuacjach, powinieneś udostępnić interfejsy dla:

  • Surowa funkcjonalność, bez wstępnego przetwarzania.
  • Przebiegiem wyprzedzającym krok sama .
  • Połączenie surowej funkcjonalności i przetwarzania wstępnego.

Ostatni jest opcjonalny; powinieneś go podać tylko wtedy, gdy z niego skorzysta duża liczba połączeń.

Ujawnienie surowej funkcjonalności daje dzwoniącemu możliwość korzystania z niej bez etapu wstępnego przetwarzania, gdy zajdzie taka potrzeba. Samo ujawnienie kroku preprocesora pozwala wywołującemu użyć go w sytuacjach, w których nawet nie wywołuje funkcji lub gdy chce przetworzyć niektóre dane wejściowe przed wywołaniem funkcji (na przykład, gdy najpierw chce przekazać ją do innej funkcji). Zapewnienie kombinacji umożliwia dzwoniącym wywoływanie obu bez żadnych kłopotów, co jest przydatne przede wszystkim, jeśli większość dzwoniących użyje go w ten sposób.

jpmc26
źródło
2
+1 za przewidywalne. I kolejne +1 (chciałbym) za proste. Wolałbym, żebyś pomógł mi dostrzec i poprawić moje błędy, niż próbować je ukryć.
John M Gant,
4

Jak powiedzieli inni, dopasowanie łańcucha „wybaczenie” oznacza wprowadzenie dodatkowej złożoności. Oznacza to więcej pracy przy wdrażaniu dopasowania. Na przykład masz teraz o wiele więcej przypadków testowych. Musisz wykonać dodatkową pracę, aby upewnić się, że w przestrzeni nazw nie ma semantycznie równych nazw. Większa złożoność oznacza również, że w przyszłości będzie więcej błędów. Prostszy mechanizm, taki jak rower, wymaga mniej konserwacji niż bardziej skomplikowany, taki jak samochód.

Czy więc łagodne dopasowywanie ciągów jest warte wszystkich tych dodatkowych kosztów? Zależy to od przypadku użycia, jak zauważyli inni. Jeśli ciągi są jakimś zewnętrznym wejściem, nad którym nie masz kontroli, a łagodne dopasowanie jest wyraźną zaletą, być może warto. Być może dane wejściowe pochodzą od użytkowników końcowych, którzy mogą nie być zbyt sumienni w kwestii znaków kosmicznych i wielkich liter, a Ty masz silną motywację, aby ułatwić korzystanie z produktu.

Z drugiej strony, gdyby dane wejściowe pochodziły z, powiedzmy, plików właściwości zebranych przez ludzi technicznych, którzy powinni to zrozumieć "Fred Mertz" != "FredMertz", byłbym bardziej skłonny do zaostrzenia dopasowania i zaoszczędzić koszty rozwoju.

W każdym razie uważam, że przycinanie i ignorowanie wiodących i końcowych spacji ma wartość - widziałem zbyt wiele godzin marnowanych na debugowanie tego rodzaju problemów.

mat_noshi
źródło
3

Wspominasz o kontekście, z którego pochodzi to pytanie.

Biorąc to pod uwagę, chciałbym, aby metoda zrobiła tylko jedną rzecz: określa wymagania dla łańcucha, niech wykona się na tej podstawie - nie próbowałbym go tutaj transformować. Uprość to i wyjaśnij; udokumentuj to i staraj się zachować synchronizację dokumentacji i kodu.

Jeśli chcesz przekształcić dane pochodzące z bazy danych użytkowników w bardziej wybaczający sposób, umieść tę funkcjonalność w osobnej metodzie transformacji i udokumentuj powiązaną funkcjonalność .

W pewnym momencie wymagania funkcji muszą zostać spełnione, jasno udokumentowane, a wykonanie musi być kontynuowane. „Przebaczenie” w jego punkcie jest nieco nieme, jest to decyzja projektowa i argumentowałbym za funkcją, która nie zmienia swojego argumentu. Zmutowanie funkcji wejścia powoduje ukrycie części sprawdzania poprawności, która byłaby wymagana od klienta. Posiadanie funkcji, która powoduje mutację, pomaga klientowi zrobić to dobrze.

Duży nacisk tutaj jest klarowność i dokumentów, co kod robi .

Niall
źródło
-1
  1. Możesz nazwać metodę według akcji takiej jak doSomething (), takeBackUp ().
  2. Aby ułatwić utrzymanie, możesz zachować wspólne umowy i zatwierdzać różne procedury. Zadzwoń do nich zgodnie z przypadkami użycia.
  3. Programowanie defensywne: twoja procedura obsługuje szeroki zakres danych wejściowych, w tym (Minimalne rzeczy, które są przypadkami użycia, muszą zostać uwzględnione)
użytkownik435491
źródło