从战中清理代码
從戰(zhàn)中清除代碼–驗(yàn)證
讓我們直接從一個(gè)例子開始。 考慮一個(gè)簡(jiǎn)單的Web服務(wù),該服務(wù)允許客戶向商店下訂單。 訂單控制器的非常簡(jiǎn)化的版本可能如下所示–
@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);} }和相應(yīng)的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創(chuàng)建訂單的最常見方法是將其傳遞給服務(wù),根據(jù)需要對(duì)其進(jìn)行驗(yàn)證,然后將其保存在數(shù)據(jù)庫(kù)中
@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方法寫得不好。 很難測(cè)試。 將來引入新的驗(yàn)證規(guī)則也很困難,因此刪除/修改任何現(xiàn)有的驗(yàn)證規(guī)則也很困難。 從我的經(jīng)驗(yàn)中,我看到大多數(shù)人通常在集成測(cè)試類中針對(duì)這種類型的驗(yàn)證檢查編寫一些通用斷言,僅涉及一個(gè)或兩個(gè)(或更多,但不是全部)驗(yàn)證規(guī)則。 因此,將來只能在“ 編輯”和“祈禱”模式下進(jìn)行重構(gòu)。
如果使用多態(tài)來替換這些條件,我們可以改善代碼結(jié)構(gòu)。 我們創(chuàng)建一個(gè)通用的超級(jí)類型來表示一個(gè)驗(yàn)證規(guī)則
public interface OrderItemValidator {void validate(OrderItem orderItem); }下一步是創(chuàng)建驗(yàn)證規(guī)則實(shí)現(xiàn),該實(shí)現(xiàn)將集中于DTO的單獨(dú)驗(yàn)證區(qū)域。 讓我們從菜單驗(yàn)證器開始
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.");}} }然后商品說明驗(yàn)證器
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"));} }價(jià)格驗(yàn)證器
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);}} }最后,數(shù)量驗(yàn)證器
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.");}} }現(xiàn)在,可以彼此獨(dú)立地輕松地測(cè)試每個(gè)驗(yàn)證器實(shí)現(xiàn)。 關(guān)于它們每個(gè)的推理也變得更加容易。 將來的添加/修改/刪除也是如此。
現(xiàn)在是接線部分。 我們?nèi)绾螌⑦@些驗(yàn)證器與訂單服務(wù)集成在一起?
一種方法是直接在OrderService構(gòu)造函數(shù)中創(chuàng)建一個(gè)列表,并使用驗(yàn)證程序填充它。 或者我們可以使用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));} }為了使它起作用,我們將必須將每個(gè)驗(yàn)證器實(shí)現(xiàn)聲明為Spring Bean。
我們可以進(jìn)一步改進(jìn)抽象。 OrderService現(xiàn)在正在接受驗(yàn)證者列表。 但是,我們可以將其更改為僅了解OrderItemValidator類型,而無需其他任何更改。 這為我們提供了將來注入單個(gè)驗(yàn)證器或驗(yàn)證器的任何組合的靈活性。
因此,現(xiàn)在我們的目標(biāo)是更改訂單服務(wù),以與單個(gè)驗(yàn)證器相同的方式來處理訂單項(xiàng)驗(yàn)證器的組成。 有一個(gè)著名的設(shè)計(jì)模式稱為
Composite讓我們可以做到這一點(diǎn)。
讓我們?yōu)轵?yàn)證器接口創(chuàng)建一個(gè)新的實(shí)現(xiàn),它將是復(fù)合的
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));} }然后,我們創(chuàng)建一個(gè)新的Spring配置類,該類將實(shí)例化并初始化此組合,然后將其公開為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);} }我們完成了!
這種方法的好處很多。 整個(gè)驗(yàn)證邏輯已完全從訂購(gòu)服務(wù)中抽象出來。 測(cè)試更容易。 將來的維護(hù)更加容易。 客戶只知道一種驗(yàn)證器類型,而沒有其他信息。
但是,上述所有方法也都存在一些問題。 有時(shí)人們對(duì)此設(shè)計(jì)不滿意。 他們可能覺得這太抽象了,或者對(duì)于將來的維護(hù)他們將不需要太多的靈活性或可測(cè)試性。 我建議根據(jù)團(tuán)隊(duì)文化采用這種方法。 畢竟,在軟件開發(fā)中沒有唯一正確的方法。
請(qǐng)注意,為了本文的方便,我在這里也做了一些簡(jiǎn)化。 其中包括驗(yàn)證失敗時(shí)引發(fā)通用IllegalArgumentException。 您可能希望生產(chǎn)級(jí)應(yīng)用程序中有一個(gè)更特定/自定義的異常,以在不同情況之間進(jìn)行標(biāo)識(shí)。 十進(jìn)制分析也很幼稚地完成,您可能要修復(fù)特定的格式,然后使用DecimalFormat對(duì)其進(jìn)行解析。
完整的代碼已上傳到Github 。
翻譯自: https://www.javacodegeeks.com/2017/05/clean-code-trenches.html
總結(jié)
- 上一篇: java 和javafx_JavaFX
- 下一篇: mapreduce排序算法_MapRed