验证——通知代替抛错
先列出下面常用的验证代码
public void check() {
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
}
对一些数据运行一系列检查(这里只是涉及的类中的一些字段)。如果其中任何一项检查失败,就抛出一个异常,并返回错误消息。
对于这种验证我是不太喜欢用这种方式的。异常(Exception)表明代码行为超出了预期范围。但是像这种你在检查一些输入参数,这些错误都是你意料知道的会导致程序失败——如果一个失败行为是预期的,那么你就不应该使用异常。
第二个问题是,像这种一旦触发了一个异常,只是返回一次错误信息而不是所有的错误信息。也就是说这种最好的做法就是用一次输入就能报告所有错误信息。
我更倾向于选择用通知模式来导出所有验证错误信息。这个通知对象包含了所有的错误信息,你可以根据这个对象信息来获取这些验证信息,如下面代码:
private void validateNumberOfSeats(Notification note) {
if (numberOfSeats < 1) note.addError("number of seats must be positive");
// more checks like this
}
我们就可以像调用 Nofication.hasErrors()
就能获取其中的错误信息,如果验证不通过的话。这样我们就可以把那些详细的错误信息添加至这个对象中。
何时使用通知模式
我需要指出的是,我并不是提倡在代码中消除所有错误信息。异常(Exceptions)是一个非常有用的技术,他可以远离从主逻辑流程来处理异常行为。只有当异常发出的结果不是真正异常时,才可以使用这种重构,因此应该通过程序的主程序逻辑来处理。比如现在说的这个例子就是如此。
在考虑异常时,那些有经验的程序员给出了一条有用的经验法则:
我们认为异常不应该作为程序正常流程的一部分使用:异常应该为意外事件所用。假设一个未捕获的异常将终止你的程序,你要扪心自问:”如果我删除所有异常处理程序,这段代码还会运行吗?“,如果答案是,”不会“,那么异常(Exceptions)可能是在非异常情况下使用的。—— Dave Thomas and Andy Hunt
这样做的一个重要后果是,是否对特定任务使用异常取决于上下文。正如从不存在的文件中读取可能是异常,也可能不是异常,这取决于环境。又如,如果您试图读取一个众所周知的文件位置,例如 unix 系统上的 /etc/hosts,那么您可能会认为该文件应该在那里,因此找不到抛出异常是合理的。另一方面,如果您试图从用户在命令行中输入的路径读取文件,那么您就应该预料到该文件可能不存在,并应该使用另一种机制——一种传达错误的正常性质的机制(比如通知模式)。
例子
场景:接收前段传参来预定电影院座位,请求体有两个字段,我们就来验证这两个字段,演出日期字段和座位数字段
// class BookingRequest
private Integer numberOfSeats;
private String date;
下面是我的一个验证
// class BookingRequest
public void check() {
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
}
构建通知
因为通知对象要存储错误信息,所以首先要创建一个通知对象来容纳错误信息,通知对象一定要尽量简单
public class Notification {
private List<String> errors = new ArrayList<>();
public void addError(String message) { errors.add(message); }
public boolean hasErrors() {
return ! errors.isEmpty();
}
拆分验证方法
一般是要分两个部分,一部分是内部验证,处理通知以及阻止抛出任何异常,另一部分是外部验证,阻止当前的验证方法抛出可能存在的任何检查(check)失败
public void check() {
validation();
}
public void validation() {
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
}
然后我只需要调整 validation 方法返回 Notification 对象即可。
// class BookingRequest
public Notification validation() {
Notification note = new Notification();
if (date == null) throw new IllegalArgumentException("date is missing");
LocalDate parsedDate;
try {
parsedDate = LocalDate.parse(date);
}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);
}
if (parsedDate.isBefore(LocalDate.now())) throw new IllegalArgumentException("date cannot be before today");
if (numberOfSeats == null) throw new IllegalArgumentException("number of seats cannot be null");
if (numberOfSeats < 1) throw new IllegalArgumentException("number of seats must be positive");
return note;
}
测试代码
// class BookingRequest
public void check() {
if (validation().hasErrors())
throw new IllegalArgumentException(validation().errorMessage());
}
优化,提取错误类
很多时候我们返回错误并不只是一个字符串的信息,还有其它更详尽的错误附加信息等。所以我们可以将错误信息独立成一个错误类
// class Notification
private static class Error {
String message;
Exception cause;
private Error(String message, Exception cause) {
this.message = message;
this.cause = cause;
}
}
现在就变为了
// class Notification…
private List<Error> errors = new ArrayList<>();
public void addError(String message, Exception e) {
errors.add(new Error(message, e));
}
public String errorMessage() {
return errors.stream()
.map(e -> e.message)
.collect(Collectors.joining(", "));
}