如何执行输入验证而没有异常或冗余


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语句中的表达式和中的显式check 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-在我看来,这与最不惊奇的原则以及快速失败的原则完全相反。就像输入参数为null时返回null(然后忘记正确处理结果,当然,您好,无法解释的NPE)一样。
Maarten Bodewes

嗯,例外。对?
罗伯特·哈维

2
等一下 你是来这里咨询的吧?我要说的(以我看来不是那么微妙的方式)是那些例外是有意设计的,因此,如果要消除它们,您可能应该有充分的理由。顺便说一句,我同意amon的观点:您可能不应该拥有一个不能接受参数奇数的函数。
罗伯特·哈维

1
@MaartenBodewes:您必须记住,padToEvenWithIsEven 它不执行用户输入的验证。它对其输入执行有效性检查,以保护自己免受调用代码中的编程错误的影响。此验证需要进行的广泛程度取决于成本/风险分析,在此过程中,您将检查成本与编写调用代码的人传递错误参数的风险进行对比。
Bart van Ingen Schenau

Answers:


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

如果反复对值执行相同的验证,或者想只允许一个类型的值的子集,然后microtypes /微小的类型可以是有帮助的。对于诸如填充之类的通用实用程序来说,这不是一个好主意,但是,如果您的值在域模型中扮演特定角色,那么使用专用类型而不是原始值可能是向前迈出的一大步。在这里,您可以定义:

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如果您想进行验证,则无法摆脱异常之​​类的东西。好吧,替代方法是使用静态函数,该函数将返回经过验证的对象或在失败时返回null,但基本上没有任何优势。但是通过将验证从函数移到构造函数中,我们现在可以调用函数而不会出现验证错误。这样就可以给调用者所有控制权。例如:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
amon

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()使用do-while循环以更加惯用的Java风格进行编写。

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

return result.get();

然后,在您的其余代码中,您可以确定EvenInteger它确实是偶数,并且我们的偶数支票仅在中写入一次EvenInteger::of


通常,可选表示很好地表示了潜在的无效数据。这类似于大量的TryParse方法,它们同时返回数据和指示输入有效性的标志。配合时间检查。
巴思列夫斯(Basilevs)

1

如果验证的结果相同并且验证是在同一类中进行的,则两次验证是一个问题。那不是你的例子。在重构的代码中,第一个isEven检查完成是输入验证,失败将导致请求新的输入。第二个检查完全独立于第一个检查,因为它在公共方法padToEvenWithEven上,可以从类外部调用该方法,并且结果不同(例外)。

您的问题类似于意外地将相同的代码混淆为不干燥的问题。您将实现与设计混淆了。它们并不相同,仅仅是因为您有一条或十几条相同的线,并不意味着它们具有相同的用途,并且可以永远被认为是可互换的。另外,可能会让您的课程做得太多,但请跳过,因为这可能只是一个玩具示例...

如果这是性能问题,则可以通过创建一个不执行任何验证的私有方法来解决该问题,该方法将在验证后由您的公共padToEvenWithEven调用,而由您的其他方法代替。如果这不是性能问题,那么请让您的不同方法执行执行分配的任务所需的检查。


OP表示已完成输入验证以防止函数抛出。因此,检查完全依赖并且有意地相同。
D Drmmr

@DDrmmr:不,它们不依赖。该函数引发,因为那是合同的一部分。就像我说的,答案是创建一个私有方法,除了抛出异常外,它会执行所有操作,然后让公共方法调用私有方法。公共方法保留支票,在此支票有不同的用途-处理未经验证的输入。
jmoreno '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.