Как выполнить проверку входных данных без исключений или избыточности

11

Когда я пытаюсь создать интерфейс для конкретной программы, я обычно стараюсь избегать исключений, которые зависят от неподтвержденного ввода.

Поэтому часто случается так, что я думаю о таком фрагменте кода (это просто пример ради примера, не обращайте внимания на функцию, которую он выполняет, пример в Java):

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();
}

Хорошо, так сказать, что evenSizeна самом деле происходит от ввода пользователя. Так что я не уверен, что это даже. Но я не хочу вызывать этот метод с вероятностью возникновения исключения. Поэтому я выполняю следующую функцию:

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

но теперь у меня есть две проверки, которые выполняют одинаковую проверку входных данных: выражение в ifоператоре и явная регистрация isEven. Дублирующий код, не очень хороший, так что давайте сделаем рефакторинг:

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();
}

Хорошо, это решило проблему, но теперь мы попадаем в следующую ситуацию:

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)

Теперь у нас есть избыточная проверка: мы уже знаем, что значение четное, но padToEvenWithIsEvenвсе еще выполняет проверку параметров, которая всегда будет возвращать true, как мы уже вызывали эту функцию.

Конечно, сейчас isEvenпроблем не возникает, но если проверка параметров более громоздкая, то это может повлечь за собой слишком большие затраты. Кроме того, выполнение избыточного вызова просто не кажется правильным.

Иногда мы можем обойти это, введя «проверенный тип» или создав функцию, где эта проблема не может возникнуть:

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();
}

но это требует умного мышления и довольно большого рефакторинга.

Есть ли (более) общий способ, которым мы можем избежать избыточных вызовов isEvenи выполнять двойную проверку параметров? Я бы хотел, чтобы решение не вызывало padToEvenневерный параметр, вызывая исключение.


Без исключений я не имею в виду программирование без исключений, я имею в виду, что пользовательский ввод не вызывает исключение по конструкции, в то время как сама универсальная функция все еще содержит проверку параметров (если только для защиты от ошибок программирования).

Мартен Бодевес
источник
Вы просто пытаетесь удалить исключение? Вы можете сделать это, сделав предположение о фактическом размере. Например, если вы передадите 13, наберите 12 или 14 и просто избегайте чек. Если вы не можете сделать одно из этих предположений, то вы застряли с исключением, потому что параметр не может использоваться, и ваша функция не может продолжаться.
Роберт Харви
@RobertHarvey Это, на мой взгляд, идет вразрез с принципом наименьшего удивления, а также с принципом быстрого провала. Это очень похоже на возврат нуля, если входной параметр равен нулю (и затем забыл правильно обработать результат, конечно, привет необъяснимый NPE).
Maarten Bodewes
Ergo, исключение. Правильно?
Роберт Харви
2
Чего ждать? Вы пришли сюда за советом, верно? То, что я говорю (по-моему, не так уж и тонко), это то, что эти исключения созданы специально, поэтому, если вы хотите устранить их, у вас, вероятно, должна быть веская причина. Кстати, я согласен с amon: у вас, вероятно, не должно быть функции, которая не может принимать нечетные числа для параметра.
Роберт Харви
1
@MaartenBodewes: Вы должны помнить, что padToEvenWithIsEven не выполняет проверку ввода пользователя. Он выполняет проверку достоверности своего ввода, чтобы защитить себя от ошибок программирования в вызывающем коде. Насколько обширной должна быть эта проверка, зависит от анализа затрат / рисков, где вы сравниваете стоимость проверки с риском того, что лицо, пишущее код вызова, передает неверный параметр.
Барт ван Инген Шенау

Ответы:

7

В случае вашего примера лучшее решение - использовать более общую функцию заполнения; если вызывающий абонент хочет увеличить размер до четного, он может проверить это самостоятельно.

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();
}

Если вы неоднократно выполняете одну и ту же проверку значения или хотите разрешить только подмножество значений типа, то микротипы / крошечные типы могут быть полезны. Для утилит общего назначения, таких как заполнение, это не очень хорошая идея, но если ваше значение играет определенную роль в вашей доменной модели, использование выделенного типа вместо примитивных значений может стать большим шагом вперед. Здесь вы можете определить:

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;
  }
}

Теперь вы можете объявить

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

и не нужно делать никаких внутренних проверок. Для такого простого теста упаковка int внутри объекта, вероятно, будет более дорогой с точки зрения производительности во время выполнения, но использование системы типов в ваших интересах может уменьшить количество ошибок и прояснить ваш дизайн.

Использование таких крошечных типов может быть даже полезным, даже если они не выполняют проверку, например, для устранения неоднозначности строки, представляющей a FirstNameиз a LastName. Я часто использую этот шаблон в статически типизированных языках.

Амон
источник
Разве ваша вторая функция все еще не выдает исключение в некоторых случаях?
Роберт Харви
1
@RobertHarvey Да, например, в случае нулевых указателей. Но теперь главная проверка - четность номера - вытеснена из функции и входит в обязанности звонящего. Затем они могут иметь дело с исключением так, как считают нужным. Я думаю, что ответ фокусируется не столько на избавлении от всех исключений, сколько от дублирования кода в проверочном коде.
Амон
1
Отличный ответ. Конечно, в Java штраф за создание классов данных довольно высок (много дополнительного кода), но это больше проблема языка, чем идеи, которую вы представили. Я предполагаю, что вы не будете использовать это решение для всех случаев использования, в которых может возникнуть моя проблема, но это хорошее решение против чрезмерного использования примитивов или для крайних случаев, когда стоимость проверки параметров исключительно сложна.
Maarten Bodewes
1
@MaartenBodewes Если вы хотите сделать проверку, вы не можете избавиться от чего-то вроде исключений. Что ж, альтернатива состоит в том, чтобы использовать статическую функцию, которая возвращает проверенный объект или ноль при сбое, но это в принципе не имеет преимуществ. Но переместив валидацию из функции в конструктор, мы теперь можем вызвать функцию без возможности ошибки валидации. Это дает звонящему весь контроль. Например:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
Амон
1
@MaartenBodewes Проверка дубликатов происходит постоянно. Например, прежде чем пытаться закрыть дверь, вы можете проверить, если она уже закрыта, но сама дверь также не позволит ее закрыть, если она уже есть. Выхода из этого просто нет, если только вы не уверены, что вызывающая сторона не выполняет недопустимых операций. В вашем случае у вас может быть unsafePadStringToEvenоперация, которая не выполняет никаких проверок, но кажется плохой идеей просто избегать проверки.
plalx
9

В качестве дополнения к ответу @ amon мы можем объединить его EvenIntegerс тем, что сообщество функционального программирования может назвать «умным конструктором» - функцией, которая обертывает тупой конструктор и гарантирует, что объект находится в допустимом состоянии (мы делаем тупой класс конструктора или в языковых модулях, не относящихся к классу, закрытый модуль / пакет, чтобы убедиться, что используются только умные конструкторы). Хитрость заключается в том, чтобы вернуть Optional(или эквивалент), чтобы сделать функцию более компонуемой.

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;
    }
}

Затем мы можем легко использовать стандартные Optionalметоды для написания логики ввода:

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

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

Вы также можете написать keepAsking()в более идиоматическом стиле Java с циклом do-while.

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

return result.get();

Тогда в остальной части вашего кода вы можете EvenIntegerс уверенностью полагаться, что он действительно будет четным, а наша проверка была записана только один раз, в EvenInteger::of.

walpen
источник
Необязательный в общем случае представляет потенциально недействительные данные достаточно хорошо. Это аналогично множеству методов TryParse, которые возвращают как данные, так и флаг, указывающий достоверность ввода. С проверками времени выполнения.
Базилевс
1

Выполнение проверки дважды является проблемой, если результат проверки совпадает И проверка выполняется в том же классе. Это не твой пример. В вашем рефакторинговом коде первая проверка isEven - это проверка правильности ввода, в результате сбоя запрашивается новый ввод. Вторая проверка полностью независима от первой, так как она выполняется для открытого метода padToEvenWithEven, который может быть вызван извне класса и имеет другой результат (исключение).

Ваша проблема похожа на проблему того, что случайно идентичный код путают с несухим. Вы путаете реализацию с дизайном. Они не одинаковы, и то, что у вас одна или дюжина одинаковых строк, не означает, что они служат одной и той же цели и могут навсегда считаться взаимозаменяемыми. Кроме того, возможно, ваш класс делает слишком много, но пропустите это, поскольку это, вероятно, просто игрушечный пример ...

Если это проблема с производительностью, вы можете решить эту проблему, создав закрытый метод, который не выполняет никакой проверки, который ваш public padToEvenWithEven вызвал после выполнения проверки, и который вместо этого будет вызывать ваш другой метод. Если это не проблема производительности, то пусть ваши разные методы выполняют проверки, которые им требуются для выполнения назначенных им задач.

jmoreno
источник
ОП заявил, что проверка входных данных выполняется для предотвращения выброса функции. Таким образом, проверки полностью зависят и намеренно совпадают.
D Drmmr
@DDrmmr: нет, они не зависят. Функция выбрасывает, потому что это является частью ее контракта. Как я уже сказал, ответ заключается в том, что создание приватного метода делает все, кроме генерации исключения, а затем вызывает открытый метод для вызова приватного метода. Открытый метод сохраняет проверку, где он служит другой цели - обрабатывать неподтвержденный ввод.
Jmoreno