工作生活

一次DDD重构实践

2019-07-01  本文已影响0人  shawnliang

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

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

  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

上一篇下一篇

猜你喜欢

热点阅读