【DDD】学习笔记-服务行为模型

news2024/11/18 11:37:30

如果将服务视为一种行为,就必然需要考虑客户端与服务之间的协作。服务行为的调用者可以认为是服务消费者(Service Consumer),提供服务行为的对象则是服务提供者(Service Provider)。为了服务消费者能够发现服务,还需要提供者发布已经公开的服务,因此需要引入服务注册(Service Registry),从而满足 SOA 的概念模型:

img

以服务行为来驱动服务的定义,需要从消费者与提供者之间的协作关系来确定服务接口。消费者发起服务请求,提供者履行职责并返回结果,这就构成了所谓的“服务契约(Service Contract)”。契约是义务与权利的一种规范。Bertrand Meyer 认为:

“对于一个大型系统来说,光保证它的各组成部分的质量是不够的。最有价值的是确保在任何两个组成部分的交接处设计明晰的彼此义务和权利规范,即所谓契约。”

在面向外部远程服务进行设计时,契约的设计尤为重要,它直接影响了整个系统的稳定性与性能。

虽然服务资源模型同样构成了服务契约,但当从行为角度来思考服务的定义时,行为导致的动态协作关系需要我们更多地考虑协作双方的权利和义务,这就无意中使得服务行为模型的定义暗合由 Bertrand Meyer 提出的“契约式设计(Design by Contract)”思想。Meyer 认为:

“契约的主要目的是:尽可能准确地规定软件元素彼此通讯时的彼此义务和权利,从而有效组织通信,进而帮助我们构造出更好的软件。”

Meyer 之所以把商业中的契约概念引入到软件设计中,目的是对消费者和提供者两方的协作进行约束。作为请求方的消费者,需要定义发起请求的必要条件,这就是服务行为的输入参数,在契约式设计中被称之为前置条件(pre-condition)。作为响应方的提供者,需要阐明服务必须对消费者做出保证的条件,在契约式设计中被称之为后置条件(post-condition)

前置条件和后置条件是对称的,因此前置条件是消费者的义务,同时就是提供者的权利;后置条件是提供者的义务,同时就是消费者的权利。

以转账服务为例,从发起请求的角度来看,服务消费者为义务方,服务提供者为权利方。契约的前置条件为源账户、目标账户和转账金额。当服务消费者发起转账请求时,它的义务是提供前置条件包含的信息。如果消费者未提供这三个信息,又或者提供的信息是非法的,例如值为负数的转账金额,服务提供者就有权利拒绝请求。从响应请求的角度来看,权利与义务发生了颠倒,服务消费者成了权利方,服务提供者则为义务方。

一旦服务提供者响应了转账请求,其义务就是返回转账操作是否成功的结果,同时,这也是消费者应该享有的权利。如果消费者不知道转账结果,就会为这笔交易感到惴惴不安,甚而会因为缺乏足够的返回信息而发起额外的服务,例如再次发起转账请求,又或者要求查询交易历史记录。这就会导致消费者和提供者之间的契约关系遭到破坏。因此,遵循契约式设计的转账服务接口可以定义为:

public interface TransferService {
    TransferResult transfer(SourceAccount from, DestinationAccount to, Money amount);
}

在这个服务 API 定义中,利用 SourceAccount 与 DestinationAccount 来区分源账户和目标账户;通过 Money 类型来避免传递值为负数的转账金额,同时 Money 还封装了货币币种,保证了转账交易结果的准确性;TransferResult 封装了转账的结果,它与 boolean 类型的返回结果不同之处在于,它不仅可以标示结果为成功还是失败,还可以包含转账结果的提示消息。

契约式设计会谨慎地规定双方各自拥有的权利和义务。为了让服务能够更好地“招徕”顾客,会更多地考虑服务消费者,毕竟“顾客是上帝”嘛,需要在权利上适当向消费者倾斜,努力让消费者更加舒适地调用服务。此时,应遵循“最小知识法则”,让消费者对提供者尽可能少地了解,从而消除掉用户一切不需要知道的复杂度。袁英杰在《该怎样设计 API?》一文中阐述了 API 定义哲学,即“当我们给用户提供 API 时,不应该由技术实现的难易程度来决定,而是站在用户的角度,消除掉一切不必要的复杂度,让用户可以最快速、最直接地达到他的目的。”从契约的角度讲,就是要将服务消费者承担的义务降到最少,让服务消费者只需要描述它真正需要描述的信息。

仍然以转账服务为例。服务消费者提供源账户、目标账户与转账金额是合理的,因为这些信息只有服务消费者才知道。如果我们定义的转账服务行为还要求服务消费者提供转账时间,就会造成过分的无理要求。且不说转账时间的准确性,倘若该信息同时被服务消费者与服务提供者拥有,基于“最小知识法则”,就应该由服务提供者来承担。如果一个服务提供者总是过分地对服务消费者提出更多要求,就会加重消费者额外的负担,让消费者变得不愿意“消费”该服务。

当服务行为设计的驱动者转向服务消费者时,设计思路就可以按照“意图导向编程(Programming by Intention)”的设计轨迹。Alan Shalloway 在《敏捷技能修炼》一书中阐释了何谓意图导向编程,即:

“先假设当前这个对象中,已经有了一个理想方法,它可以准确无误地完成你想做的事情,而不是直接盯着每一点要求来编写代码。先问问自己:‘假如这个理想的方法已经存在,它应该具有什么样的输入参数,返回什么值?还有,对我来说,什么样的名字最符合它的意义?’”

在定义服务行为模型时,也可以尝试采用这种方式进行思考:

  • 假如服务行为已经存在,它的前置条件与后置条件应该是什么?
  • 服务消费者应该承担的最小义务包括哪些?
  • 而它又应该享有什么样的权利?
  • 该用什么样的名字才能表达服务行为的价值?

在进行这样的意图导向思考时,应结合具体的业务场景。业务场景不同,需要定义的服务契约也不相同。例如都是投保行为,如果是企业购买团体保险,服务契约的前置条件需要包含保额、投保人、被保人、等级保益、受益人和销售渠道等,但如果是货物运输的运输保险,服务契约的前置条件则包括保额、货物名称、运输路线、运输工具和开航日期等。服务契约的后置条件虽然都是创建一个投保单,但是投保单的内容却存在非常大的差异。

在识别业务场景时,需要保证业务场景的合理粒度。划分场景粒度时可以参考如下两个特征:

  • 为消费者提供了业务价值:服务对消费者有价值,就是能解决消费者的问题,达成消费者的目标,例如下订单对于买家就是有价值的,而验证订单有效性对于买家就没有价值,应作为下订单内部的子功能。
  • 具有完整时序的线上操作过程,不能中断:当消费者发起对服务的请求时,执行过程具有明显的时序性,如果中间存在时序上的中断,就应该划分为两个不同的场景。例如投保服务,在录入投保信息后,会发起一个工作流,由核保人对录入的投保信息进行审核。由于核保人是一个人工处理的过程,就会导致录入投保信息到审核之间存在一个明显的时序中断。这时就应该分为两个不同的场景,对应两个不同的服务——投保服务与核保服务。

在确定服务契约时,还需要考虑作为前置条件和后置条件的输入参数与返回值应该定义成什么样的类型?我们需要考虑两方面的因素:

  • 参数与返回值的序列化:通常需要定义 POJO 对象,通过 getter 和 setter 来表示属性。正因为此,一般不建议将服务参数与返回值定义为接口,因为在网络通信的背景下,对数据模型的抽象并无意义。
  • 是否需要解除客户端对服务接口定义类型的依赖:RPC 方式不同于 REST 服务,服务消费者在调用远程服务时,需要在客户端获得远程服务的一个引用(相当于远程代理);如果服务参数与返回值为自定义类型,就需要为客户端提供定义了这些类型的 JAR 包。一种方式是将服务契约的定义与实现分开,使得服务契约的 JAR 包可以单独部署在客户端。另一种方式则是采用泛化调用,即直接使用 Map 对象而非 POJO 对象来装配对象;但是,采用泛化调用会牺牲自定义类型的封装性,无法表达业务含义。

与服务资源模型一样,在确定服务行为时,需要先确定该行为的类别究竟是命令(Command)还是查询(Query),并遵循 Meyer 提出的命令查询分离(Command-Query Separation,CQS)原则。命令操作和查询操作具有迥然不同的特质。命令操作通常会带来副作用,因此它往往意味着状态的修改,可能不符合操作的安全性与幂等性。纯粹的查询操作则不然,因为它天然就是安全和幂等的。一旦将命令与查询分离,就意味着服务行为会按照各自的契约行事,只要明确了契约的前置条件,在调用时就可以推测程序的状态。如果二者合二为一,就无法分辨状态的变更究竟是因为调用端调用还是服务内部自身引起,从而带来难以预知的结果。

远程服务的行为更需要遵循命令查询分离原则。因为远程服务采用的网络通信是不稳定的,这就增加了对结果预判的复杂性。服务行为的失败或错误并不一定意味着服务行为执行失败,有可能是返回响应消息时的通信故障。由于调用者不知道服务失败的具体原因,为了保证服务行为执行成功,就需要发起对服务的重试(Retry)。为避免重试带来不可预知的后果,通常要求服务行为是幂等的。倘若遵循命令查询分离原则来设计服务行为,就只需要针对命令操作考虑幂等设计即可。

还是以转账服务为例。执行转账服务会扣除源账户的余额,并在目标账户中增加相同的金额,同时还会生成一笔新的交易记录。显然,转账服务属于命令操作,它会创建交易记录,并修改账户的余额值。当服务消费者发起转账服务请求时,可能因为网络通信故障导致转账失败。如果不考虑业务规则的约束导致的失败,则失败可能因为:

  • 请求消息发送到服务端时发生通信故障,导致服务端无法收到服务请求。此时的转账交易不成功。
  • 请求消息成功发送到服务端并正确地完成了转账交易,但在服务端返回应答消息时出现了传输失败。此时的转账交易成功,但消费者会认为转账失败,会再次发起转账请求。

为了避免第二种场景的重复转账,需要将转账服务设计为幂等,方法是利用唯一标识的业务 ID 对请求进行判断。修改前面定义的转账服务接口:

public interface TransferService {
    TransferResult transfer(Source sourceAccount, Destination destAccount, Money amount, String transactionId);
}

transactionId 作为交易 ID 可以唯一标识每一次发起的转账交易。当服务提供者接收到转账请求时,首先通过 transactionId 到交易明细表中查询该 ID 是否已经存在,若存在,则认为该交易已经执行,就可以忽略本次请求,实现服务的幂等性。显然,服务的幂等性在一定程度上影响了服务 API 的设计。

在定义服务的 API 时,还应该考虑该服务行为究竟是同步操作还是异步操作。所幸,多数框架都能够做到无需修改 API 的定义即可透明地支持同步与异步操作。例如,Dubbo 框架就允许我们在服务消费者的配置中,将 async 属性设置为 true,自动实现消费者对服务发起的异步调用,接口并不需要做任何调整。比如,我们定义了查询最近餐厅的服务提供者:

public interface RestaurantService {
    Restaurant requireNearestRestaurant(Location location);
}

该服务的实现为:

public class SphericalRestaurentService implements RestaurantService {
    private static long RADIUS = 3000;
    @Override
    public Restaurant requireNeareastRestaurant(Location location) {
        List<Restaurant> restaurants = requireAllRestaurants(location, RADIUS);
        Collections.sort(restaurants, new LocationComparator());
        return restaurants.get(0);        
    }

    private double distance(Location start, Location end) {
        double radiansOfStartLongitude = radians(start.getLongitude());
        double radiansOfStartDimension = radians(start.getDimension());
        double radiansOfEndLongitude = radians(end.getLongitude());
        double raidansOfEndDimension = radians(end.getDimension());

        return Math.acos(
            Math.sin(radiansOfStartLongitude) * Math.sin(radiansOfEndLongitude) + 
            Math.cos(radiansOfStartLongitude) * Math.cos(radiansOfEndLongitude) * Math.cos(raidansOfEndDimension - radiansOfStartDimension)
        );
    }
}

很显然,该方法的实现为同步操作,但在服务消费者的配置文件中,却可以将其配置为异步操作:

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"

       xmlns="http://www.springframework.org/schema/beans"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

       http://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <dubbo:application name="smart-restaurants"/>

    <dubbo:registry group="dddpractice" address="zookeeper://127.0.0.1:2181"/>

    <dubbo:reference id="restaurantService" interface="xyz.zhangyi.dddpractice.smartrestaurants.api.RestaurantService">
        <dubbo:method name="requireNeareastRestaurant" async="true" />
    </dubbo:reference>

</beans>

一旦配置为异步操作时,服务消费者就可以通过 RpcContext 获得 Future<Restaurant>

RestaurantService service = (RestaurantService)context.getBean("restaurantService");
Future<Restaurant> rFuture = RpcContext.getContext().getFuture();
Restaurant restaurant = rFuture.get();

服务资源模型与服务行为模型的区分在于建模的驱动力。服务资源模型是名词驱动的,首先得到的是资源;服务行为模型是动词驱动的,首先得到的是服务行为。仍然采用 Thomas Erl 建议的服务契约模型对 Restaurant 服务进行建模,二者在图示上的区别如下所示:

img

显然,服务资源模型提供的服务行为无法做到服务行为模型那样通过方法名直接表达“获得最近餐馆”的业务语义,但它却通过 URI 保证了接口的统一性,满足了服务版本的可扩展性。

服务契约的定义与设计还需要遵循远程服务的设计原则,例如考虑服务的版本、兼容性、序列化、异常等。同时,我们也可以结合 SOA 与微服务的服务设计原则来指导服务设计,例如保证服务契约的标准化,满足服务松耦合原则、抽象原则、可重用原则、自治原则等。这些内容实际上属于服务设计的范畴,在领域驱动设计中,可以结合限界上下文和上下文映射在战略设计阶段开展。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1437999.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ STL: vector使用及源码剖析

vector使用 vector定义 语句 作用 vector<int> a(n); 指定容器大小为n vector<int> a(n, x); 指定容器大小为n&#xff0c;并初始化所有元素为x vector<vector<int>> a(m, vector<int>(n)); m行n列的二维数组&#xff0c;可以直接…

游戏开发-会飞的小鸟(已完结,附源码)

游戏开发-会飞的小鸟&#xff08;已完结&#xff0c;附源码&#xff09; 你将学到的课程链接详细介绍 你将学到的 掌握Java编程的基本技能开发出自己的“会飞的小鸟”游戏对面向对象编程有深刻的理解学会运用常见算法和数据结构解决问题能够独立调试和优化自己的代码 课程链接…

(2)(2.14) SPL Satellite Telemetry

文章目录 前言 1 本地 Wi-Fi&#xff08;费用&#xff1a;30 美元以上&#xff0c;范围&#xff1a;室内&#xff09; 2 蜂窝电话&#xff08;费用&#xff1a;100 美元以上&#xff0c;范围&#xff1a;蜂窝电话覆盖区域&#xff09; 3 手机卫星&#xff08;费用&#xff…

Android.mk 语法详解

一.Android.mk简介 Android.mk 是Android 提供的一种makefile 文件,注意用来编译生成&#xff08;exe&#xff0c;so&#xff0c;a&#xff0c;jar&#xff0c;apk&#xff09;等文件。 二.Android.mk编写 分析一个最简单的Android.mk LOCAL_PATH : $(call my-dir) //定义了…

[Python] opencv - 什么是直方图?如何绘制图像的直方图?如何对直方图进行均匀化处理?

什么是直方图&#xff1f; 直方图是一种统计图&#xff0c;用于展示数据的分布情况。它将数据按照一定的区间或者组进行划分&#xff0c;然后计算在每个区间或组内的数据频数或频率&#xff08;即数据出现的次数或占比&#xff09;&#xff0c;然后用矩形或者柱形图的形式将这…

『运维备忘录』之 TAR 命令详解

运维人员不仅要熟悉操作系统、服务器、网络等只是&#xff0c;甚至对于开发相关的也要有所了解。很多运维工作者可能一时半会记不住那么多命令、代码、方法、原理或者用法等等。这里我将结合自身工作&#xff0c;持续给大家更新运维工作所需要接触到的知识点&#xff0c;希望大…

绕过安全狗

本节我们想要绕过的安全狗版本为v4.023957 &#xff0c;它是网站安全狗的Apache版。 首先搭建环境。渗透环境选用DVWA漏洞集成环境&#xff0c;下载地址 为http://www.dvwa.co.uk/ 。DVWA是一款集成的渗透测试演练环境&#xff0c;当刚刚入门 并且找不到合适的靶机时&#xff…

c++ 类,第一篇章,初始化列表 (详细)

快过年啦&#xff01;雀儿在这里提前祝大家新年快乐&#xff01; 初始化&#xff0c;就是在一个变量在创建的时候被赋值&#xff0c;一共有四种可能 //X是类名&#xff0c;a是对象名&#xff0c;v是初始值 X a{v}; X a1{v}; X a2v; X a3(v);一共四种写法&#xff0c;如上。 第…

正点原子--STM32基本定时器学习笔记(1)

目录 1. 定时器概述 1.1 软件定时原理 1.2 定时器定时原理 1.3 定时器分类 1.4 定时器特性表 1.5 基本、通用、高级定时器的功能整体区别 2. 基本定时器简介 3. 基本定时器框图 时钟树分析 这部分是笔者对基本定时器的理论知识进行学习与总结&#xff01;主要记录学习…

Leaf——美团点评分布式ID生成系统

0.普通算法生成id的缺点 1.Leaf-segment数据库方案 第一种Leaf-segment方案&#xff0c;在使用数据库的方案上&#xff0c;做了如下改变&#xff1a; - 原方案每次获取ID都得读写一次数据库&#xff0c;造成数据库压力大。改为利用proxy server批量获取&#xff0c;每次获取一…

基于spring cloud alibaba的微服务平台架构规划

平台基础能力规划&#xff08;继续完善更新…&#xff09; 一、统一网关服务&#xff08;独立服务&#xff09; 二、统一登录鉴权系统管理&#xff08;独立服务&#xff09; 1.统一登录 2.统一鉴权 3.身份管理 用户管理 角色管理 业务系统和菜单管理 部门管理 岗位管理 字典管…

一步步建立一个C#项目(连续读取S7-1200PLC数据)

这篇博客作为C#的基础系列,和大家分享如何一步步建立一个C#项目完成对S7-1200PLC数据的连续读取。首先创建一个窗体应用。 1、窗体应用 2、配置存储位置 3、选择框架 拖拽一个Button,可以选择视图菜单---工具箱 4、工具箱 拖拽Lable控件和TextBook控件 5、拖拽控件 接下来…

算法day12

算法day12 二叉树理论基础114 二叉树的前序遍历145 二叉树的后序遍历94 二叉树的中序遍历迭代法 二叉树理论基础 直接看代码随想录就完事了&#xff0c;之前考研也学过&#xff0c;大概都能理解 我这里就说说代码层面的。 二叉树的存储&#xff1a; 1、链式存储&#xff1a;这…

简单实验 spring cloud gateWay 路由实验 实验

1.概要 1.1 说明 微服务统一网关实验&#xff0c;这里简单实验一下路由的功能 1.2 实验步骤&#xff0c;使用下面这个工程作为基础工程添加了一个gateWay做如下使用 简单实践 spring cloud nacos nacos-server-2.3.0-CSDN博客 2 代码 2.1 工程文件 <?xml version&quo…

【Linux取经路】探寻shell的实现原理

文章目录 一、打印命令行提示符二、读取键盘输入的指令三、指令切割四、普通命令的执行五、内建指令执行5.1 cd指令5.2 export指令5.3 echo指令 六、结语 一、打印命令行提示符 const char* getusername() // 获取用户名 {return getenv("USER"); }const char* geth…

生成式学习,特别是生成对抗网络(GANs),存在哪些优点和缺点,在使用时需要注意哪些注意事项?

生成对抗网络&#xff08;GANs&#xff09; 1. 生成对抗网络&#xff08;GANs&#xff09;的优点&#xff1a;2. 生成对抗网络&#xff08;GANs&#xff09;的缺点&#xff1a;3. 使用生成对抗网络&#xff08;GANs&#xff09;需要注意的问题 1. 生成对抗网络&#xff08;GANs…

学生管理系统(javaSE第一阶段项目)

JavaSE第一阶段项目_学生管理系统 1.项目介绍 此项目是JavaSE第一阶段的项目,主要完成学生对象在数组中的增删改查,大家可以在此项目中发挥自己的想象力做完善,添加其他功能等操作,但是重点仍然是咱们前9个模块的知识点2.项目展示 2.1.添加功能 2.2.查看功能 2.3.修改功能 2…

第二证券:大涨5%,这一指数爆发!

A股商场今日上午进一步上行&#xff0c;各大指数持续上涨&#xff0c;其间上证指数克复2800点。小市值股票体现更佳&#xff0c;中证1000指数上午大涨5%。 港股商场方面&#xff0c;今日上午一度大幅上涨&#xff0c;后涨幅有所回落。港股百胜我国今日上午体现抢眼&#xff0c…

jvm垃圾收集器之七种武器

1.回收算法 1.1 标记-清除算法(Mark-Sweep) 分为两个阶段&#xff0c;标注和清除。标记阶段标记出所有需要回收的对象&#xff0c;清除阶段回收被标记的对象所占用的空间。 该算法最大的问题是内存碎片化严重&#xff0c;后续可能发生大对象不能找到可利用空间的问题。 1.2 …