Jak przeprowadzić sprawdzanie poprawności danych wejściowych bez wyjątków lub nadmiarowości

11

Kiedy próbuję utworzyć interfejs dla określonego programu, zwykle staram się unikać zgłaszania wyjątków, które zależą od niepoprawnych danych wejściowych.

Tak więc często zdarza się, że myślałem o takim kodzie (ten przykład jest tylko przykładem, nie przejmuj się funkcją, którą wykonuje, przykład w Javie):

public static String padToEvenOriginal(int evenSize, String string) {
    if (evenSize % 2 == 1) {
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, powiedzmy, że evenSizetak naprawdę pochodzi od danych wejściowych użytkownika. Nie jestem więc pewien, czy tak jest. Ale nie chcę wywoływać tej metody z możliwością zgłoszenia wyjątku. Dlatego wykonuję następującą funkcję:

public static boolean isEven(int evenSize) {
    return evenSize % 2 == 0;
}

ale teraz mam dwie kontrole, które wykonują tę samą weryfikację danych wejściowych: wyrażenie w ifinstrukcji i jawne sprawdzenie isEven. Duplikat kodu, niezbyt fajny, więc przeróbmy:

public static String padToEvenWithIsEven(int evenSize, String string) {
    if (!isEven(evenSize)) { // to avoid duplicate code
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, to rozwiązało, ale teraz dochodzimy do następującej sytuacji:

String test = "123";
int size;
do {
    size = getSizeFromInput();
} while (!isEven(size)); // checks if it is even
String evenTest = padToEvenWithIsEven(size, test);
System.out.println(evenTest); // checks if it is even (redundant)

teraz mamy nadmiarowe sprawdzenie: już wiemy, że wartość jest parzysta, ale padToEvenWithIsEvennadal wykonuje sprawdzenie parametru, które zawsze zwróci true, ponieważ już wywołaliśmy tę funkcję.

Teraz isEvenoczywiście nie stanowi to problemu, ale jeśli sprawdzanie parametrów jest bardziej kłopotliwe, może to wiązać się z nadmiernymi kosztami. Poza tym wykonywanie zbędnego połączenia po prostu nie wydaje się właściwe.

Czasami możemy obejść ten problem, wprowadzając „typ zatwierdzony” lub tworząc funkcję, w której ten problem nie może wystąpić:

public static String padToEvenSmarter(int numberOfBigrams, String string) {
    int size = numberOfBigrams * 2;
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append('x');
    }
    return sb.toString();
}

ale wymaga to mądrego myślenia i dość dużego refaktora.

Czy istnieje (bardziej) ogólny sposób, w jaki możemy uniknąć zbędnych wywołań isEveni sprawdzania podwójnych parametrów? Chciałbym, aby rozwiązanie nie wywoływało padToEvennieprawidłowego parametru, wyzwalając wyjątek.


Bez wyjątków nie mam na myśli programowania bez wyjątków, to znaczy, że dane wprowadzone przez użytkownika nie wyzwalają wyjątku zgodnie z projektem, podczas gdy sama funkcja ogólna nadal zawiera sprawdzanie parametrów (choćby w celu ochrony przed błędami programowania).

Maarten Bodewes
źródło
Czy tylko próbujesz usunąć wyjątek? Możesz to zrobić, zakładając, jaki powinien być rzeczywisty rozmiar. Na przykład, jeśli zdasz 13, dodaj do 12 lub 14 i po prostu unikaj czeku. Jeśli nie możesz przyjąć jednego z tych założeń, utkniesz w wyjątku, ponieważ parametru nie można użyć, a funkcja nie może być kontynuowana.
Robert Harvey
@RobertHarvey To - moim zdaniem - jest sprzeczne z zasadą najmniejszego zaskoczenia, a także z zasadą szybkiego niepowodzenia. Przypomina to zwrócenie wartości NULL, jeśli parametr wejściowy ma wartość NULL (a następnie zapomnienie o prawidłowej obsłudze wyniku, oczywiście, witam niewyjaśnione NPE).
Maarten Bodewes
Ergo, wyjątek. Dobrze?
Robert Harvey
2
Czekaj, co? Przyszedłeś tu po radę, prawda? Mówię (na mój pozornie nie tak subtelny sposób), że te wyjątki są z założenia, więc jeśli chcesz je wyeliminować, prawdopodobnie powinieneś mieć dobry powód. Nawiasem mówiąc, zgadzam się z amonem: prawdopodobnie nie powinieneś mieć funkcji, która nie akceptuje liczb nieparzystych dla parametru.
Robert Harvey
1
@ MaartenBodewes: Musisz pamiętać, że padToEvenWithIsEven nie wykonuje się weryfikacji danych wejściowych użytkownika. Wykonuje sprawdzenie poprawności na wejściu, aby zabezpieczyć się przed błędami programowania w kodzie wywołującym. To, jak obszerna musi być ta walidacja, zależy od analizy kosztów / ryzyka, w której koszt kontroli jest porównywany z ryzykiem, że osoba pisząca kod wywołujący przekaże nieprawidłowy parametr.
Bart van Ingen Schenau

Odpowiedzi:

7

W twoim przykładzie najlepszym rozwiązaniem jest użycie bardziej ogólnej funkcji wypełniania; jeśli dzwoniący chce wyrównać rozmiar, może sam to sprawdzić.

public static String padString(int size, String string) {
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

Jeśli wielokrotnie przeprowadzasz tę samą weryfikację wartości lub chcesz zezwolić tylko na podzbiór wartości danego typu, wówczas mikrotypy / małe typy mogą być pomocne. W przypadku narzędzi ogólnego przeznaczenia, takich jak wypełnianie, nie jest to dobry pomysł, ale jeśli twoja wartość odgrywa szczególną rolę w modelu domeny, użycie dedykowanego typu zamiast pierwotnych wartości może być wielkim krokiem naprzód. Tutaj możesz zdefiniować:

final class EvenInteger {
  public final int value;

  public EvenInteger(int value) {
    if (!(value % 2 == 0))
      throw new IllegalArgumentException("EvenInteger(" + value + ") is not even");
    this.value = value;
  }
}

Teraz możesz zadeklarować

public static String padStringToEven(EvenInteger evenSize, String string)
    ...

i nie musisz dokonywać żadnej wewnętrznej weryfikacji. W przypadku tak prostego testu owijania int wewnątrz obiektu będzie prawdopodobnie droższe pod względem wydajności w czasie wykonywania, ale użycie systemu typów na swoją korzyść może zmniejszyć błędy i wyjaśnić projekt.

Używanie tak małych typów może być przydatne nawet wtedy, gdy nie przeprowadzają one sprawdzania poprawności, np. W celu ujednoznacznienia łańcucha reprezentującego FirstNamea LastName. Często używam tego wzorca w językach o typie statycznym.

amon
źródło
Czy twoja druga funkcja nadal nie generuje wyjątku w niektórych przypadkach?
Robert Harvey
1
@RobertHarvey Tak, na przykład w przypadku wskaźników zerowych. Ale teraz główna kontrola - czy numer jest parzysty - jest wypychana z funkcji na odpowiedzialność osoby dzwoniącej. Następnie mogą poradzić sobie z wyjątkiem w sposób, który uznają za odpowiedni. Myślę, że odpowiedź skupia się mniej na usuwaniu wszystkich wyjątków niż na usuwaniu duplikacji kodu w kodzie sprawdzania poprawności.
amon
1
Świetna odpowiedź. Oczywiście w Javie kara za tworzenie klas danych jest dość wysoka (dużo dodatkowego kodu), ale jest to bardziej problem języka niż pomysł, który przedstawiłeś. Sądzę, że nie użyłbyś tego rozwiązania we wszystkich przypadkach użycia, w których wystąpiłby mój problem, ale jest to dobre rozwiązanie przeciwko nadużywaniu prymitywów lub w przypadkach krawędzi, w których koszt sprawdzenia parametrów jest wyjątkowo wysoki.
Maarten Bodewes
1
@ MaartenBodewes Jeśli chcesz przeprowadzić weryfikację, nie możesz pozbyć się czegoś takiego jak wyjątki. Cóż, alternatywą jest użycie funkcji statycznej, która zwraca sprawdzony obiekt lub zero w przypadku awarii, ale zasadniczo nie ma żadnych zalet. Ale przenosząc sprawdzanie poprawności z funkcji do konstruktora, możemy teraz wywoływać funkcję bez możliwości wystąpienia błędu sprawdzania poprawności. Daje to dzwoniącemu pełną kontrolę. Np .:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
amon
1
@MaartenBodewes Duplikowanie sprawdzania odbywa się cały czas. Na przykład przed próbą zamknięcia drzwi możesz sprawdzić, czy są już zamknięte, ale same drzwi również nie pozwolą na ponowne zamknięcie, jeśli już jest. Po prostu nie ma wyjścia, z wyjątkiem sytuacji, gdy zawsze ufasz, że osoba dzwoniąca nie wykonuje żadnych nieprawidłowych operacji. W twoim przypadku możesz mieć unsafePadStringToEvenoperację, która nie wykonuje żadnych kontroli, ale wydaje się, że to zły pomysł, aby uniknąć sprawdzania poprawności.
plalx
9

Jako rozszerzenie odpowiedzi @ amon, możemy połączyć jego EvenIntegerz tym, co społeczność programistów funkcjonalnych mogłaby nazwać „Smart Constructor” - funkcją, która otacza głupi konstruktor i upewnia się, że obiekt jest w poprawnym stanie (robimy głupie klasa konstruktora lub moduł / pakiet prywatny w językach innych niż klasy, aby upewnić się, że używane są tylko inteligentne konstruktory). Sztuczka polega na zwróceniu Optional(lub odpowiedniku), aby uczynić funkcję bardziej złożoną.

public final class EvenInteger {
    private final int value;

    private EvenInteger(value) {
        this.value = value;
    }

    public static Optional<EvenInteger> of(final int value) {
        if (value % 2 == 0) {
            return Optional.of(new EvenInteger(value));
        }
        return Optional.empty();
    }

    public int getValue() {
        return this.value;
    }
}

Następnie możemy łatwo użyć standardowych Optionalmetod do zapisania logiki wejściowej:

class GetEvenInput {
    public Optional<EvenInteger> askOnce() {
        int size = getSizeFromInput();
        return EvenInteger.of(size);
    }

    public EvenInteger keepAsking() {
        return askOnce().orElseGet(() -> keepAsking());
    }
}

Możesz także pisać keepAsking()w bardziej idiomatycznym stylu Java z pętlą do-while.

Optional<EvenInteger> result;
do {
    result = askOnce();
} while (!result.isPresent());

return result.get();

Następnie w pozostałej części kodu możesz polegać na EvenIntegerpewności, że naprawdę będzie on parzysty, a nasza kontrola parzystości została napisana tylko raz EvenInteger::of.

walpen
źródło
Opcjonalnie ogólnie dość dobrze reprezentuje potencjalnie nieprawidłowe dane. Jest to analogiczne do wielu metod TryParse, które zwracają zarówno dane, jak i flagę wskazującą ważność danych wejściowych. Z kontrolami czasu wykonania.
Basilevs,
1

Dwukrotne sprawdzenie poprawności stanowi problem, jeśli wynik sprawdzania poprawności jest taki sam ORAZ sprawdzanie poprawności odbywa się w tej samej klasie. To nie jest twój przykład. W refaktoryzowanym kodzie pierwszym sprawdzeniem jest nawet sprawdzenie poprawności danych wejściowych, awaria powoduje żądanie nowej danych wejściowych. Drugie sprawdzenie jest całkowicie niezależne od pierwszego, ponieważ odbywa się na publicznej metodzie padToEvenWithEven, która może być wywołana spoza klasy i ma inny wynik (wyjątek).

Twój problem jest podobny do problemu pomylenia przypadkowo identycznego kodu z nie-suchym. Mylicie implementację z projektem. Nie są takie same, a to, że masz jedną lub kilkanaście linii, które są takie same, nie oznacza, że ​​służą one temu samemu celowi i można je na zawsze traktować zamiennie. Zapewne twoja klasa robi zbyt wiele, ale pomiń to, bo to prawdopodobnie tylko zabawkowy przykład ...

Jeśli jest to problem z wydajnością, możesz rozwiązać problem, tworząc prywatną metodę, która nie wykonuje żadnej weryfikacji, którą publiczna platforma PadToEvenWithEven wywołała po sprawdzeniu poprawności i którą zamiast tego wywołałaby inna metoda. Jeśli nie jest to problem z wydajnością, pozwól innym metodom wykonać kontrole wymagane do wykonania przypisanych zadań.

jmoreno
źródło
OP stwierdził, że weryfikacja danych wejściowych jest wykonywana, aby zapobiec wyrzuceniu funkcji. Czeki są więc całkowicie zależne i celowo takie same.
D Drmmr
@DDrmmr: nie, nie są zależne. Funkcja rzuca, ponieważ jest to część jej kontraktu. Jak powiedziałem, odpowiedź polega na utworzeniu metody prywatnej, która robi wszystko oprócz rzucenia wyjątku, a następnie wywołania metody publicznej przez metodę prywatną. Metoda publiczna zachowuje czek, gdy służy do innego celu - obsługi nieważnych danych wejściowych.
jmoreno