美文网首页工作生活
一次DDD重构实践

一次DDD重构实践

作者: shawnliang | 来源:发表于2019-07-01 00:42 被阅读0次

    我来问道无馀说,云在青天水在瓶。
    --- 唐.李翱《赠药山高僧惟俨二首⑴ 》

    最近在开发一个配额创建和修改的功能,需求很简单:

    1. 配额能够正确创建,比如:2019-05-25, 用户lxy创建了一个配额,包含10件某商品。订单的状态包括:新建、提交等等。

    2. 每个配额单能够分配到具体的商店。
      比如第一步创建的订单,能够分配到两个商店,A商店4件,B商店6件。

    3. 配额单创建后能够修改。
      3-1 配额商品数量能够修改,比如由10件变成15件。
      3-2 配额商品分配到商店的信息能够添加或修改,比如分配到A商店的4件商品变成6件,增加一个条目,分配3件商品到C商店。

    数据库表是这样设计的(省去审计字段),

    quota表:

    id product_id quntity status
    quotaid productid 100 0

    quota_item表:

    id quota_id shop_id quntity
    item_id quotaid shopid 20
    item_id2 quotaid shopid2 80

    需求比较清晰,和BA、QA沟通理解一致后,开始开发。

    项目用springboot框架开发,使用洋葱架构分层,核心代码如下:

    Controller:
    @RestController
    public class QuotaController implements QuotaApi {
        private final QuotaApplicationService applicationService;
    
        public QuotaController(QuotaApplicationService applicationService) {
            this.applicationService = applicationService;
        }
    
        @Override
        public QuotaCreateResponse createQuota(@RequestBody QuotaCreateRequest request) {
            return applicationService.createQuota(
                    Quota.builder()
                            .productId(request.getProductId())
                            .quantity(request.getQuantity()).status("create").build());
        }
    
        @Override
        public QuotaItemCreateResponse createQuotaItem(@RequestBody QuotaItemCreateRequest request) {
            QuotaItemCreateCommand command = QuotaItemMapper.MAPPER.toCommand(request);
            return applicationService.createQuotaItem(command);
        }
    }
    
    ApplicationService:
    @Service
    public class QuotaApplicationService {
        private final QuotaRepository repository;
        private final QuotaItemRepository itemRepository;
    
        public QuotaApplicationService(QuotaRepository repository, QuotaItemRepository itemRepository) {
            this.repository = repository;
            this.itemRepository = itemRepository;
        }
    
        public QuotaCreateResponse createQuota(Quota quota) {
            Quota result = repository.createQuota(quota);
    
            return QuotaMapper.MAPPER.toResponse(result);
        }
    
        public QuotaItemCreateResponse createQuotaItem(QuotaItemCreateCommand command) {
            List<String> shopIds = command.getQuotaItemDtoList().stream()
                    .map(QuotaItemCreateCommand.QuotaItemDto::getShopId)
                    .collect(Collectors.toList());
    
            List<QuotaItem> existQuotaItems = searchQuotaItems(command.getQuotaId(), shopIds);
            QuotaHelper helper = new QuotaHelper(command, existQuotaItems);
    
            Quota quota = repository.updateQuota(helper.getUpdateQuota());
            List<QuotaItem> insertResult = itemRepository.saveAll(helper.getInsertQuotaItems());
            List<QuotaItem> updateResult = itemRepository.updateAll(helper.getUpdateQuotaItems());
    
            return buildResponse(quota, insertResult, updateResult);
        }
    
        private List<QuotaItem> searchQuotaItems(String quotaId, List<String> shopIds) {
            return repository.queryQuotaItems(quotaId, shopIds);
        }
    
        private static class QuotaHelper {
            private final QuotaItemCreateCommand command;
            private final List<QuotaItem> existQuotaItems;
    
            QuotaHelper(QuotaItemCreateCommand command, List<QuotaItem> existQuotaItems) {
                this.command = command;
                this.existQuotaItems = existQuotaItems;
            }
    
            Quota getUpdateQuota() {
                return Quota.builder().id(command.getQuotaId()).quantity(command.getQuantity()).status("update").build();
            }
    
            List<QuotaItem> getInsertQuotaItems() {
                List<QuotaItemCreateCommand.QuotaItemDto> createDtos = command.getQuotaItemDtoList().stream()
                        .filter(quotaItemDto -> existQuotaItems.stream()
                                .noneMatch(quotaItem -> quotaItemDto.getShopId().equals(quotaItem.getShopId())))
                        .collect(Collectors.toList());
    
                return QuotaItemMapper.MAPPER.toDomain(createDtos).stream().peek(quotaItem -> {
                    quotaItem.setId(UUID.randomUUID().toString());
                    quotaItem.setQuotaId(command.getQuotaId());
                }).collect(Collectors.toList());
            }
    
            List<QuotaItem> getUpdateQuotaItems() {
                existQuotaItems.forEach(quotaItem -> {
                    Optional<QuotaItemCreateCommand.QuotaItemDto> updateItem =
                            command.getQuotaItemDtoList().stream()
                                    .filter(quotaItemDto -> quotaItemDto.getShopId().equals(quotaItem.getShopId()))
                                    .findFirst();
    
                    updateItem.ifPresent(quotaItemDto -> quotaItem.setQuantity(quotaItemDto.getQuantity()));
                });
    
                return existQuotaItems;
            }
        }
    
        private QuotaItemCreateResponse buildResponse(Quota quota, List<QuotaItem> insertResult, List<QuotaItem> updateResult) {
            updateResult.addAll(insertResult);
    
            return QuotaItemCreateResponse.builder()
                    .quotaId(quota.getId())
                    .quantity(quota.getQuantity())
                    .quotaItemDtoList(QuotaItemMapper.MAPPER.toResponse(updateResult))
                    .build();
        }
    }
    
    Domain:
    public class Quota {
        private String id;
        private String productId;
        private Long quantity;
        private String status;
    }
    
    public class QuotaItem {
        private String id;
        private String quotaId;
        private String shopId;
        private Long quantity;
    }
    

    代码实现很简单,主要逻辑在QuotaApplicationService里,在创建QuotaItem时引入了一个辅助类QuotaHelper,判断哪些Item是需要更新的,哪些是新创建的,然后分别做更新和创建。至于domain,完全是贫血模型,没有承担任何业务逻辑,类型于传统的JavaBean

    团队code diff后,小伙伴们指出了代码中存在的明显缺陷。提炼如下:

    在数据库层面,quota和quotaItem是两张表,但是在领域模型层面。quotaItem是从属于quota的,quota没有quotaItem是可以独立存在的。但是,quotaItem必须属于一个quota。

    按照这个思路,对代码进行重构,主要是对领域模型的重构以及因此带来的变更

    domain:
    public class Quota {
        private String id;
        private String productId;
        private Long quantity;
        private String status;
    
        private List<QuotaItem> items;
    
        public void update(Long quantity, List<QuotaItemCreateCommand.QuotaItemDto> quotaItemDtoList) {
            this.quantity = quantity;
            Map<String, QuotaItem> itemDtoMap =
                    items.stream().collect(Collectors.toMap(QuotaItem::getShopId, Function.identity()));
    
            this.items = quotaItemDtoList.stream().map(quotaItemDto -> {
                if (itemDtoMap.containsKey(quotaItemDto.getShopId())) {
                    QuotaItem quotaItem = itemDtoMap.get(quotaItemDto.getShopId());
                    quotaItem.setQuantity(quotaItemDto.getQuantity());
    
                    return quotaItem;
                }
    
                return QuotaItem.builder()
                        .id(UUID.randomUUID().toString())
                        .quotaId(this.getId())
                        .shopId(quotaItemDto.getShopId())
                        .quantity(quotaItemDto.getQuantity())
                        .build();
            }).collect(Collectors.toList());
        }
    }
    
    controller:
    @RestController
    public class QuotaController implements QuotaApi {
        private final QuotaApplicationService applicationService;
    
        public QuotaController(QuotaApplicationService applicationService) {
            this.applicationService = applicationService;
        }
    
        @Override
        public QuotaCreateResponse createQuota(@RequestBody QuotaCreateRequest request) {
            return applicationService.createQuota(
                    Quota.builder()
                            .productId(request.getProductId())
                            .quantity(request.getQuantity()).status("create").build());
        }
    
        @Override
        public QuotaItemCreateResponse createQuotaItem(@RequestBody QuotaItemCreateRequest request) {
            QuotaItemCreateCommand command = QuotaItemMapper.MAPPER.toCommand(request);
            return buildResponse(applicationService.createQuotaItem(command));
        }
    
        private QuotaItemCreateResponse buildResponse(Quota quota) {
            return QuotaItemCreateResponse.builder()
                    .quotaId(quota.getId())
                    .quantity(quota.getQuantity())
                    .quotaItemDtoList(QuotaItemMapper.MAPPER.toResponse(quota.getItems()))
                    .build();
        }
    }
    
    ApplicationService:
    @Service
    public class QuotaApplicationService {
        private final QuotaRepository repository;
    
        public QuotaApplicationService(QuotaRepository repository) {
            this.repository = repository;
        }
    
        public QuotaCreateResponse createQuota(Quota quota) {
            Quota result = repository.createQuota(quota);
    
            return QuotaMapper.MAPPER.toResponse(result);
        }
    
        public Quota createQuotaItem(QuotaItemCreateCommand command) {
            Optional<Quota> optionalQuota = repository.searchQuota(command.getQuotaId());
            if (!optionalQuota.isPresent()) {
                throw new RuntimeException("quota does not exist, id is: "+ command.getQuotaId());
            }
    
            Quota quota = optionalQuota.get();
            quota.update(command.getQuantity(), command.getQuotaItemDtoList());
    
            repository.updateQuota(quota);
    
            return quota;
        }
    }
    
    

    重构之后,ApplicationService中createQuotaItem的实现清晰地分为三部分:
    1.查询存在的quota
    2.领域模型quota自己完成update操作
    3.持久化

    领域模型不再是贫血模型,完成它本该完成的更新模型数据的职能。

    很多开发人员认为DDD很难,编码时候更是无从下手。思路是DDD的思路,一上手还是三层架构风格的代码。其实很多情况下是领域模型的抽象不够准确,领域模型只是对数据库表结构的翻译。再加上代码职责不够单一,controller和application service的职责混乱,这样的代码自然很难说是DDD的。

    其实,DDD很简单,首先定义体现业务本来面貌的领域模型,然后domain, controller,application service, domain service以及repository,每个组件完成自己该干的事情。

    云在青天水在瓶,该怎么样就是怎么样。

    其实这么说也不太负责,因为抽象一个"体现业务本来面貌的领域模型"本来就是DDD中最为核心且最有难度的事情。

    经常思考互联网发展到现在出现的一些社交产品对社交关系的建模。

    最早出现的同学录,大家要交流只能留言。沟通实时性太差。社交关系显然不是这样的。
    QQ群出现后,同学录就逐渐消亡了。在QQ群里,每个人说的话别人都能第一时间看到。
    但是,QQ群无法充分体现个体的差异。
    所以,后来出现了QQ空间,微博。个体的表达需求满足了,但是沟通的实时性还是差了一些。
    再后来,大家都知道,出现了微信。每个人都能在自己的一亩三分地朋友圈展示自己,好友可以第一时间点赞和评论。如果有群体沟通的需求可以建立各种各样的群。看谁不爽还可以屏蔽,拉黑。现实社会本不就是这样的么?
    到目前为止,微信是对社交关系建模最为准确的产品。

    本文代码可以从这里获取:
    https://github.com/worldlxy/refactor-to-ddd

    相关文章

      网友评论

        本文标题:一次DDD重构实践

        本文链接:https://www.haomeiwen.com/subject/xczicctx.html