Як виконати перевірку вводу без винятків або надмірності


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все ще виконується перевірка параметрів, яка завжди поверне справжнє значення, як ми вже називали цю функцію.

Зараз, 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 Це, на мій погляд, - прямо проти принципу найменшого здивування, а також проти принципу невдалої швидкості. Це дуже схоже на повернення null, якщо вхідний параметр є null (а потім забувши правильно обробити результат, звичайно, привіт, незрозумілий NPE).
Maarten Bodewes

Ерго, виняток. Правильно?
Роберт Харві

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 всередині об'єкта, швидше за все, буде дорожче з точки зору продуктивності роботи, але використання типової системи на вашу користь може зменшити помилки та уточнити ваш дизайн.

Використання таких крихітних типів може бути навіть корисним навіть тоді, коли вони не виконують перевірку, наприклад, для роз'єднання рядків, що представляють FirstNameз a LastName. Я часто використовую цей зразок на мовах, що мають статичний тип.


Чи не все-таки ваша друга функція все ще є винятком у деяких випадках?
Роберт Харві

1
@RobertHarvey Так, наприклад у разі нульових покажчиків. Але тепер головна перевірка - чи число є парним - вимушена виходити з функції на відповідальність за абонента. Потім вони можуть вирішувати виняток таким чином, як вважають за потрібне. Я думаю, що відповідь менше зосереджується на позбавленні від усіх винятків, ніж на позбавленні від дублювання коду у валідаційному коді.
amon

1
Чудова відповідь. Звичайно, у Java покарання за створення класів даних є досить високим (багато додаткового коду), але це більше проблема мови, ніж представлена ​​вами ідея. Я здогадуюсь, що ви б не використовували це рішення для всіх випадків використання, коли моя проблема виникне, але це хороше рішення проти надмірного використання примітивів або для крайових випадків, коли вартість перевірки параметрів надзвичайно важка.
Maarten Bodewes

1
@MaartenBodewes Якщо ви хочете зробити перевірку, ви не можете позбутися чогось типу винятків. Ну, альтернативою є використання статичної функції, яка повертає перевірений об'єкт або null при відмові, але це, в основному, не має переваг. Але перемістивши валідацію з функції в конструктор, ми можемо викликати функцію без можливості помилки перевірки. Це дає користувачеві весь контроль. Напр .: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 з циклом "час роботи".

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

return result.get();

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


Необов'язково в цілому представляє потенційно недійсні дані досить добре. Це аналогічно безлічі методів TryParse, які повертають і дані, і прапор, що вказує на вхідну силу. З часом перевірки.
Василевс

1

Проведення перевірки двічі є проблемою, якщо результат перевірки однаковий І перевірка проводиться в тому ж класі. Це не ваш приклад. У вашому реконструйованому коді першою є перевірка, що робиться, це перевірка вводу, помилка призводить до запиту нового вводу. Друга перевірка є повністю незалежною від першої, оскільки вона знаходиться на відкритому методі padToEvenWithEven, який можна викликати поза класом і має інший результат (виняток).

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

Якщо це питання щодо продуктивності, ви можете вирішити проблему, створивши приватний метод, який не проводить ніякої перевірки, яку викликав ваш загальнодоступний padToEvenWithEven після його перевірки та який інший метод закликав би замість цього. Якщо це не проблема ефективності, то дозвольте вашим різним методам робити перевірки, необхідні для виконання покладених на них завдань.


ОП заявило, що перевірка вводу робиться для запобігання перекидання функції. Тож чеки повністю залежать і навмисно однакові.
D Drmmr

@DDrmmr: ні, вони не залежні. Функція кидає, тому що це частина її контракту. Як я вже сказав, відповідь полягає в тому, що створення приватного методу робить все, крім викиду винятку, а потім має відкритий метод викликати приватний метод. Публічний метод зберігає чек, де він виконує іншу мету - обробляти неправомірну інформацію.
jmoreno
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.