编写验证代码时使用什么代替异常?

时间:2019-03-06 12:04:09

标签: validation oop language-agnostic

我正在编写一些验证代码,不确定如何将验证消息传递回调用代码。

想到了异常,但是我认为异常不应在用户输入验证中使用。正如@Blowdart所说的:

  

没有例外   控制流机制。用户经常会输入错误的密码,这不是   例外情况。异常应该是真正罕见的事情,   UserHasDiedAtKeyboard类型的情况。

来自:https://stackoverflow.com/a/77175/125938。我将此情绪扩展到用户可能输入的所有“不正确”用户输入。

所以问题是要使用什么代替异常。在某些情况下,我只能使用IsValid…方法来返回布尔值以确保有效性,但是如果我想将错误消息传递回去怎么办?是否应该使用Message属性创建自定义“ ValidationError”对象?哪些因素有意义并引起“最低惊讶度”(最好是经过实践检验的模式)?

3 个答案:

答案 0 :(得分:1)

如果我要以一种真正的面向对象的方式进行操作,那么我将坚持关注点分离原则,并组成一系列类,每个类分别处理输入-验证-输出过程中的单独步骤。 / p>

假设我们正在从用户输入的字符串中解析日期。

我的第一堂课将封装原始值并尝试解析日期(伪代码):

class TextualDate {
   public TextualDate(string value) {
      // just initialize with the provided value
   }

   public Option<Date> AsDate() {
      // try parsing and either return the date or not
      // the Option<Date> type is here to suggest that the conversion might not succeed
   }
}

接下来,我将有一个验证类,该实例化TextualDate类,调用其AsDate()方法并返回验证结果:

class ValidatedDate {
  public ValidatedDate(TextualDate value) {
    // initialize with the provided value
   _textualDate = value;
  }

  private TextualDate _textualDate;

  public ValidationResult Validated {
    var maybeDate = _textualDate.AsDate();
    // see whether we have a date or not
    return new ValidationResult(...);
  }
}

在ValidationResult类中,我们可能会找到一些状态属性(“确定”,“失败”),任何直接提供的错误消息或作为在消息目录中进行查找的键之类的错误消息。

这样,我们可以隔离问题,仅处理UI层上的错误消息,同时能够独立使用和重用验证逻辑。

答案 1 :(得分:1)

  

用户经常会输入错误的密码,这不是例外情况。

是,不是。是否引发异常取决于您要询问的问题。在登录用户的过程中,在得出是否可以登录用户的结论之前,通常会询问很多问题。您将代码分解成专门部分的次数越多,在其中某些部分引发异常就越有意义。

假设您在HTTP上下文中以以下方式指定登录过程:

  1. 从请求中获取用户名*和密码*。
  2. 通过用户名从数据库中获取用户记录*。
  3. 检查记录的密码*是否等于*输入的密码。
  4. 如果是,请开始会话。
  5. 如果以上任何步骤均未成功完成,请输出相应的错误消息。

上方标有星号的所有项目都可能失败

  1. 请求中可能不包含用户名或密码。
  2. 此用户名可能没有用户记录,或者数据库可能已关闭。
  3. 由于任何原因,记录可能没有密码和/或已损坏。出于任何原因,存储的密码可能会使用不受支持的哈希算法,因此无法进行比较。

很明显,在此过程中,有许多情况都是理想的例外情况。如果密码只是假的,测试密码的实际功能可能抛出异常。那应该是一个布尔值。但是由于其他多种原因,它仍然可能会引发异常。如果正确使用了异常,则最终将得到如下所示的代码(伪-伪代码):

try {
    username = request.get('username')
    password = request.get('password')
    user = db.get(username=username)
    if (user.password.matches(password)) {
        session.start()
    } else {
        print 'Nope, try again'
    }
} catch (RequestDoesNotHaveThisDataException) {
    logger.info('Invalid request')
    response.status(400)
} catch (UserRecordNotFoundException) {
    print 'Nope, try again'
} catch (UnsupportedHashingAlgorithmException, PasswordIsNullException) {
    logger.error('Invalid password hash for user ' + user.id)
    response.status(500)
    print 'Sorry, please contact our support staff'
} catch (DatabaseDownException e) {
    // mostly for illustration purposes, 
    // this exception should probably not even be caught here
    logger.exception('SEND HALP!')
    throw e
}

是的,这是一个非常简单的过程,但是从字面上看,每一步都有一个或多个例外情况。您问“用户在请求中发送的用户名是什么?”这个问题,,如果由于用户未发送任何用户名而导致该问题没有答案,则您的情况很特殊。与尝试使用if..else覆盖每种情况相比,异常在这里大大简化了控制流程。

  

如果用户名无效或密码不正确,也不例外。
  (根据您引用的答案。)

如您所见,我们正在尝试通过从数据库中获取用户名记录来测试用户名是否“有效”。如果我们有一个旨在从数据库中获取用户记录的函数,并且没有这样的记录,那么异常是完全有效的响应。如果我们定义该函数来测试,则是否存在这样的记录,并且nullfalse是有效的返回值……可以。但是在这种情况下,我们并没有这样写,坦率地说,这导致我发现控制流程更简单。

现在,只有密码验证本身不会使用 例外,因为询问的问题是“此密码与该密码匹配吗?” ,答案为何?可以明确地为。同样,只有出现了诸如不受支持的哈希算法之类的异常情况时,该问题才能得到答案,并且完全有例外。

说了这么多,您可能会注意到,除了数据库中真正致命的情况以外,大多数情况不会外在地导致异常。这里的组件期望并处理某些其子组件认为异常的情况。此处的代码正在询问问题,并准备将Mu处理为其中的一些问题。这就是说,“不应在流程X,Y或Z中使用例外,因为它不够特殊”的一般规则太教条了。是否允许例外取决于每个代码段的目的。


说了这么多,您要问的是某种形式的验证。上面的代码显示了两个数据可能各自无效的情况,并且最终使用异常仍然导致“是”或“否”响应。您当然可以将其封装在这样的对象中:

val = new LoginFormValidator()
val.setDataFromRequest(request)
val.validate()

if (val.isValid) {
    print 'Hurray'
} else {
    print 'You have errors:'

    for (error in val.errors) {
        print error.fieldName + ': ' + error.reason
    }
}

此验证器是否在内部使用了您不需要关心的任何异常,但最后,它将所有异常保存为内部结果的“是”或“否”结果,您可以从那里获取它们要么作为汇总(val.isValid),要么单独(for (error in val.errors))。

答案 2 :(得分:1)

过去我也面临着类似的困境-我不得不编写一些服务来从第三者那里获取数据,以各种方式对其进行处理,然后将该数据发送给其他服务以进行进一步处理。
所有这些服务都可能由于错误或不完整的数据而失败了,但是这也不是意外的,也不例外-我拒绝在这些情况下使用异常。
我进行了广泛的研究,在两天内阅读了可以在该主题上学到的所有知识,最后决定了以下内容:

一种方法可能需要返回数据,而可能不需要(在Visual Basic中为sub,在Java / C#中为void)-但是在两种情况下,我都希望指示成功/失败和潜在的错误消息。

如果您选择的语言支持元组,则可以从方法中返回元组:

public (bool Success, string ErrorMessage) DoSomething()
{
    // implementation here
}

public (bool Success, someType Value, string ErrorMessage) DoSomething()
{
    // implementation here
}

如果没有,则可以执行我的操作(那是c#5-因此没有值元组)并创建结果类:

public class Result
{
    public static Result Success()
    {
        return new Result(true, null);
    }

    public static Result Fail(string errorMessage)
    {
        return new Result(false, errorMessage);
    }

    protected Result(bool success, string errorMessage)
    {
        Success = success;
        ErrorMessage = errorMessage;
    }

    public bool Success {get; private set;}
    public string ErrorMessage {get; private set;}  
}

public class Result<T>
{
    public static Result<T> Success(T value)
    {
        return new Result(true, null, value);
    }

    public new static Result<T> Fail(string errorMessage)
    {
        return new Result(false, errorMessage, default(T));
    }

    private Result<T>(bool success, string errorMessage, T value)
        : base(success, errorMessage)
    {
        Value = value;
    }

    public T Value {get; private set;}
}

并像这样使用它:

public Result CouldBeVoid()
{
    bool IsOk;
    // implementation

    return IsOk ? 
    Result.Success() : 
    Result.Fail("Something went wrong") ;

}


public Result<int> CouldBeInt()
{
    bool IsOk;
    // implementation

    return IsOk ? 
    Result.Success(intValue) : 
    Result.Fail("Something went wrong") ;
}


var result = CouldBeVoid();
if(!result) 
    // do something with error message

var result = CouldBeInt()

if(result)
    // do something with int value
else
    // do something with error message