安卓清理垃圾清理代码
从战中清除代码–验证
让我们直接从一个例子开始。 考虑一个简单的Web服务,该服务允许客户向商店下订单。 订单控制器的非常简化的版本可能如下所示–
@RestController
@RequestMapping(value = "/",consumes = MediaType.APPLICATION_JSON_VALUE,produces = MediaType.APPLICATION_JSON_VALUE)
public class OrderController {private final OrderService orderService;public OrderController(OrderService orderService) {this.orderService = orderService;}@PostMappingpublic void doSomething(@Valid @RequestBody OrderDTO order) {orderService.createOrder(order);}
}
和相应的DTO类
@Getter
@Setter
@ToString
public class OrderDTO {@NotNullprivate String customerId;@NotNull@Size(min = 1)private List<OrderItem> orderItems;@Getter@Setter@ToStringpublic static class OrderItem {private String menuId;private String description;private String price;private Integer quantity;}
}
从此DTO创建订单的最常见方法是将其传递给服务,根据需要对其进行验证,然后将其保存在数据库中
@Service
@Slf4j
class OrderService {private final MenuRepository menuRepository;OrderService(MenuRepository menuRepository) {this.menuRepository = menuRepository;}void createOrder(OrderDTO orderDTO) {orderDTO.getOrderItems().forEach(this::validate);log.info("Order {} saved", orderDTO);}private void validate(OrderItem orderItem) {String menuId = orderItem.getMenuId();if (menuId == null || menuId.trim().isEmpty()) {throw new IllegalArgumentException("A menu item must be specified.");}if (!menuRepository.menuExists(menuId.trim())) {throw new IllegalArgumentException("Given menu " + menuId + " does not exist.");}String description = orderItem.getDescription();if (description == null || description.trim().isEmpty()) {throw new IllegalArgumentException("Item description should be provided");}String price = orderItem.getPrice();if (price == null || price.trim().isEmpty()) {throw new IllegalArgumentException("Price cannot be empty.");}try {new BigDecimal(price);} catch (NumberFormatException ex) {throw new IllegalArgumentException("Given price is not in valid format", ex);}if (orderItem.getQuantity() == null) {throw new IllegalArgumentException("Quantity must be given");}if (orderItem.getQuantity() <= 0) {throw new IllegalArgumentException("Given quantity "+ orderItem.getQuantity()+ " is not valid.");}}
}
validate方法写得不好。 很难测试。 将来引入新的验证规则也很困难,因此删除/修改任何现有的验证规则也很困难。 从我的经验中,我看到大多数人通常在集成测试类中针对这种类型的验证检查编写一些通用断言,仅涉及一个或两个(或更多,但不是全部)验证规则。 因此,将来只能在“ 编辑”和“祈祷”模式下进行重构。
如果使用多态来替换这些条件,则可以改善代码结构。 我们创建一个通用的超级类型来表示一个验证规则
public interface OrderItemValidator {void validate(OrderItem orderItem);
}
下一步是创建验证规则实现,该实现将集中于DTO的单独验证区域。 让我们从菜单验证器开始
public class MenuValidator implements OrderItemValidator {private final MenuRepository menuRepository;public MenuValidator(MenuRepository menuRepository) {this.menuRepository = menuRepository;}@Overridepublic void validate(OrderItem orderItem) {String menuId = Optional.ofNullable(orderItem.getMenuId()).map(String::trim).filter(id -> !id.isEmpty()).orElseThrow(() -> new IllegalArgumentException("A menu item must be specified."));if (!menuRepository.menuExists(menuId)) {throw new IllegalArgumentException("Given menu [" + menuId + "] does not exist.");}}
}
然后商品说明验证器
public class ItemDescriptionValidator implements OrderItemValidator {@Overridepublic void validate(OrderItem orderItem) {Optional.ofNullable(orderItem).map(OrderItem::getDescription).map(String::trim).filter(description -> !description.isEmpty()).orElseThrow(() -> new IllegalArgumentException("Item description should be provided"));}
}
价格验证器
public class PriceValidator implements OrderItemValidator {@Overridepublic void validate(OrderItem orderItem) {String price = Optional.ofNullable(orderItem).map(OrderItem::getPrice).map(String::trim).filter(itemPrice -> !itemPrice.isEmpty()).orElseThrow(() -> new IllegalArgumentException("Price cannot be empty."));try {new BigDecimal(price);} catch (NumberFormatException ex) {throw new IllegalArgumentException("Given price [" + price + "] is not in valid format", ex);}}
}
最后,数量验证器
public class QuantityValidator implements OrderItemValidator {@Overridepublic void validate(OrderItem orderItem) {Integer quantity = Optional.ofNullable(orderItem).map(OrderItem::getQuantity).orElseThrow(() -> new IllegalArgumentException("Quantity must be given"));if (quantity <= 0) {throw new IllegalArgumentException("Given quantity " + quantity + " is not valid.");}}
}
这些验证器实现中的每一个现在都可以轻松地,彼此独立地进行测试。 关于它们每个的推理也变得更加容易。 将来的添加/修改/删除也是如此。
现在是接线部分。 我们如何将这些验证器与订单服务集成在一起?
一种方法是直接在OrderService构造函数中创建一个列表,并使用验证程序填充它。 或者我们可以使用Spring将List注入到OrderService中
@Service
@Slf4j
class OrderService {private final List<OrderItemValidator> validators;OrderService(List<OrderItemValidator> validators) {this.validators = validators;}void createOrder(OrderDTO orderDTO) {orderDTO.getOrderItems().forEach(this::validate);log.info("Order {} saved", orderDTO);}private void validate(OrderItem orderItem) {validators.forEach(validator -> validator.validate(orderItem));}
}
为了使它起作用,我们将必须将每个验证器实现声明为Spring Bean。
我们可以进一步改进抽象。 OrderService现在正在接受验证者列表。 但是,我们可以将其更改为仅了解OrderItemValidator类型,而无需其他任何操作。 这使我们可以灵活地将来注入单个验证器或任何组成的验证器。
因此,现在我们的目标是更改订单服务,以与单个验证器相同的方式来处理订单项验证器的组成。 有一个著名的设计模式称为
Composite让我们可以做到这一点。
让我们创建验证器接口的新实现,该接口将为复合
class OrderItemValidatorComposite implements OrderItemValidator {private final List<OrderItemValidator> validators;OrderItemValidatorComposite(List<OrderItemValidator> validators) {this.validators = validators;}@Overridepublic void validate(OrderItem orderItem) {validators.forEach(validators -> validators.validate(orderItem));}
}
然后,我们创建一个新的Spring配置类,该类将实例化并初始化此组合,然后将其公开为bean
@Configuration
class ValidatorConfiguration {@BeanOrderItemValidator orderItemValidator(MenuRepository menuRepository) {return new OrderItemValidatorComposite(Arrays.asList(new MenuValidator(menuRepository),new ItemDescriptionValidator(),new PriceValidator(),new QuantityValidator()));}
}
然后,我们通过以下方式更改OrderService类
@Service
@Slf4j
class OrderService {private final OrderItemValidator validator;OrderService(OrderItemValidator orderItemValidator) {this.validator = orderItemValidator;}void createOrder(OrderDTO orderDTO) {orderDTO.getOrderItems().forEach(validator::validate);log.info("Order {} saved", orderDTO);}
}
我们完成了!
这种方法的好处很多。 整个验证逻辑已完全从订购服务中抽象出来。 测试更容易。 将来的维护更加容易。 客户只知道一种验证器类型,而没有其他信息。
但是,上述所有方法也都存在一些问题。 有时人们对此设计不满意。 他们可能觉得这太抽象了,或者对于将来的维护他们将不需要太多的灵活性或可测试性。 我建议根据团队文化采用这种方法。 毕竟,在软件开发中没有唯一正确的方法。
请注意,为了本文方便,我在这里也做了一些简化。 其中包括在验证失败时引发通用IllegalArgumentException。 您可能希望生产级应用程序中有一个更特定/自定义的异常,以在不同情况之间进行标识。 十进制分析也很天真地完成,您可能要修复特定的格式,然后使用DecimalFormat对其进行解析。
完整的代码已上传到Github 。
翻译自: https://www.javacodegeeks.com/2017/05/clean-code-trenches.html
安卓清理垃圾清理代码