我来问道无馀说,云在青天水在瓶。
--- 唐.李翱《赠药山高僧惟俨二首⑴ 》
最近在开发一个配额创建和修改的功能,需求很简单:
-
配额能够正确创建,比如:2019-05-25, 用户lxy创建了一个配额,包含10件某商品。订单的状态包括:新建、提交等等。
-
每个配额单能够分配到具体的商店。
比如第一步创建的订单,能够分配到两个商店,A商店4件,B商店6件。 -
配额单创建后能够修改。
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
网友评论