当前位置: 首页 > news >正文

广州网站建设小程序开发wordpress个人简历

广州网站建设小程序开发,wordpress个人简历,百度网站登录入口,域名注册服务网站查询简介#xff1a; 在日常工作中我观察到#xff0c;面对老系统重构和迁移场景#xff0c;有大量代码属于流水账代码#xff0c;通常能看到开发在对外的API接口里直接写业务逻辑代码#xff0c;或者在一个服务里大量的堆接口#xff0c;导致业务逻辑实际无法收敛#xff0…简介 在日常工作中我观察到面对老系统重构和迁移场景有大量代码属于流水账代码通常能看到开发在对外的API接口里直接写业务逻辑代码或者在一个服务里大量的堆接口导致业务逻辑实际无法收敛接口复用性比较差。所以本文主要想系统性的解释一下如何通过DDD的重构将原有的流水账代码改造为逻辑清晰、职责分明的模块。 作者 | 殷浩 来源 | 阿里技术公众号 在日常工作中我观察到面对老系统重构和迁移场景有大量代码属于流水账代码通常能看到开发在对外的API接口里直接写业务逻辑代码或者在一个服务里大量的堆接口导致业务逻辑实际无法收敛接口复用性比较差。所以本文主要想系统性的解释一下如何通过DDD的重构将原有的流水账代码改造为逻辑清晰、职责分明的模块。 一 案例简介 这里举一个简单的常见案例下单链路。假设我们在做一个checkout接口需要做各种校验、查询商品信息、调用库存服务扣库存、然后生成订单 一个比较典型的代码如下 RestController RequestMapping(/) public class CheckoutController {Resourceprivate ItemService itemService;Resourceprivate InventoryService inventoryService;Resourceprivate OrderRepository orderRepository;PostMapping(checkout)public ResultOrderDO 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);} } 为什么这种典型的流水账代码在实际应用中会有问题呢其本质问题是违背了SRPSingle Responsbility Principle单一职责原则。这段代码里混杂了业务计算、校验逻辑、基础设施、和通信协议等在未来无论哪一部分的逻辑变更都会直接影响到这段代码当后人不断地在上面叠加新的逻辑时会使代码复杂度增加、逻辑分支越来越多最终造成bug或者没人敢重构的历史包袱。 所以我们才需要用DDD的分层思想去重构一下以上的代码通过不同的代码分层和规范拆分出逻辑清晰职责明确的分层和模块也便于一些通用能力的沉淀。 主要的几个步骤分为 分离出独立的Interface接口层负责处理网络协议相关的逻辑。从真实业务场景中找出具体用例Use Cases然后将具体用例通过专用的Command指令、Query查询、和Event事件对象来承接。分离出独立的Application应用层负责业务流程的编排响应Command、Query和Event。每个应用层的方法应该代表整个业务流程中的一个节点。处理一些跨层的横切关注点如鉴权、异常处理、校验、缓存、日志等。 下面会针对每个点做详细的解释。 二 Interface接口层 随着REST和MVC架构的普及经常能看到开发同学直接在Controller中写业务逻辑如上面的典型案例但实际上MVC Controller不是唯一的重灾区。以下的几种常见的代码写法通常都可能包含了同样的问题 HTTP 框架如Spring MVC框架Spring Cloud等。RPC 框架如Dubbo、HSF、gRPC等。消息队列MQ的“消费者”比如JMS的 onMessageRocketMQ的MessageListener等。Socket通信Socket通信的receive、WebSocket的onMessage等。文件系统WatcherService等。分布式任务调度SchedulerX等。 这些的方法都有一个共同的点就是都有自己的网络协议而如果我们的业务代码和网络协议混杂在一起则会直接导致代码跟网络协议绑定无法被复用。 所以在DDD的分层架构中我们单独会抽取出来Interface接口层作为所有对外的门户将网络协议和业务逻辑解耦。 1 接口层的组成 接口层主要由以下几个功能组成 网络协议的转化通常这个已经由各种框架给封装掉了我们需要构建的类要么是被注解的bean要么是继承了某个接口的bean。统一鉴权比如在一些需要AppKeySecret的场景需要针对某个租户做鉴权的包括一些加密串的校验Session管理一般在面向用户的接口或者有登陆态的通过Session或者RPC上下文可以拿到当前调用的用户以便传递给下游服务。限流配置对接口做限流避免大流量打到下游服务前置缓存针对变更不是很频繁的只读场景可以前置结果缓存到接口层异常处理通常在接口层要避免将异常直接暴露给调用端所以需要在接口层做统一的异常捕获转化为调用端可以理解的数据格式日志在接口层打调用日志用来做统计和debug等。一般微服务框架可能都直接包含了这些功能。 当然如果有一个独立的网关设施/应用则可以抽离出鉴权、Session、限流、日志等逻辑但是目前来看API网关也只能解决一部分的功能即使在有API网关的场景下应用里独立的接口层还是有必要的。 在Interface层鉴权、Session、限流、缓存、日志等都比较直接只有一个异常处理的点需要重点说下。 2 返回值和异常处理规范Result vs Exception 注这部分主要还是面向REST和RPC接口其他的协议需要根据协议的规范产生返回值。在我见过的一些代码里接口的返回值比较多样化有些直接返回DTO甚至DO另一些返回Result。 接口层的核心价值是对外所以如果只是返回DTO或DO会不可避免的面临异常和错误栈泄漏到使用方的情况包括错误栈被序列化反序列化的消耗。所以这里提出一个规范 Interface层的HTTP和RPC接口返回值为Result捕捉所有异常 Application层的所有接口返回值为DTO不负责处理异常Application层的具体规范等下再讲在这里先展示Interface层的逻辑。 举个例子 PostMapping(checkout) public ResultOrderDTO 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 ResultOrderDTO checkout(Long itemId, Integer quantity) {CheckoutCommand cmd new CheckoutCommand();OrderDTO orderDTO checkoutService.checkout(cmd);return Result.success(orderDTO); } 3 接口层的接口的数量和业务间的隔离 在传统REST和RPC的接口规范中通常一个领域的接口无论是REST的Resource资源的GET/POST/DELETE还是RPC的方法是追求相对固定的统一的而且会追求统一个领域的方法放在一个领域的服务或Controller中。 但是我发现在实际做业务的过程中特别是当支撑的上游业务比较多时刻意去追求接口的统一通常会导致方法中的参数膨胀或者导致方法的膨胀。举个例子假设有一个宠物卡和一个亲子卡的业务公用一个开卡服务但是宠物需要传入宠物类型亲子的需要传入宝宝年龄。 // 可以是RPC Provider 或者 Controller public interface CardService {// 1统一接口参数膨胀Result openCard(int petType, int babyAge);// 2统一泛化接口参数语意丢失Result openCardV2(MapString, Object params);// 3不泛化同一个类里的接口膨胀Result openPetCard(int petType);Result openBabyCard(int babyAge); } 可以看出来无论怎么操作都有可能导致CardService这个服务未来越来越难以维护方法越来越多一个业务的变更有可能会导致整个服务/Controller的变更最终变得无法维护。我曾经参与过的一个服务提供了几十个方法上万行代码可想而知无论是使用方对接口的理解成本还是对代码的维护成本都是极高的。 所以这里提出另一个规范 一个Interface层的类应该是“小而美”的应该是面向“一个单一的业务”或“一类同样需求的业务”需要尽量避免用同一个类承接不同类型业务的需求。基于上面的这个规范可以发现宠物卡和亲子卡虽然看起来像是类似的需求但并非是“同样需求”的可以预见到在未来的某个时刻这两个业务的需求和需要提供的接口会越走越远所以需要将这两个接口类拆分开 public interface PetCardService {Result openPetCard(int petType); }public interface BabyCardService {Result openBabyCard(int babyAge); } 这个的好处是符合了Single Responsibility Principle单一职责原则也就是说一个接口类仅仅会因为一个或一类业务的变化而变化。一个建议是当一个现有的接口类过度膨胀时可以考虑对接口类做拆分拆分原则和SRP一致。 也许会有人问如果按照这种做法会不会产生大量的接口类导致代码逻辑重复答案是不会因为在DDD分层架构里接口类的核心作用仅仅是协议层每类业务的协议可以是不同的而真实的业务逻辑会沉淀到应用层。也就是说Interface和Application的关系是多对多的 因为业务需求是快速变化的所以接口层也要跟着快速变化通过独立的接口层可以避免业务间相互影响但我们希望相对稳定的是Application层的逻辑。所以我们接下来看一下Application层的一些规范。 三 Application层 1 Application层的组成部分 Application层的几个核心类 ApplicationService应用服务最核心的类负责业务流程的编排但本身不负责任何业务逻辑。DTO Assembler负责将内部领域模型转化为可对外的DTO。Command、Query、Event对象作为ApplicationService的入参。返回的DTO作为ApplicationService的出参。 Application层最核心的对象是ApplicationService它的核心功能是承接“业务流程“。但是在讲ApplicationService的规范之前必须要先重点的讲几个特殊类型的对象即Command、Query和Event。 2 Command、Query、Event对象 从本质上来看这几种对象都是Value Object但是从语义上来看有比较大的差异 Command指令指调用方明确想让系统操作的指令其预期是对一个系统有影响也就是写操作。通常来讲指令需要有一个明确的返回值如同步的操作结果或异步的指令已经被接受。Query查询指调用方明确想查询的东西包括查询参数、过滤、分页等条件其预期是对一个系统的数据完全不影响的也就是只读操作。Event事件指一件已经发生过的既有事实需要系统根据这个事实作出改变或者响应的通常事件处理都会有一定的写操作。事件处理器不会有返回值。这里需要注意一下的是Application层的Event概念和Domain层的DomainEvent是类似的概念但不一定是同一回事这里的Event更多是外部一种通知机制而已。 简单总结下 为什么要用CQE对象 通常在很多代码里能看到接口上有多个参数比如上文中的案例 如果需要在接口上增加参数考虑到向前兼容则需要增加一个方法 或者常见的查询方法由于条件的不同导致多个方法 List OrderDO queryByItemId(Long itemId); List OrderDO queryBySellerId(Long sellerId); List OrderDO queryBySellerIdWithPage(Long sellerId, int currentPage, int pageSize); 可以看出来传统的接口写法有几个问题 接口膨胀一个查询条件一个方法。难以扩展每新增一个参数都有可能需要调用方升级。难以测试接口一多职责随之变得繁杂业务场景各异测试用例难以维护。 但是另外一个最重要的问题是这种类型的参数罗列本身没有任何业务上的”语意“只是一堆参数而已无法明确的表达出来意图。 CQE的规范 所以在Application层的接口里强力建议的一个规范是 ApplicationService的接口入参只能是一个Command、Query或Event对象CQE对象需要能代表当前方法的语意。唯一可以的例外是根据单一ID查询的情况可以省略掉一个Query对象的创建。按照上面的规范实现案例是 public interface CheckoutService {OrderDTO checkout(Valid CheckoutCommand cmd);ListOrderDTO 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对象那么他们之间有什么区别呢 CQECQE对象是ApplicationService的输入是有明确的”意图“的所以这个对象必须保证其”正确性“。DTODTO对象只是数据容器只是为了和外部交互所以本身不包含任何逻辑只是贫血对象。 但可能最重要的一点因为CQE是”意图“所以CQE对象在理论上可以有”无限“个每个代表不同的意图但是DTO作为模型数据容器和模型一一对应所以是有限的。 CQE的校验 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;NotNullPositive(message 需要是合法的itemId)private Long itemId;NotNullMin(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;NotNullPositive(message 需要是合法的itemId)private Long itemId;NotNullMin(value 1, message 最少1件)Max(value 1000, message 最多不能超过1000件)private Integer quantity;} 3 ApplicationService 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);// 批量查询ListOrderDTO query(OrderQuery query);// 单个查询OrderDTO getOrder(Long orderId); }2针对于比较复杂的业务流程可以通过增加独立的CommandHandler、EventHandler来降低一个类中的代码量 Component public class CheckoutCommandHandler implements CommandHandlerCheckoutCommand, OrderDTO {Overridepublic OrderDTO handle(CheckoutCommand cmd) {//} }public class CheckoutServiceImpl implements CheckoutService {Resourceprivate CheckoutCommandHandler checkoutCommandHandler;Overridepublic OrderDTO checkout(Valid CheckoutCommand cmd) {return checkoutCommandHandler.handle(cmd);} } 3比较激进一点通过CommandBus、EventBus直接将指令或事件抛给对应的HandlerEventBus比较常见。具体案例代码如下通过消息队列收到MQ消息后生成Event然后由EventBus做路由到对应的Handler // 在这里框架通常可以根据接口识别到这个负责处理PaymentReceivedEvent // 也可以通过增加注解识别 Component public class PaymentReceivedHandler implements EventHandlerPaymentReceivedEvent {Overridepublic void process(PaymentReceivedEvent event) {//} }// Interface层这个是RocketMQ的Listener public class OrderMessageListener implements MessageListenerOrderly {Resourceprivate EventBus eventBus;Overridepublic ConsumeOrderlyStatus consumeMessage(ListMessageExt msgs, ConsumeOrderlyContext context) {PaymentReceivedEvent event new PaymentReceivedEvent();eventBus.dispatch(event); // 不需要指定消费者return ConsumeOrderlyStatus.SUCCESS;} } 不建议这种做法可以实现Interface层和某个具体的ApplicationService或Handler的完全静态解藕在运行时动态dispatch做的比较好的框架如AxonFramework。虽然看起来很便利但是根据我们自己业务的实践和踩坑发现当代码中的CQE对象越来越多handler越来越复杂时运行时的dispatch缺乏了静态代码间的关联关系导致代码很难读懂特别是当你需要trace一个复杂调用链路时因为dispatch是运行时的很难摸清楚具体调用到的对象。所以我们虽然曾经有过这种尝试但现在已经不建议这么做了。 Application Service 是业务流程的封装不处理业务逻辑 虽然之前曾经无数次重复ApplicationService只负责业务流程串联不负责业务逻辑但如何判断一段代码到底是业务流程还是逻辑呢 举个之前的例子最初的代码重构后 Service Validated public class CheckoutServiceImpl implements CheckoutService {private final OrderDtoAssembler orderDtoAssembler OrderDtoAssembler.INSTANCE;Resourceprivate ItemService itemService;Resourceprivate InventoryService inventoryService;Resourceprivate OrderRepository orderRepository;Overridepublic 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里。但这不代表完全不能有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的代码通常有类似的结构AppService通常不做任何决策Precondition除外仅仅是把所有决策交给DomainService或Entity把跟外部交互的交给Infrastructure接口如Repository或防腐层。 一般的“套路”如下 准备数据包括从外部服务或持久化源取出相对应的Entity、VO以及外部服务返回的DTO。执行操作包括新对象的创建、赋值以及调用领域对象的方法对其进行操作。需要注意的是这个时候通常都是纯内存操作非持久化。持久化将操作结果持久化或操作外部系统产生相应的影响包括发消息等异步操作。 如果涉及到对多个外部系统包括自身的DB都有变更的情况这个时候通常处在“分布式事务”的场景里无论是用分布式TX、TCC、还是Saga模式取决于具体场景的设计在此处暂时略过。 4 DTO Assembler 一个经常被忽视的问题是 ApplicationService应该返回 Entity 还是 DTO这里提出一个规范在DDD分层架构中 ApplicationService应该永远返回DTO而不是Entity。为什么呢 构建领域边界ApplicationService的入参是CQE对象出参是DTO这些基本上都属于简单的POJO来确保Application层的内外互相不影响。降低规则依赖Entity里面通常会包含业务规则如果ApplicationService返回Entity则会导致调用方直接依赖业务规则。如果内部规则变更可能直接影响到外部。通过DTO组合降低成本Entity是有限的DTO可以是多个Entity、VO的自由组合一次性封装成复杂DTO或者有选择的抽取部分参数封装成DTO可以降低对外的成本。 因为我们操作的对象是Entity但是输出的对象是DTO这里就需要一个专属类型的对象叫DTO Assembler。DTO Assembler的唯一职责是将一个或多个Entity/VO转化为DTO。注意DTO Assembler通常不建议有反操作也就是不会从DTO到Entity因为通常一个DTO转化为Entity时是无法保证Entity的准确性的。 通常Entity转DTO是有成本的无论是代码量还是运行时的操作。手写转换代码容易出错为了节省代码量用Reflection会造成极大的性能损耗。所以这里我还是不遗余力的推荐MapStruct这个库。MapStruct通过静态编译时代码生成通过写接口和配置注解就可以生成对应的代码且因为生成的代码是直接赋值其性能损耗基本可以忽略不计。 通过MapStruct代码即可简化为 import org.mapstruct.Mapper; Mapper public interface OrderDtoAssembler {OrderDtoAssembler INSTANCE Mappers.getMapper(OrderDtoAssembler.class);OrderDTO orderToDTO(Order order); }public class CheckoutServiceImpl implements CheckoutService {private final OrderDtoAssembler orderDtoAssembler OrderDtoAssembler.INSTANCE;Overridepublic OrderDTO checkout(Valid CheckoutCommand cmd) {// ...Order order new Order(); // ...Order savedOrder orderRepository.save(order);return orderDtoAssembler.orderToDTO(savedOrder);} } 结合之前的Data MapperDTO、Entity和DataObject之间的关系如下图 5 Result vs Exception 最后上文曾经提及在Interface层应该返回Result在Application层应该返回DTO在这里再次重复提出规范 Application层只返回DTO可以直接抛异常不用统一处理。所有调用到的服务也都可以直接抛异常除非需要特殊处理否则不需要刻意捕捉异常。异常的好处是能明确的知道错误的来源堆栈等在Interface层统一捕捉异常是为了避免异常堆栈信息泄漏到API之外但是在Application层异常机制仍然是信息量最大代码结构最清晰的方法避免了Result的一些常见且繁杂的Result.isSuccess判断。所以在Application层、Domain层以及Infrastructure层遇到错误直接抛异常是最合理的方法。 6 Anti-Corruption Layer防腐层 本文仅仅简单描述一下ACL的原理和作用具体的实施规范可能要等到另外一篇文章。在ApplicationService中经常会依赖外部服务从代码层面对外部系统产生了依赖。比如上文中的 ItemDO item itemService.getItem(cmd.getItemId()); boolean withholdSuccess inventoryService.withhold(cmd.getItemId(), cmd.getQuantity()); 会发现我们的ApplicationService会强依赖ItemService、InventoryService以及ItemDO这个对象。如果任何一个服务的方法变更或者ItemDO字段变更都会有可能影响到ApplicationService的代码。也就是说我们自己的代码会因为强依赖了外部系统的变化而变更这个在复杂系统中应该是尽量避免的。那么如何做到对外部系统的隔离呢需要加入ACL防腐层。 ACL防腐层的简单原理如下 对于依赖的外部对象我们抽取出所需要的字段生成一个内部所需的VO或DTO类。构建一个新的Facade在Facade中封装调用链路将外部类转化为内部类。针对外部系统调用同样的用Facade方法封装外部调用链路。 无防腐层的情况 有防腐层的情况 具体简单实现假设所有外部依赖都命名为ExternalXXXService Data public class ItemDTO {private Long itemId;private Long sellerId;private String title;private Long priceInCents; }// 商品Facade接口 public interface ItemFacade {ItemDTO getItem(Long itemId); } // 商品facade实现 Service public class ItemFacadeImpl implements ItemFacade {Resourceprivate ExternalItemService externalItemService;Overridepublic ItemDTO getItem(Long itemId) {ItemDO itemDO externalItemService.getItem(itemId);if (itemDO ! null) {ItemDTO dto new ItemDTO();dto.setItemId(itemDO.getItemId());dto.setTitle(itemDO.getTitle());dto.setPriceInCents(itemDO.getPriceInCents());dto.setSellerId(itemDO.getSellerId());return dto;}return null;} }// 库存Facade public interface InventoryFacade {boolean withhold(Long itemId, Integer quantity); } Service public class InventoryFacadeImpl implements InventoryFacade {Resourceprivate ExternalInventoryService externalInventoryService;Overridepublic boolean withhold(Long itemId, Integer quantity) {return externalInventoryService.withhold(itemId, quantity);} } 通过ACL改造之后我们ApplicationService的代码改为 Service public class CheckoutServiceImpl implements CheckoutService {Resourceprivate ItemFacade itemFacade;Resourceprivate InventoryFacade inventoryFacade;Overridepublic OrderDTO checkout(Valid CheckoutCommand cmd) {ItemDTO item itemFacade.getItem(cmd.getItemId());if (item null) {throw new IllegalArgumentException(Item not found);}boolean withholdSuccess inventoryFacade.withhold(cmd.getItemId(), cmd.getQuantity());if (!withholdSuccess) {throw new IllegalArgumentException(Inventory not enough);}// ...} } 很显然这么做的好处是ApplicationService的代码已经完全不再直接依赖外部的类和方法而是依赖了我们自己内部定义的值类和接口。如果未来外部服务有任何的变更需要修改的是Facade类和数据转化逻辑而不需要修改ApplicationService的逻辑。 Repository可以认为是一种特殊的ACL屏蔽了具体数据操作的细节即使底层数据库结构变更数据库类型变更或者加入其他的持久化方式Repository的接口保持稳定ApplicationService就能保持不变。 在一些理论框架里ACL Facade也被叫做Gateway含义是一样的。 四 Orchestration vs Choreography 在本文最后想聊一下复杂业务流程的设计规范。在复杂的业务流程里我们通常面临两种模式Orchestration 和 Choreography。很无奈这两个英文单词的百度翻译/谷歌翻译都是“编排”但实际上这两种模式是完全不一样的设计模式。 Orchestration的编排比如SOA/微服务的服务编排Service Orchestration是我们通常熟悉的用法Choreography是最近出现了事件驱动架构EDA才慢慢流行起来。网上可能会有其他的翻译比如编制、编舞、协作等但感觉都没有真正的把英文单词的意思表达出来所以为了避免误解在下文我尽量还是用英文原词。如果谁有更好的翻译方法欢迎联系我。 1 模式简介 Orchestration通常出现在脑海里的是一个交响乐团Orchestra注意这两个词的相似性。交响乐团的核心是一个唯一的指挥家Conductor在一个交响乐中所有的音乐家必须听从Conductor的指挥做操作不可以独自发挥。所以在Orchestration模式中所有的流程都是由一个节点或服务触发的。我们常见的业务流程代码包括调用外部服务就是Orchestration由我们的服务统一触发。Choreography通常会出现在脑海的场景是一个舞剧来自于希腊文的舞蹈Choros。其中每个不同的舞蹈家都在做自己的事但是没有一个中心化的指挥。通过协作配合每个人做好自己的事整个舞蹈可以展现出一个完整的、和谐的画面。所以在Choreography模式中每个服务都是独立的个体可能会响应外部的一些事件但整个系统是一个整体。 2 案例 用一个常见的例子下单后支付并发货。 如果这个案例是Orchestration则业务逻辑为下单时从一个预存的账户里扣取资金并且生成物流单发货从图上看是这样的 如果这个案例是Choreography则业务逻辑为下单然后等支付成功事件然后再发货类似这样 3 模式的区别和选择 虽然看起来这两种模式都能达到一样的业务目的但是在实际开发中他们有巨大的差异。 从代码依赖关系来看 Orchestration涉及到一个服务调用到另外的服务对于调用方来说是强依赖的服务提供方。Choreography每一个服务只是做好自己的事然后通过事件触发其他的服务服务之间没有直接调用上的依赖。但要注意的是下游还是会依赖上游的代码比如事件类所以可以认为是下游对上游有依赖。 从代码灵活性来看 Orchestration因为服务间的依赖关系是写死的增加新的业务流程必然需要修改代码。Choreography因为服务间没有直接调用关系可以增加或替换服务而不需要改上游代码。 从调用链路来看 Orchestration是从一个服务主动调用另一个服务所以是Command-Driven指令驱动的。Choreography是每个服务被动的被外部事件触发所以是Event-Driven事件驱动的。 从业务职责来看 Orchestration有主动的调用方比如下单服务。无论下游的依赖是谁主动的调用方都需要为整个业务流程和结果负责。Choreography没有主动调用方每个服务只关心自己的触发条件和结果没有任何一个服务会为整个业务链路负责。 小结 另外需要重点明确的“指令驱动”和“事件驱动”的区别不是“同步”和“异步”。指令可以是同步调用也可以是异步消息触发但异步指令不是事件反过来事件可以是异步消息但也完全可以是进程内的同步调用。所以指令驱动和事件驱动差异的本质不在于调用方式而是一件事情是否“已经”发生。 所以在日常业务中当你碰到一个需求时该如何选择是用Orchestration还是Choreography 这里给出两个判断方法 1明确依赖的方向 在代码中的依赖是比较明确的如果你是下游上游对你无感知则只能走事件驱动如果上游必须要对你有感知则可以走指令驱动。反过来如果你是上游需要对下游强依赖则是指令驱动如果下游是谁无所谓则可以走事件驱动。 2找出业务中的“负责人” 第二种方法是根据业务场景找出其中的“负责人”。比如如果业务需要通知卖家下单系统的单一职责不应该为消息通知负责但订单管理系统需要根据订单状态的推进主动触发消息所以是这个功能的负责人。 在一个复杂业务流程里通常两个模式都要有但也很容易设计错误。如果出现依赖关系很奇怪或者代码里调用链路/负责人梳理不清楚的情况可以尝试转换一下模式可能会好很多。 哪个模式更好 很显然没有最好的模式只有最合适自己业务场景的模式。 反例最近几年比较流行的Event-Driven ArchitectureEDA事件驱动架构以及Reactive-Programming响应式编程比如RxJava虽然有很多创新但在一定程度上是“当你有把锤子所有问题都是钉子”的典型案例。他们对一些基于事件的、流处理的问题有奇效但如果拿这些框架硬套指令驱动的业务就会感到代码极其“不协调”认知成本提高。所以在日常选型中还是要先根据业务场景梳理出来是哪些流程中的部分是Orchestration哪些是Choreography然后再选择相对应的框架。 4 跟DDD分层架构的关系 最后讲了这么多O vs C跟DDD有啥关系很简单 OC其实是Interface层的关注点Orchestration 对外的API而Choreography 消息或事件。当你决策了O还是C之后需要在Interface层承接这些“驱动力”。无论OC如何设计Application层都“无感知”因为ApplicationService天生就可以处理Command、Query和Event至于这些对象怎么来是Interface层的决策。 所以虽然Orchestration 和 Choreography是两种完全不同的业务设计模式但最终落到Application层的代码应该是一致的这也是为什么Application层是“用例”而不是“接口”是相对稳定的存在。 五 总结 只要是做业务的一定会需要写业务流程和服务编排但不代表这种代码一定质量差。通过DDD的分层架构里的Interface层和Application层的合理拆分代码可以变得优雅、灵活能更快的响应业务但同时又能更好的沉淀。本文主要介绍了一些代码的设计规范帮助大家掌握一定的技巧。 Interface层 职责主要负责承接网络协议的转化、Session管理等。接口数量避免所谓的统一API不必人为限制接口类的数量每个/每类业务对应一套接口即可接口参数应该符合业务需求避免大而全的入参。接口出参统一返回Result。异常处理应该捕捉所有异常避免异常信息的泄漏。可以通过AOP统一处理避免代码里有大量重复代码。 Application层 入参具像化Command、Query、Event对象作为ApplicationService的入参唯一可以的例外是单ID查询的场景。CQE的语意化CQE对象有语意不同用例之间语意不同即使参数一样也要避免复用。入参校验基础校验通过Bean Validation api解决。Spring Validation自带Validation的AOP也可以自己写AOP。出参统一返回DTO而不是Entity或DO。DTO转化用DTO Assembler负责Entity/VO到DTO的转化。异常处理不统一捕捉异常可以随意抛异常。 部分Infra层 用ACL防腐层将外部依赖转化为内部代码隔离外部的影响。 业务流程设计模式 没有最好的模式取决于业务场景、依赖关系、以及是否有业务“负责人”。避免拿着锤子找钉子。 原文链接 本文为阿里云原创内容未经允许不得转载。
http://www.pierceye.com/news/572423/

相关文章:

  • 宁波新亚建设公司网站简单网站建设
  • 做网站没赚到钱网站后台地址忘记了
  • 备案网站公共查询安阳县
  • wordpress 超级管理员seo优化网络公司
  • 商务推广网站宝塔做网站
  • 我想建一个网站怎么建python做的大型网站
  • 为网站设计手机版wordpress怎样比较安全
  • 网站优化方式重庆建设网站哪家专业
  • php做网站基本流程旅游网站论文
  • 网站前期准备网页制作需要学多久
  • 广园路建设公司网站建app网站要多少钱
  • 网站域名是什么东西wordpress农历插件
  • 专业网站建设公司首选公司wordpress fruitful
  • 微博wap版登录入口seo 网站标题长度
  • 网站面包屑导航设计即位置导航局域网安装wordpress
  • 泰安网站建设xtempire国家开放大学网站界面设计
  • 绘制网站结构图建站公司售后服务
  • 漂亮的博客网站模板装修公司网站开发
  • 厦门网站注册与网页设计公司wordpress找不到php的拓展
  • 常熟网站建设icp备案自己怎样创建网站
  • 移动互联网站建设seo流量排名门户
  • 做腰椎核磁证网站是 收 七设计网络品牌营销方案思路
  • 外贸网站建站系统基于php网站开发
  • 可以做代销的网站都有哪些神马网站快速排名案例
  • 个人能申请网站吗百度站长提交网址
  • 给素材网站做签约设计不想做了网络规划设计师教程第2版pdf
  • 新做的网站怎样推广html代码加密
  • 织梦淘宝客网站嘉兴网站开发公司
  • 宁波网站推广营销网上购物软件哪个好
  • 网站 风格做网站都可以做什么