在我以前的文章中,我写了一个输入验证设计,该设计取代了难以维护和测试的 if-else块。 但是,正如某些读者指出的那样,它有一个缺点–如果输入数据有多个验证错误,则用户将不得不多次提交请求以查找所有错误。 从可用性的角度来看,这不是一个好的设计。
当我们发现验证错误时,抛出异常的另一种方法是返回一个包含错误的Notification对象。 这将使我们能够在用户输入上运行所有验证规则,并同时捕获所有违规行为。 Martin Fowler 撰写了一篇文章,详细介绍了该方法。 我强烈建议您继续阅读,如果您还没有读过的话。
在本文中,我将重构以前的实现,以使用错误通知对象来验证用户输入。
第一步,我将创建一个ErrorNotification对象,该对象封装了我的应用程序错误-
public class ErrorNotification {private List<String> errors = new ArrayList<>();public void addError(String message) {this.errors.add(message);}public boolean hasError() {return !this.errors.isEmpty();}public String getAllErrors() {return this.errors.stream().collect(joining(", "));}
}
然后,我将更改OrderItemValidator接口以返回ErrorNotification对象–
public interface OrderItemValidator {ErrorNotification validate(OrderItem orderItem);
}
然后更改所有实现以适应新的返回类型。
最初,我将更改所有实现以返回一个空的错误对象,以便摆脱编译错误。 例如,我将通过以下方式更改ItemDescriptionValidator –
class ItemDescriptionValidator implements OrderItemValidator {@Overridepublic ErrorNotification validate(OrderItem orderItem) {ErrorNotification errorNotification = new ErrorNotification();Optional.ofNullable(orderItem).map(OrderItem::getDescription).map(String::trim).filter(description -> !description.isEmpty()).orElseThrow(() -> new IllegalArgumentException("Item description should be provided"));return errorNotification;}
}
修复编译错误之后,我现在将开始用每个验证器中的通知消息替换异常。 为此,我将首先修改相关测试以反映我的意图,然后修改验证器以通过测试。
让我们从ItemDescriptionValidatorTest类开始–
public class ItemDescriptionValidatorTest {@Testpublic void validate_descriptionIsNull_invalid() {ItemDescriptionValidator validator = new ItemDescriptionValidator();ErrorNotification errorNotification = validator.validate(new OrderItem());assertThat(errorNotification.getAllErrors()).isEqualTo("Item description should be provided");}@Testpublic void validate_descriptionIsBlank_invalid() {OrderItem orderItem = new OrderItem();orderItem.setDescription(" ");ItemDescriptionValidator validator = new ItemDescriptionValidator();ErrorNotification errorNotification = validator.validate(new OrderItem());assertThat(errorNotification.getAllErrors()).isEqualTo("Item description should be provided");}@Testpublic void validate_descriptionGiven_valid() {OrderItem orderItem = new OrderItem();orderItem.setDescription("dummy description");ItemDescriptionValidator validator = new ItemDescriptionValidator();ErrorNotification errorNotification = validator.validate(orderItem);assertThat(errorNotification.getAllErrors()).isEmpty();}
}
当我运行这些测试时,只有其中一项通过,而其中两项失败,这是预料之中的。 我现在将修改验证器代码以通过测试–
class ItemDescriptionValidator implements OrderItemValidator {static final String MISSING_ITEM_DESCRIPTION = "Item description should be provided";@Overridepublic ErrorNotification validate(OrderItem orderItem) {ErrorNotification errorNotification = new ErrorNotification();Optional.ofNullable(orderItem).map(OrderItem::getDescription).map(String::trim).filter(description -> !description.isEmpty()).ifPresentOrElse(description -> {},() -> errorNotification.addError(MISSING_ITEM_DESCRIPTION));return errorNotification;}
}
我对上面的ifPresentOrElse方法的使用感到不舒服。 我在这里使用它的主要原因是因为Optionals没有ifNotPresent方法之类的东西,该方法允许我仅在不存在该值的情况下才采取措施(请向读者提出要求-如果您知道一种更好的方法,为此,请发表评论!)。
进行此重构后, ItemValidatorTest类中的所有测试均通过测试。 大!
现在,让我们重构MenuValidatorTest类中的测试–
public class MenuValidatorTest {@Testpublic void validate_menuIdInvalid_invalid() {OrderItem orderItem = new OrderItem();String menuId = "some menu id";orderItem.setMenuId(menuId);MenuRepository menuRepository = mock(MenuRepository.class);when(menuRepository.menuExists(any())).thenReturn(false);MenuValidator validator = new MenuValidator(menuRepository);ErrorNotification errorNotification = validator.validate(orderItem);assertThat(errorNotification.getAllErrors()).isEqualTo(String.format(MenuValidator.INVALID_MENU_ERROR_FORMAT, menuId));}@Testpublic void validate_menuIdNull_invalid() {MenuRepository menuRepository = mock(MenuRepository.class);when(menuRepository.menuExists(any())).thenReturn(true);MenuValidator validator = new MenuValidator(menuRepository);ErrorNotification errorNotification = validator.validate(new OrderItem());assertThat(errorNotification.getAllErrors()).isEqualTo(MenuValidator.MISSING_MENU_ERROR);}@Testpublic void validate_menuIdIsBlank_invalid() {OrderItem orderItem = new OrderItem();orderItem.setMenuId(" \t");MenuRepository menuRepository = mock(MenuRepository.class);when(menuRepository.menuExists(any())).thenReturn(true);MenuValidator validator = new MenuValidator(menuRepository);ErrorNotification errorNotification = validator.validate(orderItem);assertThat(errorNotification.getAllErrors()).isEqualTo(MenuValidator.MISSING_MENU_ERROR);}@Testpublic void validate_menuIdValid_validated() {OrderItem orderItem = new OrderItem();String menuId = "some menu id";orderItem.setMenuId(menuId);MenuRepository menuRepository = mock(MenuRepository.class);when(menuRepository.menuExists(menuId)).thenReturn(true);MenuValidator validator = new MenuValidator(menuRepository);ErrorNotification errorNotification = validator.validate(orderItem);assertThat(errorNotification.getAllErrors()).isEmpty();}
}
然后是MenuValidator类–
@RequiredArgsConstructor
class MenuValidator implements OrderItemValidator {private final MenuRepository menuRepository;static final String MISSING_MENU_ERROR = "A menu item must be specified.";static final String INVALID_MENU_ERROR_FORMAT = "Given menu [%s] does not exist.";@Overridepublic ErrorNotification validate(OrderItem orderItem) {ErrorNotification errorNotification = new ErrorNotification();Optional.ofNullable(orderItem.getMenuId()).map(String::trim).filter(menuId -> !menuId.isEmpty()).ifPresentOrElse(validateMenuExists(errorNotification),() -> errorNotification.addError(MISSING_MENU_ERROR));return errorNotification;}private Consumer<String> validateMenuExists(ErrorNotification errorNotification) {return menuId -> {if (!menuRepository.menuExists(menuId)) {errorNotification.addError(String.format(INVALID_MENU_ERROR_FORMAT, menuId));}};}
}
等等。
修改了各个验证器之后,我现在将修改Composite以收集单个订单商品的所有错误–
@RequiredArgsConstructor
class OrderItemValidatorComposite implements OrderItemValidator {private final List<OrderItemValidator> validators;@Overridepublic ErrorNotification validate(OrderItem orderItem) {ErrorNotification errorNotification = new ErrorNotification();validators.stream().map(validator -> validator.validate(orderItem)).forEach(errorNotification::addAll);return errorNotification;}
}
为此,我在ErrorNotification类中添加了一个名为addAll的新方法,该方法基本上从另一个ErrorNotification对象复制所有错误。
最后,我现在将修改服务方法以收集订单中所有订单项的所有错误消息–
@Service
@Slf4j
@RequiredArgsConstructor
class OrderService {private final OrderItemValidator validator;void createOrder(OrderDTO orderDTO) {ErrorNotification errorNotification = new ErrorNotification();orderDTO.getOrderItems().stream().map(validator::validate).forEach(errorNotification::addAll);if (errorNotification.hasError()) {throw new IllegalArgumentException(errorNotification.getAllErrors());}log.info("Order {} saved", orderDTO);}
}
进行此更改会导致OrderServiceIT中的测试之一失败,因为当价格无效时,它专门查找原因设置为NumberFormatException的异常。 重构后,我们可以安全地删除此检查,因为它不再相关。
本文的完整源代码已推送到GitHub (特定的提交URL在此处 )。
翻译自: https://www.javacodegeeks.com/2017/11/replacing-exceptions-error-notifications-input-validation-java.html