这是“领域驱动设计实践之路”系列的第四篇文章,从单体架构的弊端引入微服务,结合领域驱动的概念介绍了如何作微服务划分、设计领域模型并展现了总体的微服务化的系统架构设计。结合分层架构、六边形架构和整洁架构的思想,以实际使用场景为背景,展现了一个微服务的程序结构设计。数据库
单体结构示例(引用自互联网)编程
通常在业务发展的初期,整个应用涉及的功能需求较少,相对比较简单,单体架构的应用比较容易部署、测试,横向扩展也比较易实现。后端
然而,随着需求的不断增长, 愈来愈多的人加入开发团队,代码库也在飞速地膨胀。慢慢地,单体应用变得愈来愈臃肿,可维护性、灵活性逐渐下降,维护成本愈来愈高。设计模式
下面分析下单体架构应用存在的一些弊端:缓存
在项目初期应该有人能够作到对应用各个功能和实现了如指掌,随着业务需求的增多,各类业务流程错综复杂的揉在一块儿,整个系统变得庞大且复杂,以致于不多有开发者清楚每个功能和业务流程细节。安全
这样会使得新业务的需求评估或者异常问题定位会占用较多的时间,同时也蕴含着未知风险。更糟糕的是,这种极度的复杂性会造成一种恶性循环,每一次更改都会使得系统变得更复杂,更难懂。服务器
随着时间推移、需求变动和人员更迭,会逐渐造成应用程序的技术债务,而且越积越多。好比,团队必须长期使用一套相同的技术栈,很难采用新的框架和编程语言。有时候想引入一些新的工具时,就会使得项目中须要同时维护多套技术框架,好比同时维护Hibernate和Mybatis,使得成本变高。闭包
因为业务项目的全部功能模块都在一个应用上承担,包括核心和非核心模块,任何一个模块或者一个小细节的地方,由于设计不合理、代码质量差等缘由,都有可能形成应用实例的崩溃,从而使得业务全面受到影响。其根本缘由就是核心和非核心功能的代码都运行在同一个环境中。架构
多个相似的业务项目之间势必会存在相似的功能模块,若是都采用单体模式,就会带来重复功能建设和维护。并且,有时候还须要互相产生交互,打通单体系统之间的交互集成和协做的成本也须要额外付出。并发
再者,当项目大到必定程度,不一样的模块多是不一样的团队来维护,迭代联调的冲突,代码合并分支的冲突都会影响整个开发进度,从而使得业务响应速度愈来愈慢。
随着业务的发展,系统在出现业务处理瓶颈的时候,每每是因为某一个或几个功能模块负载较高形成的,但由于全部功能都打包在一块儿,在出现此类问题时,只能经过增长应用实例的方式分担负载,没办法对单独的几个功能模块进行服务能力的扩展,从而带来资源额外配置的消耗,成本较高。
针对以上痛点,近年来愈来愈多的互联网公司采用“微服务”架构构建自身的业务平台,而“微服务”也得到了愈来愈多技术人员的确定。
微服务实际上是SOA的一种演变后的形态,与SOA的方法和原则没有本质区别。SOA理念的核心价值是,松耦合的服务带来业务的复用,按照业务而不是技术的维度,结合高内聚、低耦合的原则来划分微服务,这正好与领域驱动设计所倡导的理念相契合。
从广义上讲,领域便是一个组织所作的事情以及其中包含的一切。每一个组织都有它本身的业务范围和作事方式,这个业务范围以及在其中所进行的活动即是领域。
DDD的子域和限界上下文的概念,能够很好地跟微服务架构中的服务进行匹配。并且,微服务架构中的自治化团队负责服务开发的概念,也与DDD中每一个领域模型都由一个独立团队负责开发的概念吻合。DDD倡导按业务领域来划分系统,微服务架构更强调从业务维度去作分治来应对系统复杂度,跳过业务架构设计出来的架构关注点不在业务响应上,可能就是个大泥球,在面临需求迭代或响应市场变化时就很痛苦。
DDD的核心诉求就是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构。而微服务追求业务层面的复用,设计出来的系统架构和业务一致;在技术架构上则系统模块之间充分解耦,能够自由地选择合适的技术架构,去中心化地治理技术和数据。
以电商的资源订购系统为例,典型业务用例场景包括查看资源,购买资源,查询用户已购资源等。
领域驱动为每个子域定义单独的领域模型,子域是领域的一部分,从业务的角度分析咱们须要覆盖的业务用例场景,以高内聚低耦合的思想,结合单一职责原则(SRP)和闭包原则(CCP),从业务领域的角度,划分出用户管理子域,资源管理子域,订单子域和支付子域共四个子域。
每一个子域对应一个限界上下文。限界上下文是一种概念上的边界,领域模型便工做于其中,每一个限界上下文都有本身的通用语言。限界上下文使得你在领域模型周围加上了一个显式的、清晰的边界。固然,限界上下文不只仅包含领域模型。当使用微服务架构时,每一个限界上下文对应一个微服务。
聚合是一个边界内领域对象的集群,能够将其视为一个单元,它由根实体和可能的一个或多个其余实体和值对象组成。聚合将领域模型分解为块,每一个聚合均可以做为一个单元进行处理。
聚合根是聚合中惟一能够由外部类引用的部分,客户端只能经过调用聚合根上的方法来更新聚合。
聚合表明了一致的边界,对于一个设计良好的聚合来讲,不管因为何种业务需求而发生改变,在单个事务中,聚合中的全部不变条件都是一致的。聚合的一个很重要的经验设计原则是,一个事务中只修改一个聚合实例。更新聚合时须要更新整个聚合而不是聚合中的一部分,不然容易产生一致性问题。
好比A和B同时在网上购买东西,使用同一张订单,同时意识到本身购买的东西超过预算,此时A减小点心数量,B减小面包数量,两个消费者并发执行事务,那么订单总额可能会低于最低订单限额要求,但对于一个消费者来讲是知足最低限额要求的。因此应该站在聚合根的角度执行更新操做,这会强制执行一致性业务规则。
另外,咱们不该该设计过大的聚合,处理大聚合构成的"巨无霸"对象时,容易出现不一样用例同时须要修改其中的某个部分,由于聚合设计时考虑的一致性约束是对整个聚合产生做用的,因此对聚合的修改会形成对聚合总体的变动,若是采用乐观并发,这样就容易产生某些用例会被拒绝的场景,并且还会影响系统的性能和可伸缩性。
使用大聚合时,每每为了完成一项基本操做,须要将成百上千个对象一同加载到内存中,形成资源的浪费。因此应尽可能采用小聚合,一方面使用根实体来表示聚合,其中只包含最小数量的属性或值类型属性,这里的最小数量表示所需的最小属性集合,很少也很多。必须与其余属性保持一致的属性是所需的属性。
在聚合中,若是你认为有些被包含部分应该建模成一个实体,此时,思考下这个部分是否会随着时间而改变,或者该部分是否能被所有替换。若是能够所有替换,那么能够建模成值对象,而非实体。由于值对象自己是不可变的,只能进行所有替换,使用起来更安全,因此,通常状况下优先使用值对象。不少状况下,许多建模成实体的概念均可以重构成值对象。小聚合还有助于事务的成功执行,即它能够减小事务提交冲突,这样不只能够提高系统的性能和可伸缩性,另外系统的可用性也获得了加强。
另外聚合直接的引用经过惟一标识实现,而不是经过对象引用,这样不只减小聚合的使用空间,更重要的是能够实现聚合直接的松耦合。若是聚合是另外一个服务的一部分,则不会出现跨服务的对象引用问题,固然在聚合内部对象之间是能够相互引用的。
上述关于聚合的主要使用原则总结起来能够概括为如下几点:
固然在实际使用的过程当中,好比某一个业务用例须要获取到聚合中的某个领域对象,但该领域对象的获取路径较繁琐,为了兼容该特殊场景,能够将聚合中的属性(实体或值对象)直接返回给应用层,使得应用层直接操做该领域对象。
咱们常常会遇到在一个聚合上执行命令方法时,还须要在其余聚合上执行额外的业务规则,尽可能使用最终一致性,由于最终一致性能够按聚合维度分步骤处理各个环节,从而提高系统的吞吐量。对于一个业务用例,若是应该由执行该用例的用户来保证数据的一致性,那么能够考虑使用事务一致性,固然此时依然须要遵循其余聚合原则。若是须要其余用户或者系统来保证数据一致性,那么使用最终一致性。实际上,最终一致性能够支持绝大部分的业务场景。
基于上面对电商的资源订购系统业务子域的划分,设计出资源聚合,订单聚合,支付聚合和用户聚合,资源聚合与订单聚合之间经过资源ID进行关联,订单聚合与支付聚合之间经过订单ID和用户ID进行关联,支付聚合和用户聚合之间经过用户ID进行关联。资源聚合根中包含多个资源包值对象,一个资源包值对象又包含多个预览图值对象。固然在实际开发的过程当中,根据实际状况聚合根中也能够包含实体对象。每一个聚合对应一个微服务,对于特别复杂的系统,一个子域可能包含多个聚合,也就包含多个微服务。
基于上面对电商的资源订购系统子域的分析,服务器后台使用用户服务,资源服务,订单服务和支付服务四个微服务实现。上图中的API Gateway也是一种服务,同时能够当作是DDD中的应用层,相似面向对象设计中的外观(Facade)模式。
做为整个后端架构的统一门面,封装了应用程序内部架构,负责业务用例的任务协调,每一个用例对应了一个服务方法,调用多个微服务并将聚合结果返回给客户端。它还可能有其余职责,好比身份验证,访问受权,缓存,速率限制等。以查询已购资源为例,API Gateway须要查询订单服务获取当前用户已购的资源ID列表,而后根据资源ID列表查询资源服务获取已购资源的详细信息,最终将聚合结果返回给客户端。
固然在实际应用的过程当中,咱们也能够根据API请求的复杂度,从业务角度,将API Gateway划分为多个不一样的服务,防止又回归到API Gateway的单体瓶颈。
另外,有时候从业务领域角度划分出来的某些子域比较小,从资源利用率的角度,单独放到一个微服务中有点单薄。这个时候咱们能够打破一个限界上下文对应一个微服务的理念,将多个子域合并到同一个微服务中,由微服务本身的应用层实现多子域任务的协调。
因此,在咱们的系统架构中可能会出现微服务级别的小应用层和API Gateway级别的大应用层使用场景,理论当然是理论,仍是须要结合实际状况灵活应用。
分层架构图(引用自互联网)
六边形架构图(引用自互联网)
整洁架构图(引用自互联网)
上面整洁架构图中的同心圆分别表明了软件系统中的不一样层次,一般越靠近中心,其所在的软件层次就越高。
整洁架构的依赖关系规则告诉咱们,源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略。换句话说,任何属于内层圆中的代码都不该该牵涉外层圆中的代码,尤为是内层圆中的代码不该该引用外层圆中代码所声明的名字,包括函数、类、变量以及一切其余有命名的软件实体。一样,外层圆使用的数据格式也不该该被内层圆中的代码所使用,尤为是当数据格式由外层圆的框架所生成时。
总之,不该该让外层圆中发生的任何变动影响到内层圆的代码。业务实体这一层封装的是整个业务领域中最通用、最高层的业务逻辑,它们应该属于系统中最不容易受外界影响而变更的部分,也就是说通常状况下咱们的核心领域模型部分是比较稳定的,不该该由于外层的基础设施好比数据存储技术选型的变化,或者UI展现方式等的变化受影响,从而须要作相应的改动。
在以往的项目经验中,大多数同窗习惯也比较熟悉分层架构,通常包括展现层、应用层,领域层和基础设施层。六边形架构的一个重要好处是它将业务逻辑与适配器中包含的表示层和数据访问层的逻辑分离开来,业务逻辑不依赖于表示层逻辑或数据访问层逻辑,因为这种分离,单独测试业务逻辑要容易得多。
另外一个好处是,能够经过多个适配器调用业务逻辑,每一个适配器实现特定的API或用户界面。业务逻辑还能够调用多个适配器,每一个适配器调用不一样的外部系统。因此六边形架构是描述微服务架构中每一个服务的架构的好方法。
根据咱们具体的实践经验,好比在咱们平时的项目中最多见的就是MySQL和Redis存储,并且也不多改变为其余存储结构。这里将分层架构和六边形架构进行思想融合,目的是一方面但愿咱们的微服务设计结构更优美,另外一方面但愿在已有编程习惯的基础上,更容易接受新的整洁架构思想。
咱们项目中微服务的实现结合分层架构,六边形架构和整洁架构的思想,以实际使用场景为背景,采用的应用程序结构图以下。
从上图能够看到,咱们一个应用总共包含应用层application,领域层domain和基础设施层infrastructure。领域服务的facade接口须要暴露给其余三方系统,因此单独封装为一个模块。由于咱们通常习惯于分层架构模式构建系统,因此按照分层架构给各层命名。
站在六边形架构的角度,应用层application等同于入站适配器,基础设施层infrastructure等同于出站适配器,因此实际上应用层和基础设施层同属外层,能够认为在同一层。
facade模块实际上是从领域层domain剥离出来的,站在整洁架构的角度,领域层就是内核业务实体,这里封装的是整个业务领域中最通用、最高层的业务逻辑,通常状况下核心领域模型部分是比较稳定的,不受外界影响而变更。facade是微服务暴露给外界的领域服务能力,通常状况下接口的设定应符合当前领域服务的边界界定,因此facade模块属于内核领域层。
facade接口的实如今应用层application的impl部分,符合整洁架构外层依赖内层的思想,对于impl输入端口和入站适配器,能够采用不一样的协议和技术框架实现,好比dubbo或HSF等。下面对各个模块的构成进行逐一解释。
对象的建立自己是一个主要操做,但被建立的对象并不适合承担复杂的装配操做。将这些职责混在一块儿可能会产生难以理解的拙劣设计。让客户直接负责建立对象又会使客户的设计陷入混乱,而且破坏装配对象的封装,并且致使客户与被建立对象的实现之间产生过于紧密的耦合。
复杂对象的建立是领域层的职责,但这项任务并不属于那些用于表示模型的对象。因此通常使用一个单独的工厂类或者在领域服务中提供一个构造领域对象的接口来负责领域对象的建立。
这里,咱们选择给领域服务增长一个领域对象建立接口来承担工厂的角色。
/** * description: 资源领域服务 * * @author Gao Ju * @date 2020/7/27 */ public class ResourceServiceImpl implements ResourceService { /** * 建立资源聚合模型 * * @param resourceCreateCommand 建立资源命令 * @return */ @Override public ResourceModel createResourceModel(ResourceCreateCommand resourceCreateCommand) { ResourceModel resourceModel = new ResourceModel(); Long resId = SequenceUtil.generateUuid(); resourceModel.setResId(resId); resourceModel.setName(resourceCreateCommand .getName()); resourceModel.setAuthor(resourceCreateCommand .getAuthor()); List<PackageItem> packageItemList = new ArrayList<>(); ... resourceModel.setPackageItemList(packageItemList); return resourceModel; } }
一般将聚合实例存放在资源库中,以后再经过该资源库来获取相同的实例。
若是修改了某个聚合,那么这种改变将被资源库持久化,若是从资源库中移除了某个实例,则将没法从资源库中从新获取该实例。
资源库是针对聚合维度建立的,聚合类型与资源库存在一对一的关系。
简单来讲,资源库是对聚合的CRUD操做的封装。资源库内部采用哪一种存储设施MySQL,MongoDB或者Redis等,对领域层来讲实际上是不感知的。
资源repository构成图
在咱们的项目中采用MySQL做为资源repository的持久化存储,上图中每一个DO对应一个数据库表,固然你也能够采用其余存储结构或设计为其余表结构,具体的处理流程均由repository进行封装,对领域服务来讲只感知Resource聚合维度的CRUD操做,示例代码以下。
/** * description: 资源仓储 * * @author Gao Ju * @date 2020/08/23 */ @Repository("resourceRepository") public class ResourceRepositoryImpl implements ResourceRepository { /** * 资源Mapper */ @Resource private ResourceMapper resourceMapper; /** * 资源包Mapper */ @Resource private PackageMapper packageMapper; /** * 资源包预览图Mapper */ @Resource private PackagePreviewMapper packagePreviewMapper; /** * 建立订单信息 * * @param resourceModel 资源聚合模型 * @return */ @Override public void add(ResourceModel resourceModel) { ResourceDO resourceDO = new ResourceDO(); resourceDO.setName(resourceModel.getName()); resourceDO.setAuthor(resourceModel.getAuthor()); List<PackageDO> packageDOList = new ArrayList<>(); List<PackagePreviewDO> packagePreviewDOList = new ArrayList<>(); for (PackageItem packageItem : resourceModel.getPackageItemList()) { PackageDO packageDO = new PackageDO(); packageDO.setResId(resourceModel.getResId()); Long packageId = SequenceUtil.generateUuid(); packageDO.setPackageId(packageId); for (PreviewItem previewItem: packageItem.getPreviewItemList()) { PackagePreviewDO packagePreviewDO = new PackagePreviewDO(); ... packagePreviewDOList.add(packagePreviewDO); } packageDOList.add(packageDO); } resourceMapper.insert(resourceDO); packageMapper.insertBatch(packageDOList); packagePreviewMapper.insertBatch(packagePreviewDOList); } }
你可能有疑问,按照整洁架构的思想,repository的接口定义在领域层,repository的实现应该定义在基础设施层,这样就符合外层依赖稳定度较高的内层了。
结合咱们实际开发过程,通常存储结构选定或者表结构设定后,通常不太容易作很大的调整,因此就按照习惯的分层结构使用,领域层直接依赖基础设施层实现,下降编码时带来的额外习惯上的成本。
领域驱动强调咱们应该建立充血领域模型,将数据和行为封装在一块儿,将领域模型与现实世界中的业务对象相映射。各种具有明确的职责划分,将领域逻辑分散到各个领域对象中。
领域中的服务表示一个无状态的操做,它用于实现特定于某个领域的任务。当某个操做不适合放在领域对象上时,最好的方式是使用领域服务。
简单总结领域服务自己所承载的职责,就是经过串联领域对象、资源库,生成并发布领域事件,执行事务控制等一系列领域内的对象的行为,为上层应用层提供交互的接口。
/** * description: 订单领域服务 * * @author Gao Ju * @date 2020/8/24 */ public class UserOrderServiceImpl implements UserOrderService { /** * 订单仓储 */ @Autowired private OrderRepository orderRepository; /** * 消息发布器 */ @Autowired private MessagePublisher messagePublisher; /** * 订单逻辑处理 * * @param userOrder 用户订单 */ @Override public void createOrder(UserOrder userOrder) { orderRepository.add(userOrder); OrderCreatedEvent orderCreatedEvent = new OrderCreatedEvent(); orderCreatedEvent.setUserId(userOrder.getUserId()); orderCreatedEvent.setOrderId(userOrder.getOrderId()); orderCreatedEvent.setPayPrice(userOrder.getPayPrice()); messagePublisher.send(orderCreatedEvent); } }
在实践的过程当中,为了简单方便,咱们仍然采用贫血领域模型,将领域对象自身行为和不属于领域对象的行为都放在领域服务中实现。
大部分场景领域服务返回聚合根或者简单类型,某些特殊场景也能够将聚合根中包含的实体或值对象返回给调用方。领域服务也能够同时操做多个领域对象,多个聚合,将其转换为另外的输出。
介于咱们实际的使用场景,领域比较简单,领域服务只操做一个领域的对象,只操做一个聚合,由应用服务来协调多个领域对象。
在领域驱动设计的上下文中,聚合在被建立时,或发生其余重大更改时发布领域事件,领域事件是聚合状态更改时所触发的。
领域事件命名时,通常选择动词的过去分词,由于状态改变时就表明当前事件已经发生,领域事件的每一个属性都是原始类型值或值对象,好比事件ID和建立时间等,事件ID也能够用来作幂等用。
从概念上讲,领域事件由聚合负责发布,聚合知道其状态什么时候发生变化,从而知道要发布的事件。
因为聚合不能使用依赖注入,须要经过方法参数的形式将消息发布器传递给聚合,但这将基础设施和业务逻辑交织在一块儿,有悖于咱们解耦设计的原则。
更好的方法是将事件发布放到领域服务中,由于服务可使用依赖注入来获取对消息发布器的引用,从而轻松发布事件。只要状态发生变化,聚合就会生成事件,聚合方法的返回值中包括一个事件列表,并将它们返回给领域服务。
Saga是一种在微服务架构中维护数据一致性的机制,Sage由一连串的本地事务组成,每个本地事务负责更新它所在服务的私有数据库,经过异步消息的方式来协调一系列本地事务,从而维护多个服务之间数据的最终一致性。Saga包括协同式和编排式,
咱们采用协同式来实现分布式事务,发布的领域事件以命令式消息的方式发送给Saga参与方。若是领域事件是自我发布自我消费,不依赖消息中间件实现,则可使用事件总线模式来进行管理。下面以购买资源的过程为例进行说明。
补偿过程
在Saga的概念中,
第1步叫可补偿性事务,由于后面的步骤可能会失败。
第3步叫关键性事务,由于它后面跟着不可能失败的步骤。第4步叫可重复性事务,由于其老是会成功。
/** * description: 领域事件基类 * * @author Gao Ju * @date 2020/7/27 */ public class BaseEvent { /** * 消息惟一ID */ private String messageId; /** * 事件类型 */ private Integer eventType; /** * 事件建立时间 */ private Date createTime; /** * 事件修改时间 */ private Date modifiedTime; } /** * description: 订单建立事件 * * @author Gao Ju * @date 2020/8/24 */ public class OrderCreatedEvent extends BaseEvent { /** * 用户ID */ private String userId; /** * 订单ID */ private String orderId; /** * 支付价格 */ private Integer payPrice; }
facade和domain属于同一层,某些提供给三方使用的类定义在facade,好比资源类型枚举CategoryEnum限制三方资源使用范围,而后domain依赖facade中enum定义。
另外,根据迪米特法则和告诉而非询问原则,客户端应该尽可能少地知道服务对象内部结构,经过调用服务对象的公共接口的方式来告诉服务对象所要执行的操做。
因此,咱们不该该把领域模型泄露到微服务以外,对外提供facade服务时,根据领域对象包装出一个数据传输对象DTO(Data Transfer Object),来实现和外部三方系统的交互,好比上图中的ResourceDTO。
应用层是业务逻辑的入口,由入站适配器调用。facade的实现,定时任务的执行和消息监听处理器都属于入站适配器,因此他们都位于应用层。
正常状况下一个微服务对应一个聚合,实践过程当中,某些场景下一个微服务能够包含多个聚合,应用层负责用例流的任务协调。领域服务依赖注入应用层,经过领域服务执行领域业务规则,应用层还会处理受权认证,缓存,DTO与领域对象之间的防腐层转换等非领域操做。
/** * description: 订单facade * * @author Gao Ju * @date 2020/8/24 */ public class UserOrderFacadeImpl implements UserOrderFacade { /** * 订单服务 */ @Resource private UserOrderService userOrderService; /** * 建立订单信息 * * @param orderPurchaseParam 订单交易参数 * @return */ @Override public FacadeResponse<UserOrderPurchase> createOrder(OrderPurchaseParam orderPurchaseParam ) { UserOrder userOrder = new UserOrder(); userOrder.setUserId(request.getUserId()); userOrder.setResId(request.getResId()); userOrder.setPayPrice(request.getPayAmount()); userOrder.setOrderStatus(OrderStatusEnum.Create.getCode()); userOrderService.handleOrder(userOrder); userOrderPurchase.setOrderId(userOrderDO.getId()); userOrderPurchase.setCreateTime(new Date()); return FacadeResponseFactory.getSuccessInstance(userOrderPurchase); } }
基础设施的职责是为应用程序的其余部分提供技术支持。与数据库的交互dao模块,与Redis缓存,本地缓存交互的cache模块,与参数中心,三方rpc服务的交互,消息框架消息发布者都封装在基础设施层。
另外,程序中用到的工具类util模块和异常类exception也统一封装在基础设施层。
从分层架构的角度,领域层能够依赖基础设施层实现与其余外设的交互。另外,不管从分层架构的上层application层仍是从六边形架构的角度的输入端口和适配器application,均可以依赖做为底层或处于同层的输出端口和适配器的infrastructure层,好比调用util或者exception模块。
其实,不管是面向服务架构SOA,微服务,领域驱动,仍是中台,其目的都是在说,咱们作架构设计的时候,应该从业务视角出发,对所涉及的业务领域,基于高内聚、低耦合的思想进行划分,最大限度且合理的实现业务重用。
这样不只方便提供专业且稳定的业务服务,更有利于业务的沉淀和可持续发展。业务之下是基于技术的系统实现,技术造就业务,业务引领技术,二者相辅相成,共同为社会进步作出贡献。
做者:Angel Gao