65.9K
CodeProject 正在变化。 阅读更多。
Home

SOA 上的业务验证技术

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2013 年 4 月 15 日

CPOL

6分钟阅读

viewsIcon

15665

关于企业应用程序业务验证的观点。

介绍 

大多数计算机系统最初设计为由少数客户端(如 UI)和一些批处理进程访问。当应用程序接口数量增长时(例如允许 RMI、WebServices、ResFull 等),验证甚至业务规则变得复杂,有时开发人员需要重构一些软件以保持一致性。

有些系统应适应在不同环境中处理不同的业务规则,这些规则可能会影响传入命令的验证,验证机制变得越来越复杂。  

虽然有些框架指导我们以特定方式实现数据验证,但大多数时候决定由架构师做出。这些示例将展示一些需要考虑的基本验证原则。

背景  

第一个建议是:通过 MVC 实现对系统的每次外部访问。
MVC 模式在每个部分承担一些与验证相关的职责: 

  • 视图负责数据收集,应收集足够的信息以在控制器上执行命令,并且没有强制责任验证任何输入值。请考虑可以访问我们的控制器但没有任何输入验证的批处理进程。
    为了改善与人类用户交互的视图上的用户交互,最好进行一些输入类型数据检查,以防止空值、避免无效数据类型或检查正确的数据格式等。这种人类用户验证旨在防止不正确的命令调用。
  • 控制器负责获取请求的值,检查正确的数据类型并在需要时执行数据转换。控制器还可以检查输入数据的一些架构限制,例如最大值、最小值、长度、无效字符或空值等验证。利用这些信息,控制器填充命令值对象并在模型中执行命令。
  • 模型接收具有预期正确数据类型的请求命令,因此其职责是在应用任何系统状态更改之前检查每个业务规则和每个数据类型的架构限制。 

你知道“胖模型,瘦控制器”的概念吗?它基本上是说控制器应该尽可能简单,并将其行为限制在类似以下内容:

  • 解析请求的数据类型 
  • 调用模型
  • 处理模型响应
  • 构建视图的上下文
  • 调用视图

我将使用一个简单的概念来关注验证。

为了简化实现,并且不处理任何特定框架问题,我将使用 Servlet 实现控制器,每个人都可以将此解决方案应用于最常用的框架和模型访问技术。

简单解决方案 

假设我们有一个允许客户购买商品的销售门户,我们将处理购买商品的过程,实体由:客户、商品和购买表示。

控制器由 BuyController 表示。

控制器与模型交互的一种简单方式可能是

控制器 

 /**
 * This routine will handle the post mechanism of a UI view. 
 * The post will process the buy item commit.
 */
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // TODO: Perform security/access validations
    
    
    // FIRST VALIDATION BLOCK.
    Map<String, String> dataTypeValidationErrors = new HashMap<String, String>();
    
    // Validates the correct data type of itemId, also check that an item is selected.
    Integer itemCode = NumberUtils.toInt(request.getParameter("itemId"), 0);
    if(itemCode == 0) {
        dataTypeValidationErrors.put("itemIdError", "Select an item to buy.");
    }

    // Validates how much items will buy.
    Integer itemQuantity = NumberUtils.toInt(request.getParameter("itemQuantity"), 0);
    if(itemQuantity == 0) {
        dataTypeValidationErrors.put("itemQuantityError", "Select how many items will buy.");
    }
    
    // And validate the payment method.
    Integer paymentMethodCode = NumberUtils.toInt(request.getParameter("paymentMethodId"), 0);
    if(paymentMethodCode == 0) {
        dataTypeValidationErrors.put("paymentMethodIdError", "Select the payment method.");
    }
    
    // On basic errors, early reject to the view, with the error messages
    if(dataTypeValidationErrors.size() > 0) {
        for(String key: dataTypeValidationErrors.keySet()) {
            request.setAttribute(key, dataTypeValidationErrors.get(key));
        }
        // TODO: Redirect to same view, with the error messages.
    }

    
    // SECOND VALIDATION BLOCK
    // Perform business validations on items.
    dataTypeValidationErrors = buyModelService.validateBuyItem(itemCode, itemQuantity);
    if(dataTypeValidationErrors.size() > 0) {
        for(String key: dataTypeValidationErrors.keySet()) {
            request.setAttribute("itemQuantityError", dataTypeValidationErrors.get(key));
        }
    }
    
    // Perform business validations on payment method.
    dataTypeValidationErrors = buyModelService.validatePaymentMethod(paymentMethodCode);
    if(dataTypeValidationErrors.size() > 0) {
        for(String key: dataTypeValidationErrors.keySet()) {
            request.setAttribute("paymentMethodIdError", dataTypeValidationErrors.get(key));
        }
    }
    
    // On business errors, reject the process to the view, showing error messages.
    if(dataTypeValidationErrors.size() > 0) {
        for(String key: dataTypeValidationErrors.keySet()) {
            request.setAttribute(key, dataTypeValidationErrors.get(key));
        }
        // TODO: redirect to view.
    }

    // If everything in ok, call buyItem
    BuyItemCommand biCommand = new BuyItemCommand();
    biCommand.setItemId(itemCode);
    biCommand.setItemQuantity(itemQuantity);
    biCommand.setPaymentMethodId(paymentMethodCode);
    try {
        Buy performedBuy = buyModelService.buyItem(biCommand);
        request.setAttribute("buy", performedBuy);
        
        // TODO: redirect to the correct view informing that the buy was done correctly.
        
    } catch (BuyException | ParameterValidationException ex) {
        request.setAttribute("error", ex.getMessage());

        // TODO: redirect to the view.
    } catch (Exception ex) {
        // TODO: log the error and redirect to general failure view.
    }
} 

当控制器设置请求属性,例如“itemIdError”、“itemQuantityError”和“paymentMethodIdError”时,我将值绑定到在正确的 UI 表单位置显示错误消息的字段。“error”是一个通用错误,用于在 UI 中显示。


业务服务 

public Buy buyItem(BuyItemCommand biCommand) throws ParameterValidationException, BuyException {
    // Business validations will be sorted by complexity, throwing exceptions early.

    // Get Client from session info.
    Customer currentCustomer = sessionInfo.getCurrentCustomer();
    if(currentCustomer == null) {
        throw new SecurityException("Client not logged In.");
    }	
    
    // Validate if Item Exists.
    Item item = itemRepository.get(biCommand.getItemId());
    if(item == null) {
        throw new ParameterValidationException("itemId", "Select a valid Item to Buy.");
    }
    
    // Validate payment Method
    PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
    if(paymentMethod == null) {
        throw new ParameterValidationException("paymentMethodId", "Select a valid payment method.");
    }
	
    // Perform business validations on items.
    if(validateBuyItem(item, biCommand.getItemQuantity()).size() > 0) {
        throw new ParameterValidationException("itemId", "Select a valid Item to Buy.");
    }
    if(validatePaymentMethod(paymentMethod).size() > 0) {
        throw new ParameterValidationException("paymentMethodId", "Select a valid payment method.");
    }

    
    // MORE VALIDATIONS : Example : Validate if the item, customer, quantity and payment method is possible.
    if(validatePaymentMethodForItem(paymentMethod, item).size() > 0) {
        throw new BuyException("You cannot buy this item with the specified payment method.");
    }
    
    // This simplified logic will store the buy in repository.
    BuyBuilder buyBuilder = new BuyBuilder();
    buyBuilder.setCustomer(currentCustomer);
    buyBuilder.setItem(item);
    buyBuilder.setPaymentMetod(paymentMethod);
    buyBuilder.setQuantity(biCommand.getItemQuantity());
    
    return buyRepository.add(buyBuilder.build());
}  

在前面的示例中,验证和规则在三个块中进行检查
  • 第一个块中的验证是关于传入数据类型的。
  • 第二个块中的验证在业务层执行,并且与业务规则相关。
  • 最后,如果发生意外故障,buyItem 方法会抛出异常。

第一个块是不可避免的,每个控制器都应该验证数据输入并使其适应调用正确的命令。实现一些库以统一架构层中的转换是一个好主意。

第二个块验证业务规则,但也定义了验证流程,这在控制器中是不希望做的。假设验证根据上下文而变化,验证过程应该具有复杂的逻辑,并且在每个控制器中不必要地实现。

此外,有时 UI 团队与模型团队不同,将验证流程转移到控制器团队意味着模型开发人员和 UI 开发人员之间的交互,这是可以避免的。

基本上,问题在于模型需要在 buyItem 上执行的验证流程已转移到控制器。

验证处理器

更好的设计是实现一个业务验证处理器来验证命令,这将根据 buyItem 事件执行的上下文遵循验证流程。这并不意味着字段验证应该消失,当环境需要客户端实时执行的每个数据输入的验证时,验证处理器可以暴露方法来验证每个 UI 字段数据,但是当用户发送命令以提交操作时,服务器应该按需运行正确的验证。

控制器 (Controller)

/**
 * The post will commit the buy.
 */
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // TODO: Perform security/access validations
    
    // Validates the parameters.
    Map<String, String> basicValidationErr = validateInputDataTypes(request);
    if(basicValidationErr.size() > 0) {
        for(String key: basicValidationErr.keySet()) {
            request.setAttribute(key, basicValidationErr.get(key));
        }
        // TODO: return to view with error messages.
    }

    // Build the Buy Request.
    BuyItemCommand biCommand = new BuyItemCommand();
    biCommand.setItemId(NumberUtils.toInt(request.getParameter("itemCode")));
    biCommand.setItemQuantity(NumberUtils.toInt(request.getParameter("itemQuantity")));
    biCommand.setPaymentMethodId(NumberUtils.toInt(request.getParameter("paymentMethodCode")));
    
    // Validates the Request calling the business validator.
    Map<String, String> bssValidationErr = buyItemValidator.validateBuyItem(biCommand);
    if(bssValidationErr.size() > 0) {
        for(String key: bssValidationErr.keySet()) {
            if(key.equals("itemCode")) {
                request.setAttribute("itemCodeError", bssValidationErr.get(key));
            } else if (key.equals("itemQuantity")) {
                request.setAttribute("itemQuantityError", bssValidationErr.get(key));
            } else if (key.equals("paymentMethodCode")) {
                request.setAttribute("paymentMethodCodeError", bssValidationErr.get(key));
            } 
        }
        
        //TODO: return to the view with the error messages
    }
    
    // Call the business proceess
    try {
        Buy performedBuy = buyModelService.buyItem(biCommand);
        request.setAttribute("buy", performedBuy);
        
        // TODO: redirect to the correct view informing that the buy was done correctly.
        
    } catch (BuyException | ParameterValidationException ex) {
        request.setAttribute("error", ex.getMessage());

        // TODO: return to the view showing the error message.
    } catch (Exception ex) {
        // TODO: log the error and redirect to general failure view.
    }
}  

验证器 (BuyItemValidator.java)
/**
 * Validates the buyItem operation
 * 
 * @param biCommand The buy command
 * @return Map of biCommand properties bounded errors.
 */
public Map<String, String> validateBuyItem(BuyItemCommand biCommand) {
     Map<String, String> problems = new HashMap<String, String>();
     
    // Validate if Item Exists.
    Item item = itemRepository.get(biCommand.getItemId());
    if(item == null) {
        problems.put("itemId", "Select a valid Item to Buy.");
    }
    
    // Validate payment Method
    PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
    if(paymentMethod == null) {
        problems.put("paymentMethodId", "Select a valid payment method.");
    }

    // If some problem is detected here, return early.
    if(!problems.isEmpty()) {
        return problems;
    }
        	 
    // Perform business validations on items.
    if(validateItemQuantity(item, biCommand.getItemQuantity()).size() > 0) {
        problems.put("itemId", "Select a valid Item to Buy.");
    }
    if(validatePaymentMethod(paymentMethod).size() > 0) {
        problems.put("paymentMethodId", "Select a valid payment method.");
    }

    // Example : Validate if the item, customer, quantity and payment method is possible.
    if(validatePaymentMethodForItem(item, paymentMethod).size() > 0) {
        problems.put("paymentMethodId", "You cannot buy this item with the specified payment method.");
    }
    
    return problems;
} 
  

和业务服务
public Buy buyItem(BuyItemRequest bIRequest) {
    // Get Client from session info.
    Customer currentCustomer = sessionInfo.getCurrentCustomer();
    if(currentCustomer == null) {
        throw new SecurityException("Client not logged In.");
    }

    // Check the same validations.
    Map<String, String> problems = buyItemValidator.validateBuyItem(bIRequest);
    if(!problems.isEmpty()) {
        throw new BuyException("Your buy cannot be performed.");
    }
    
    // Build the Buy and store it.
    Item item = itemRepository.get(bIRequest.getItemId());
    PaymentMethod paymentMethod = paymentMethodRepository.get(bIRequest.getPaymentMethodId());
    
    BuyBuilder buyBuilder = new BuyBuilder();
    buyBuilder.setCustomer(currentCustomer);
    buyBuilder.setItem(item);
    buyBuilder.setPaymentMetod(paymentMethod);
    buyBuilder.setQuantity(bIRequest.getItemQuantity());
    
    return buyRepository.add(buyBuilder.build());
}


在这个例子中,我将数据类型验证拆分到一个新方法中,还定义了一个业务方法,该方法接收与最终业务方法相同的对象,因此任何验证都可以在此验证器中完成,最重要的是,我们解决了模型层中的验证流程。

这个新类 BuyItemValidator 将验证命令 buyItem,接收与 buyItem 在相同上下文中将接收的相同参数。此处理器集中了验证流程,并支持在不同上下文中实现不同的业务规则,满足不同的客户需求。

BuyItem 方法是乐观的,在处理器上调用验证,它允许干净地专注于一个目标:保存购买。

我们还能要求更多吗?是的,当然... 

事务问题

使用这种方法,我们的企业应用程序将感觉强大,但在并发访问时可能会出现意外结果,验证可能通过初始验证,但在调用业务方法时失败。考虑商品可用性,当您调用验证器时,供应商可能拥有足够的库存来执行销售,但当事件被调用时它失败了,有人在验证和方法调用之间提交了购买。因此,如果验证在 buyItem 方法内部运行会更好,那么我们可以在同一个事务中锁定库存,同时可以返回错误或成功。

此外,通过这种验证过程还解决了另一个问题,验证代码只运行一次,更好地利用了服务器资源;在之前的实现中,一些验证运行了两次。

处理这种情况的一种方法是将响应封装到一个 Response 对象中,该对象可以包含响应本身和错误列表,如下所示:

模型

public BuyResponse buyItem(BuyItemCommand biCommand) throws BuyException {
    BuyResponse response = new BuyResponse();

    // Get Client from session info.
    Customer currentCustomer = sessionInfo.getCurrentCustomer();
    if(currentCustomer == null) {
        throw new SecurityException("Client not logged In.");
    }
    
    // Check the same validations.
    Map<String, String> problems = buyItemValidator.validateBuyItem(biCommand);
    if(!problems.isEmpty()) {
        response.setValidationProblems(problems);
        return response;
    }
    
    // Build the Buy and store it.
    Item item = itemRepository.get(biCommand.getItemId());
    PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
    
    BuyBuilder buyBuilder = new BuyBuilder();
    buyBuilder.setCustomer(currentCustomer);
    buyBuilder.setItem(item);
    buyBuilder.setPaymentMetod(paymentMethod);
    buyBuilder.setQuantity(biCommand.getItemQuantity());
    
    response.setBuy(buyRepository.add(buyBuilder.build()));
    return response;
}  

如果一切正常,业务方法应该能够响应对象,以及验证错误列表。这就是 BuyResponse 的功能,封装成功调用的对象或错误列表(如果出现问题)。控制器现在在 BuyResponse 中获取错误,或在成功时获取正确的 Buy。

还有一种替代方案,有时异常的强大被低估了,异常机制是一个强大的工具,运用得当可以创造奇迹。假设我们有一个特殊的 ParameterValidationException ,它包含有关验证错误的所有详细信息,让我们这样实现模型


模型

public Buy buyItem(BuyItemCommand biCommand) {
	BuyResponse response = new BuyResponse();

	// Get Client from session info.
	Customer currentCustomer = sessionInfo.getCurrentCustomer();
	if(currentCustomer == null) {
		throw new SecurityException("Client not logged In.");
	}
	
	// Check the same validations.
	Map<String, String> problems = buyItemValidator.validateBuyItem(biCommand);
	if(!problems.isEmpty()) {
		throw new ParameterValidationException(problems);
	}
	
	// Build the Buy and store it.
	Item item = itemRepository.get(biCommand.getItemId());
	PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
	
	BuyBuilder buyBuilder = new BuyBuilder();
	buyBuilder.setCustomer(currentCustomer);
	buyBuilder.setItem(item);
	buyBuilder.setPaymentMetod(paymentMethod);
	buyBuilder.setQuantity(biCommand.getItemQuantity());
	
    return buyRepository.add(buyBuilder.build());
}
  


控制器

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // TODO: Perform security/access validations

    // If some basic data type is not filled, will return to the same view, with the errors
    Map<String, String> basicValidationErr = validateInputDataTypes(request);
    if(basicValidationErr.size() > 0) {
        for(String key: basicValidationErr.keySet()) {
            request.setAttribute(key, basicValidationErr.get(key));
        }
        // TODO: return to view with error messages.
    }

    // Build the Buy Request.
    BuyItemCommand biCommand = new BuyItemCommand();
    biCommand.setItemId(NumberUtils.toInt(request.getParameter("itemCode")));
    biCommand.setItemQuantity(NumberUtils.toInt(request.getParameter("itemQuantity")));
    biCommand.setPaymentMethodId(NumberUtils.toInt(request.getParameter("paymentMethodCode")));
    
    // Call the business proceess
    try {
        Buy currentBuy = buyModel.buyItem(biCommand);
        request.setAttribute("buy", currentBuy);
        
        //TODO: forward to the correct view informing that the buy was done correctly.

    } catch (ParameterValidationException pe) {            
        // If some error parameter validation error happends, will receive this special Exception.
        for(String key: pe.getValidationErrors().keySet()) {
            if(key.equals("itemCode")) {
                request.setAttribute("itemCodeError", pe.getValidationErrors().get(key));
            } else if (key.equals("itemQuantity")) {
                request.setAttribute("itemQuantityError", pe.getValidationErrors().get(key));
            } else if (key.equals("paymentMethodCode")) {
                request.setAttribute("paymentMethodCodeError", pe.getValidationErrors().get(key));
            } 
        }

        //TODO: forward to the correct view informing the validation errors.
        
    } catch (BuyException ex) {
        request.setAttribute("error", ex.getMessage());
        //TODO: forward to the correct view informing the validation errors. 
       
    } catch (Exception ex) {
        // TODO: log the error and redirect to general failure view
    }
} 

这些实现“打破”了一些异常规则,为参数验证错误定义了一个特殊异常,该异常将在验证问题时抛出,并且内部包含有关问题的完整信息,例如每个请求对象属性和错误消息的映射。 

最后,基于异常的解决方案允许轻松地通过 AOP 实现业务验证,在方法执行之前运行业务验证。

享受验证。  
© . All rights reserved.