在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
谈谈你的DDD落地经验?
谈谈你对DDD的理解?
如何保证RPC代码不会腐烂,升级能力强?
最近有小伙伴在字节,又遇到了相关的面试题。小伙伴懵了, 他从来没有用过DDD,面挂了。关于DDD,尼恩之前给大家梳理过一篇很全的文章: 阿里一面:谈一下你对DDD的理解?2W字,帮你实现DDD自由
但是尼恩的文章, 太过理论化,不适合刚入门的人员。所以,尼恩也在不断的为大家找更好的学习资料。
前段时间,尼恩在阿里的技术公众号上看到了一篇文章《殷浩详解DDD:领域层设计规范》 作者是阿里 技术大佬殷浩,非常适合于初学者入门,同时也足够的有深度。
美中不足的是, 殷浩那篇文章的行文风格,对初学者不太友好, 尼恩刚开始看的时候,也比较晦涩。
于是,尼恩在读的过程中,把那些晦涩的内容,给大家用尼恩的语言, 浅化了一下, 这样大家更容易懂。
本着技术学习、技术交流的目的,这里,把尼恩修改过的 《殷浩详解DDD:领域层设计规范》,通过尼恩的公众号《技术自由圈》发布出来。
特别声明,由于没有殷浩同学的联系方式,这里没有找殷浩的授权,
如果殷浩同学或者阿里技术公众号不同意我的修改,不同意我的发布, 我即刻从《技术自由圈》公众号扯下来。
另外, 文章也特别长, 我也特别准备了PDF版本。如果需要尼恩修改过的PDF版本,也可以通过《技术自由圈》公众号找到尼恩来获取。
在正式开始第6篇之前,尼恩说一下自己对DDD的 亲身体验、和深入思考。
DDD的本质:
大大提升 核心代码 业务纯度
老的mvc架构,代码中紧紧的耦合着特定ORM框架、特定DB存储、特定的缓存、特定的事务框架、特定中间件,特定对外依赖解耦, 很多很多。
总之就是 业务和技术紧密耦合,代码的 业务纯度低, 导致软件“固化”, 没法做快速扩展和升级。
大大提升 代码工程 测维扩 能力
DDD进行了多个层次的解耦,包括 持久层的DB解耦,第三方依赖的隔离解耦,大大提升了 可测试度、可维护度、可扩展度
更大限度 积累 业务领域模型 资产
由于spring mvc 模式下, 代码的业务纯度不高, 导致尼恩的曾经一个项目,10年多时间, 衍生出 50多个不同的版本,推导重来5次,付出巨大的 时间成本、经济成本
DDD的收益:
不用DDD的反面案例,尼恩曾经见过一个项目:
本文是 《阿里DDD大佬:从0到1,带大家精通DDD》系列的第6篇
本文是 《从0到1,带大家精通DDD》系列的的链接地址是:
《阿里DDD大佬:从0到1,带大家精通DDD》
《阿里大佬:DDD 落地两大步骤,以及Repository核心模式》
《阿里面试:让代码不腐烂,DDD是怎么做的?》
《阿里大佬:DDD 领域层,该如何设计?》
《极兔面试:微服务爆炸,如何解决?Uber 是怎么解决2200个微服务爆炸的?》
大家可以先看前面的文章,再来看本篇,效果更佳。
另外,尼恩会结合一个工业级的DDD实操项目,在第34章视频《DDD的顶奢面经》中,给大家彻底介绍一下DDD的实操、COLA 框架、DDD的面试题。
在日常工作中我观察到,面对老系统重构和迁移场景,有大量代码属于流水账代码,
通常能看到开发在对外的API接口里直接写业务逻辑代码,或者在一个服务里大量的堆接口,导致业务逻辑实际无法收敛,接口复用性比较差。
本文主要想系统性的解释一下如何通过DDD的重构,将原有的流水账代码,改造为逻辑清晰、职责分明的模块。
领域驱动设计没有特定的架构风格,它的核心是域模型驱动业务的思想,常见的领域驱动设计架构有传统的四层架构模式、事件驱动架构、CQRS架构、六边形架构等。
这里举一个简单的常见案例:下单链路。
假设我们在做一个checkout接口,需要做各种校验、查询商品信息、调用库存服务扣库存、然后生成订单:
一个比较典型的代码如下:
@RestController
@RequestMapping("/")
public class CheckoutController {
@Resource
private ItemService itemService;
@Resource
private InventoryService inventoryService;
@Resource
private OrderRepository orderRepository;
@PostMapping("checkout")
public Result<OrderDO> checkout(Long itemId, Integer quantity) {
// 1) Session管理
Long userId = SessionUtils.getLoggedInUserId();
if (userId <= 0) {
return Result.fail("Not Logged In");
}
// 2)参数校验
if (itemId <= 0 || quantity <= 0 || quantity >= 1000) {
return Result.fail("Invalid Args");
}
// 3)外部数据补全
ItemDO item = itemService.getItem(itemId);
if (item == null) {
return Result.fail("Item Not Found");
}
// 4)调用外部服务
boolean withholdSuccess = inventoryService.withhold(itemId, quantity);
if (!withholdSuccess) {
return Result.fail("Inventory not enough");
}
// 5)领域计算
Long cost = item.getPriceInCents() * quantity;
// 6)领域对象操作
OrderDO order = new OrderDO();
order.setItemId(itemId);
order.setBuyerId(userId);
order.setSellerId(item.getSellerId());
order.setCount(quantity);
order.setTotalCost(cost);
// 7)数据持久化
orderRepository.createOrder(order);
// 8)返回
return Result.success(order);
}
}
为什么这种典型的流水账代码在实际应用中会有问题呢?
其本质问题是违背了SRP(Single Responsbility Principle)单一职责原则。
这段代码里混杂了业务计算、校验逻辑、基础设施、和通信协议等,在未来无论哪一部分的逻辑变更都会直接影响到这段代码,当后人不断地在上面叠加新的逻辑时,会使代码复杂度增加、逻辑分支越来越多,最终造成bug或者没人敢重构的历史包袱。
所以我们才需要用DDD的分层思想去重构一下以上的代码,通过不同的代码分层和规范,拆分出逻辑清晰,职责明确的分层和模块,也便于一些通用能力的沉淀。
主要的几个步骤分为:
下面会针对每个点做详细的解释。
随着REST和MVC架构的普及,经常能看到开发同学直接在Controller中写业务逻辑,如上面的典型案例,但实际上MVC Controller不是唯一的重灾区。
以下的几种常见的代码写法通常都可能包含了同样的问题:
这些的方法都有一个共同的点就是都有自己的网络协议,而如果我们的业务代码和网络协议混杂在一起,则会直接导致代码跟网络协议绑定,无法被复用。
所以,在DDD的分层架构中,我们单独会抽取出来Interface接口层,作为所有对外的门户,将网络协议和业务逻辑解耦。
接口层主要由以下几个功能组成:
当然,如果有一个独立的网关设施/应用,则可以抽离出鉴权、Session、限流、日志等逻辑,但是目前来看API网关也只能解决一部分的功能,即使在有API网关的场景下,应用里独立的接口层还是有必要的。
在Interface层,鉴权、Session、限流、缓存、日志等都比较直接,只有一个异常处理的点需要重点说下。
注:这部分主要还是面向REST和RPC接口,其他的协议需要根据协议的规范产生返回值。
在我见过的一些代码里,接口的返回值比较多样化,有些直接返回DTO甚至DO,另一些返回Result。
接口层的核心价值是对外,所以如果只是返回DTO或DO会不可避免的面临异常和错误栈泄漏到使用方的情况,包括错误栈被序列化反序列化的消耗。
所以,这里提出一个规范:
Application层的具体规范等下再讲,在这里先展示Interface层的逻辑。
举个例子:
@PostMapping("checkout")
public Result<OrderDTO> checkout(Long itemId, Integer quantity) {
try {
CheckoutCommand cmd = new CheckoutCommand();
OrderDTO orderDTO = checkoutService.checkout(cmd);
return Result.success(orderDTO);
} catch (ConstraintViolationException cve) {
// 捕捉一些特殊异常,比如Validation异常
return Result.fail(cve.getMessage());
} catch (Exception e) {
// 兜底异常捕获
return Result.fail(e.getMessage());
}
}
当然,每个接口都要写异常处理逻辑会比较烦,所以可以用AOP做个注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResultHandler {
}
@Aspect
@Component
public class ResultAspect {
@Around("@annotation(ResultHandler)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
Object proceed = null;
try {
proceed = joinPoint.proceed();
} catch (ConstraintViolationException cve) {
return Result.fail(cve.getMessage());
} catch (Exception e) {
return Result.fail(e.getMessage());
}
return proceed;
}
}
然后最终代码则简化为:
@PostMapping("checkout")
@ResultHandler
public Result<OrderDTO> checkout(Long itemId, Integer quantity) {
CheckoutCommand cmd = new CheckoutCommand();
OrderDTO orderDTO = checkoutService.checkout(cmd);
return Result.success(orderDTO);
}
在传统REST和RPC的接口规范中,同一个领域的方法放在一个领域的服务或Controller中。
具体来说:通常一个领域的接口,无论是REST的Resource资源的GET/POST/DELETE,还是RPC的方法,是追求相对固定的,统一的,而且会追求同一个领域的方法放在一个领域的服务或Controller中。
但是我发现在实际做业务的过程中,特别是当支撑的上游业务比较多时,刻意去追求接口的统一会有一个严重后果,通常会导致方法中的参数膨胀,或者导致方法的膨胀。
举个例子:假设有一个宠物卡和一个亲子卡的业务公用一个开卡服务,但是宠物需要传入宠物类型,亲子的需要传入宝宝年龄。
// 可以是RPC Provider 或者 Controller
public interface CardService {
// 1)统一接口,参数膨胀
Result openCard(int petType, int babyAge);
// 2)统一泛化接口,参数语意丢失
Result openCardV2(Map<String, Object> params);
// 3)不泛化,同一个类里的接口膨胀
Result openPetCard(int petType);
Result openBabyCard(int babyAge);
}
可以看出来,无论怎么操作,都有可能导致CardService这个服务未来越来越难以维护,方法越来越多,一个业务的变更有可能会导致整个服务/Controller的变更,最终变得无法维护。
我曾经参与过的一个服务,提供了几十个方法,上万行代码,可想而知,无论是使用方对接口的理解成本还是对代码的维护成本都是极高的。
所以,这里提出另一个规范:
基于上面的这个规范,可以发现宠物卡和亲子卡虽然看起来像是类似的需求,但并非是“同样需求”的,可以预见到在未来的某个时刻,这两个业务的需求和需要提供的接口会越走越远,所以需要将这两个接口类拆分开:
public interface PetCardService {
Result openPetCard(int petType);
}
public interface BabyCardService {
Result openBabyCard(int babyAge);
}
这个的好处是符合了Single Responsibility Principle单一职责原则,也就是说一个接口类仅仅会因为一个(或一类)业务的变化而变化。一个建议是当一个现有的接口类过度膨胀时,可以考虑对接口类做拆分,拆分原则和SRP(Single Responsibility Principle,简称 SRP)一致。
也许会有人问,如果按照这种做法,会不会产生大量的接口类,导致代码逻辑重复?
答案是不会,因为在DDD分层架构里,接口类的核心作用仅仅是协议层,每类业务的协议可以是不同的,而真实的业务逻辑会沉淀到应用层。
也就是说Interface和Application的关系是多对多的:
因为业务需求是快速变化的,所以接口层也要跟着快速变化,通过独立的接口层可以避免业务间相互影响,但我们希望相对稳定的是Application层的逻辑。所以我们接下来看一下Application层的一些规范。
Application层的几个核心类:
Application层最核心的对象是ApplicationService,它的核心功能是承接“业务流程“。
但是在讲ApplicationService的规范之前,必须要先重点的讲几个特殊类型的对象,即Command、Query和Event。
首先,回顾一下基础 的 CQRS 模式
CQRS(Command and Query Responsibility Segregation)命令查询职责分离模式,分别对读和写建模。
CQRS从定义上要求:
从本质上来看,Command、Query、Event 对象都是Value Object,但是从语义上来看有比较大的差异:
Command指令:指调用方明确想让系统操作的指令,其预期是对一个系统有影响,也就是写操作。
通常来讲指令需要有一个明确的返回值(如同步的操作结果,或异步的指令已经被接受)。
Query查询:指调用方明确想查询的东西,包括查询参数、过滤、分页等条件,其预期是对一个系统的数据完全不影响的,也就是只读操作。
Event事件:指一件已经发生过的既有事实,需要系统根据这个事实作出改变或者响应的,通常事件处理都会有一定的写操作。
事件处理器不会有返回值。这里需要注意一下的是,Application层的Event概念和Domain层的DomainEvent是类似的概念,但不一定是同一回事,这里的Event更多是外部一种通知机制而已。
简单总结下:
通常在很多代码里,能看到接口上有多个参数,比如上文中的案例:
如果需要在接口上增加参数,考虑到向前兼容,则需要增加一个方法:
或者常见的查询方法,由于条件的不同导致多个方法:
List < OrderDO> queryByItemId(Long itemId);
List < OrderDO> queryBySellerId(Long sellerId);
List < OrderDO> queryBySellerIdWithPage(Long sellerId, int currentPage, int pageSize);
可以看出来,传统的接口写法有几个问题:
但是另外一个最重要的问题是:这种类型的参数罗列,本身没有任何业务上的”语意“,只是一堆参数而已,无法明确的表达出来意图。
所以在Application层的接口里,强力建议的一个规范是:
ApplicationService的接口入参只能是一个Command、Query或Event对象,CQE对象需要能代表当前方法的语意。
唯一可以的例外是根据单一ID查询的情况,可以省略掉一个Query对象的创建。
按照上面的规范,实现案例是:
public interface CheckoutService {
OrderDTO checkout(@Valid CheckoutCommand cmd);
List<OrderDTO> query(OrderQuery query);
OrderDTO getOrder(Long orderId); // 注意单一ID查询可以不用Query
}
@Data
public class CheckoutCommand {
private Long userId;
private Long itemId;
private Integer quantity;
}
@Data
public class OrderQuery {
private Long sellerId;
private Long itemId;
private int currentPage;
private int pageSize;
}
这个规范的好处是:
CQE vs DTO
从上面的代码能看出来,ApplicationService的入参是CQE对象,但是出参却是一个DTO,从代码格式上来看都是简单的POJO对象,那么他们之间有什么区别呢?
但可能最重要的一点:
CQE作为ApplicationService的输入,必须保证其正确性,那么这个校验是放在哪里呢?
在最早的代码里,曾经有这样的校验逻辑,当时写在了服务里:
if (itemId <= 0 || quantity <= 0 || quantity >= 1000) {
return Result.fail("Invalid Args");
}
这种代码在日常非常常见,但其最大的问题就是,大量的非业务代码混杂在业务代码中。
很明显的违背了单一职责原则。
但,因为当时入参仅仅是简单的int,所以这个逻辑只能出现在服务里。
现在当入参改为了CQE之后,我们可以利用java标准JSR303或JSR380的Bean Validation来前置这个校验逻辑。
CQE对象的校验应该前置,避免在ApplicationService里做参数的校验。可以通过JSR303/380和Spring Validation来实现。
前面的例子可以改造为:
@Validated // Spring的注解
public class CheckoutServiceImpl implements CheckoutService {
OrderDTO checkout(@Valid CheckoutCommand cmd) { // 这里@Valid是JSR-303/380的注解
// 如果校验失败会抛异常,在interface层被捕捉
}
}
@Data
public class CheckoutCommand {
@NotNull(message = "用户未登陆")
private Long userId;
@NotNull
@Positive(message = "需要是合法的itemId")
private Long itemId;
@NotNull
@Min(value = 1, message = "最少1件")
@Max(value = 1000, message = "最多不能超过1000件")
private Integer quantity;
}
这种做法的好处是,让ApplicationService更加清爽,同时各种错误信息可以通过Bean Validation的API做各种个性化定制。
避免复用CQE
因为CQE是有“意图”和“语意”的,我们需要尽量避免CQE对象的复用,哪怕所有的参数都一样,只要他们的语意不同,尽量还是要用不同的对象。
规范:针对于不同语意的指令,要避免CQE对象的复用。
反例:一个常见的场景是“Create创建”和“Update更新”,一般来说这两种类型的对象唯一的区别是一个ID,创建没有ID,而更新则有。
所以经常能看见有的同学用同一个对象来作为两个方法的入参,唯一区别是ID是否赋值。
这个是错误的用法,因为这两个操作的语意完全不一样,他们的校验条件可能也完全不一样,所以不应该复用同一个对象。
正确的做法是:产出两个对象:
public interface CheckoutService {
OrderDTO checkout(@Valid CheckoutCommand cmd);
OrderDTO updateOrder(@Valid UpdateOrderCommand cmd);
}
@Data
public class UpdateOrderCommand {
@NotNull(message = "用户未登陆")
private Long userId;
@NotNull(message = "必须要有OrderID")
private Long orderId;
@NotNull
@Positive(message = "需要是合法的itemId")
private Long itemId;
@NotNull
@Min(value = 1, message = "最少1件")
@Max(value = 1000, message = "最多不能超过1000件")
private Integer quantity;
}
具体来说 ,ApplicationService 是将原有业务流水账代码剥离了校验逻辑、领域计算、持久化等逻辑之后剩余的流程,是“胶水层”代码。
参考一个简易的交易流程:
在这个案例里可以看出来,交易这个领域一共有5个用例:下单、支付成功、支付失败关单、物流信息更新、关闭订单。
这5个用例可以用5个Command/Event对象代替,也就是对应了5个方法。
我见过3种ApplicationService的组织形态:
(1)一个ApplicationService类是一个完整的业务流程,其中每个方法负责处理一个Use Case。
这种类型的具体案例如:
public interface CheckoutService {
// 下单
OrderDTO checkout(@Valid CheckoutCommand cmd);
// 支付成功
OrderDTO payReceived(@Valid PaymentReceivedEvent event);
// 支付取消
OrderDTO payCanceled(@Valid PaymentCanceledEvent event);
// 发货
OrderDTO packageSent(@Valid PackageSentEvent event);
// 收货
OrderDTO delivered(@Valid DeliveredEvent event);
// 批量查询
List<OrderDTO> query(OrderQuery query);
// 单个查询
OrderDTO getOrder(Long orderId);
}
(2)针对于比较复杂的业务流程,可以通过增加独立的CommandHandler、EventHandler来降低一个类中的代码量:
@Component
public class CheckoutCommandHandler implements CommandHandler<CheckoutCommand, OrderDTO> {
@Override
public OrderDTO handle(CheckoutCommand cmd) {
//
}
}
public class CheckoutServiceImpl implements CheckoutService {
@Resource
private CheckoutCommandHandler checkoutCommandHandler;
@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
return checkoutCommandHandler.handle(cmd);
}
}
(3)比较激进一点,通过CommandBus、EventBus,直接将指令或事件抛给对应的Handler,EventBus比较常见。
具体案例代码如下,通过消息队列收到MQ消息后,生成Event,然后由EventBus做路由到对应的Handler:
// 在这里框架通常可以根据接口识别到这个负责处理PaymentReceivedEvent
// 也可以通过增加注解识别
@Component
public class PaymentReceivedHandler implements EventHandler<PaymentReceivedEvent> {
@Override
public void process(PaymentReceivedEvent event) {
//
}
}
// Interface层,这个是RocketMQ的Listener
public class OrderMessageListener implements MessageListenerOrderly {
@Resource
private EventBus eventBus;
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
PaymentReceivedEvent event = new PaymentReceivedEvent();
eventBus.dispatch(event); // 不需要指定消费者
return ConsumeOrderlyStatus.SUCCESS;
}
}
不建议:这种做法可以实现Interface层和某个具体的ApplicationService或Handler的完全静态解藕,在运行时动态dispatch,做的比较好的框架如AxonFramework。
虽然看起来很便利,但是根据我们自己业务的实践和踩坑发现,当代码中的CQE对象越来越多,handler越来越复杂时,运行时的dispatch缺乏了静态代码间的关联关系,导致代码很难读懂,特别是当你需要trace一个复杂调用链路时,因为dispatch是运行时的,很难摸清楚具体调用到的对象。
所以我们虽然曾经有过这种尝试,但现在已经不建议这么做了。
虽然之前曾经无数次重复ApplicationService只负责业务流程串联,不负责业务逻辑,但如何判断一段代码到底是业务流程还是逻辑呢?
举个之前的例子,最初的代码重构后:
@Service
@Validated
public class CheckoutServiceImpl implements CheckoutService {
private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE;
@Resource
private ItemService itemService;
@Resource
private InventoryService inventoryService;
@Resource
private OrderRepository orderRepository;
@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
ItemDO item = itemService.getItem(cmd.getItemId());
if (item == null) {
throw new IllegalArgumentException("Item not found");
}
boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity());
if (!withholdSuccess) {
throw new IllegalArgumentException("Inventory not enough");
}
Order order = new Order();
order.setBuyerId(cmd.getUserId());
order.setSellerId(item.getSellerId());
order.setItemId(item.getItemId());
order.setItemTitle(item.getTitle());
order.setItemUnitPrice(item.getPriceInCents());
order.setCount(cmd.getQuantity());
Order savedOrder = orderRepository.save(order);
return orderDtoAssembler.orderToDTO(savedOrder);
}
}
(1)不要有if/else分支逻辑
也就是说代码的Cyclomatic Complexity(循环复杂度)应该尽量等于1。
通常情况下,如果有分支逻辑的,都代表一些业务判断,那么,应该将逻辑封装到DomainService或者Entity里。
但,这不代表Application Service 完全不能有if逻辑,比如,在这段代码里:
boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity());
if (!withholdSuccess) {
throw new IllegalArgumentException("Inventory not enough");
}
虽然CC > 1,但是仅仅代表了中断条件,具体的业务逻辑处理并没有受影响。可以把它看作为Precondition。
(2)不要有任何计算
在最早的代码里有这个计算:
// 5)领域计算
Long cost = item.getPriceInCents() * quantity;
order.setTotalCost(cost);
通过将这个计算逻辑封装到实体里,避免在ApplicationService里做计算:
@Data
public class Order {
private Long itemUnitPrice;
private Integer count;
// 把原来一个在ApplicationService的计算迁移到Entity里
public Long getTotalCost() {
return itemUnitPrice * count;
}
}
order.setItemUnitPrice(item.getPriceInCents());
order.setCount(cmd.getQuantity());
(3)一些数据的转化可以交给其他对象来做
比如DTO Assembler,将对象间转化的逻辑抽取和剥离在单独的类中,降低ApplicationService的复杂度。
OrderDTO dto = orderDtoAssembler.orderToDTO(savedOrder);
我们可以看出来,ApplicationService的代码通常有类似的结构:
一般ApplicationService的“套路”如下:
如果涉及到对多个外部系统(包括自身的DB)都有变更的情况,这个时候通常处在“分布式事务”的场景里,无论是用分布式TX、TCC、还是Saga模式,取决于具体场景的设计,在此处暂时略过。
…
由于字数限制,此处省略
完整内容,请参见尼恩的《DDD圣经》,pdf 找尼恩获取,方式见文末
…
由于字数限制,此处省略
完整内容,请参见尼恩的《DDD圣经》,pdf 找尼恩获取,方式见文末
…
由于字数限制,此处省略
完整内容,请参见尼恩的《DDD圣经》,pdf 找尼恩获取,方式见文末
在复杂的业务流程里,我们通常面临两种模式:Orchestration 和 Choreography。
很无奈,这两个英文单词的百度翻译/谷歌翻译,都是“编排”,但实际上这两种模式是完全不一样的设计模式。
用一个常见的例子:下单后支付并发货。
如果这个案例是Orchestration编排,则业务逻辑为:下单时从一个预存的账户里扣取资金,并且生成物流单发货,从图上看是这样的:
如果这个案例是Choreography协作,则业务逻辑为:下单,然后等支付成功事件,然后再发货,类似这样:
…
由于字数限制,此处省略
完整内容,请参见尼恩的《DDD圣经》,pdf 找尼恩获取,方式见文末
最后,讲了这么多O vs C,跟DDD有啥关系?很简单:
所以,虽然Orchestration 和 Choreography是两种完全不同的业务设计模式,但最终落到Application层的代码应该是一致的,这也是为什么Application层是“用例”而不是“接口”,是相对稳定的存在。
只要是做业务的,一定会需要写业务流程和服务编排,但不代表这种代码一定质量差。
通过DDD的分层架构里的Interface层和Application层的合理拆分,代码可以变得优雅、灵活,能更快的响应业务但同时又能更好的沉淀。
本文主要介绍了一些代码的设计规范,帮助大家掌握一定的技巧。
Interface层:
Application层:
部分Infra层:
业务流程设计模式:
DDD 面试题,是非常常见的面试题。 DDD的学习材料, 汗牛塞屋,又缺乏经典。
《殷浩详解DDD:领域层设计规范》做到从0到1带大家精通DDD,非常难得。
这里,把尼恩修改过的 《殷浩详解DDD:领域层设计规范》,通过尼恩的公众号《技术自由圈》发布出来。
大家面试的时候, 可以参考以上的内容去组织答案,如果大家能做到对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
另外在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,并且在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
当然,关于DDD,尼恩即将给大家发布一波视频 《第34章:DDD的顶奢面经》。
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓