蚂蚁金服开源了分布式框架 SOFA,楼主写了一个 demo,体验了一下 SOFA 的功能,SOFA 完全兼容 SpringBoot(当然 Dubbo 也是可以兼容的)。
项目地址:Alipay ,该主页有 5 个项目,都是阿里开源的。
sofa-boot,
sofa-rpc,
sofa-bolt,
sofa-ark,
sofa-rpc-boot-projects。
https://github.com/alipay/
SOFA(Scalable Open Financial Architecture)是蚂蚁金服研发的金融级分布式中间件,很多人认为这个国产自主研发项目来势汹汹,说它定义了新的金融级分布式架构。
从内部研发开始,SOFA 至今已经发展了 10 年,伴随着在金融场景中的不断锤炼,SOFA 也从最初单一的组件发展成为如今的金融级分布式架构完整解决方案,目前它包含了构建金融级云原生架构所需的各个组件,具体有微服务研发框架、RPC 框架、服务注册中心、限流/熔断框架、分布式链路追踪、分布式高可用消息队列、分布式事务框架与分布式数据库代理层等。
今年 4 月份,SOFA 宣布开源,目前已经陆续在 GitHub 上释出了 20 多个相关组件。随着 SOFA 的开源,网上关于 SOFA 的资料已经有不少,我们针对 SOFA 大生态中一些当下读者最为关注的组件功能、特性,以及其强调的所谓“自主研发”等内容采访了 SOFA 开源负责人鲁直,希望带给读者更多关于 SOFA 的技术细节与理念。
项目地址:
GitHub:https://github.com/alipay
Gitee:https://gitee.com/alipay
作为⼀套云原生架构中间件,SOFA 意在帮助企业快速构建稳定的金融级云原生架构,以此为基础进行云原生应用的研发。
CNCF 对云原生应用定义了三大特征,分别是:
容器化封装:以容器为基础,简化云原生应用的维护;在容器中运行应用,并将容器作为应用部署的独立单元,实现高水平的资源隔离。
动态管理:通过集中式的编排调度系统动态地对容器进行管理和调度。
面向微服务
这也就是说云原生架构需要具备满足这些条件的能力,在 SOFA 中,这些能力以相应的组件和各组件整合起来的能力呈现。鲁直介绍,随着近 10 年的发展,SOFA 的内容变得越来越丰富,已经成为了⼀套完整的分布式架构解决方案。项目目前已经集成了诸多功能组件:
研发框架-SOFABoot
RPC 框架-SOFARPC
服务注册中心-SOFARegistry
Service Mesh-SOFAMesh
限流/熔断
消息队列
分布式链路追踪
数据访问代理
分布式事务
目前 SOFA 已经开源了其中的部分内容,包括 SOFABoot、SOFARPC、SOFATracer、SOFAMesh 等,鲁直表示开源工作将持续下去,未来还会陆续将剩余的部分逐步开源出来。
在 SOFA 的描述中,其定义了“新的金融级分布式架构”,对于这个“新”,鲁直表示,这主要体现在 SOFA 的无限伸缩能力、⼀致性、秒级容灾和极低成本这四个点上。
他解释,无限伸缩指的是 SOFA 的整套架构,包括数据库、应用、网络都能够做到无限地伸缩;⼀致性是 SOFA 非常有特色的⼀个点,在性能和⼀致性上可以达到⼀个平衡,实现金融交易业务的分布式事务⼀致性;秒级容灾指的是遇到机房断点、断网等场景时,可以秒级恢复,现在配备上蚂蚁金服自研的 OceanBase 数据库,可以达到更好的效果;极低成本指的是每笔交易花费的 IT 成本非常低,在面对各种大促的时候,SOFA 也可以做到非常低成本地按需进行扩容。
“SOFA 中有⼀个单元化的架构(LDC),将数据进行分片,⼀个逻辑数据中心只处理部分数据分片,当容量达到瓶颈的时候,直接扩充逻辑数据中心的数量即可,从而实现了无限伸缩的能力”,鲁直举例进行说明:“在单元化的架构上,SOFA 又演化出了弹性伸缩的能力,简单地说,就是在面临双 11、双 12 与新春红包等大促活动的时候,因为每次大促活动的业务特点并不⼀样,所以通过弹性伸缩的能力,SOFA 可以根据业务维度在不同的大促活动中将不同的服务弹到云上,从而大大降低大促带来的成本开销。”
选择开源,投入社区,SOFA 需要不断完善
鲁直坦言 Dubbo 是阿里集团开源的一款非常优秀的 RPC 框架,它高性能、具有良好的扩展性。而 SOFARPC 对协议、网络、路由与可扩展性等层面都进行了大量改造和优化工作,更加满足和蚂蚁金服类似的大规模金融级业务场景。
在蚂蚁金服内部,SOFARPC 在 SOFA 的生态下,有完善的微服务技术栈支持,包括微服务研发框架、RPC 框架、服务注册中心、分布式定时任务、限流/熔断框架、动态配置推送、分布式链路追踪、Metrics 监控度量等等。
他从以下几个方面对 Dubbo 与 SOFARPC 进行了比较:
性能方面,类似协议下使用的技术点都是差不多的,所以基本上可比性不高。
扩展性的对比,Dubbo 和 SOFARPC 二者都具有良好的扩展性。
在开源领域,SOFARPC 目前还处在一个起步阶段,开源生态还在建设当中,这与 Dubbo 多年来沉淀下来的积极的开源生态没法比较。随着开源计划的推进,SOFA 也积极在进行开源社区的运营工作,后续的版本将增加各个周边组件,完善微服务技术栈。
对于其它功能差异,有⼀些已经开源或者即将开源的功能点供参考:SOFARPC 协议上将支持 HTTP/2、gRPC,能力上如服务预热权重、自动故障降级、协商机制、CRC 数据校验等,结合 SOFABoot 可以实现 RPC 框架与业务的类隔离,防止类冲突等。另外 SOFARPC 在跨单元机房的路由,包括配合服务注册体系实现的对异地多活的支持也是非常有特色的。
SOFARPC 甚至 SOFA 整个项目,开源至今短短 4 个多月,其实成绩有目共睹,鲁直也期待在接下来的开源工作中,能有更多开发者加入,共同完善一个积极、稳定、功能全面的 SOFA 开源社区。
增强 Istio,Service Mesh 才是未来
Service Mesh 是最近比较火的一个主题,现在谈微服务架构很难不提到这个甚至被称为“下一代微服务架构”的技术。另外我们可以看到,近期关于微服务架构的相关话题,基本上是围绕云原生、Kubernetes 和以 Istio 为代表的 Service Mesh 解决方案展开。
Istio 主要包含 Envoy、Pilot 和 Mixer 三个部分,功能简要介绍如下:
Envoy:以 Sidecar 的形式代理应用的所有出入流量。
Pilot:管理和配置所有 Envoy 代理实例。
Mixer:提供策略控制和遥测数据收集。
SOFA 中有一个基于 Istio 的 SOFAMesh 组件,它在 Istio 的基础上做了不少改进。同样是做服务间的通信,既然已经有了 SOFARPC,那为什么 SOFA 中还需要一个 SOFAMesh 呢?鲁直直言“Service Mesh 才是未来”。
他表示,SOFAMesh 的定位和 SOFARPC 并不冲突,只是把原来 SOFARPC 中的部分能力下沉,包括服务发现、负载均衡、限流、熔断与部分的链路追踪等。而在 RPC 框架中,还需要保留例如序列化、协议封装等能力,才能够和 SOFAMesh 进行通信。“我们判断在未来 Kubernetes 的整个体系下,SOFAMesh 相比于 SOFARPC,会更加适合,针对于多语⾔ SOFAMesh 也会更加友好,并且 SOFAMesh 的 Sidecar 在 Kubernetes 下面可以独立升级,维护成本也会更低,这些对于基础设施团队会是非常大的吸引力。 ”
SOFAMesh 具体对 Istio 做了哪些改进,鲁直对此一一做出了介绍。
从这张 SOFAMesh 架构图中可以看到,SOFAMesh 自研了一个 Golang 版本的 Sidecar——SOFAMosn,它用来替代 Istio 体系中用 C++ 实现的 Envoy ,之所以选择用 Golang 实现,是因为 Golang 是云原生时代的首选语言,相比于 Envoy 使用的 C++,Golang 也更加容易上手,对于社区来说会更加友好。鲁直介绍,SOFAMosn 完全兼容 Envoy 的 API,可以通过 XDS 的 API 和 Istio 的 Pilot 进行通信。
除了替换 Envoy,SOFAMesh 中还把 Pilot 尝试对接到更多的服务注册中心上,鲁直介绍,目前已经增加了对 ZooKeeper 的支持,未来随着 SOFA 服务注册中心 SOFARegistry 的开源,也会增加对它的支持。
同时,在数据同步方面,未来会增加相应模块,以实现多个服务注册中心之间的数据交换。
另外,未来也会增加 Open Service Registry API,尝试提供标准化的服务注册功能。
与社区紧密结合,SOFA 融合开源生态
在鲁直看来,开源是一个非常大的世界,里面有非常多由优秀的程序员创造的优秀软件,包括 Istio、Spring Boot 等等。行业发展到这个阶段,完全地闭门造车是不现实的,或多或少,总得利用一些开源软件,或者是利用一些开源的基础库来做一些基础的事情,或者是在⼀些开源软件的基础上做出自己的增强。
同时他也具体解释了 SOFA 为何称得上自主研发:“SOFA 的自主研发包含了很多的内容,包括 SOFABoot 里面模块化的方案,给每一个模块生成一个独立的 Spring 上下文,解决运行时模块耦合的问题;轻量级的类隔离的方案,帮助用户更加轻松地去解决类隔离的问题;SOFAMesh 中的 Sidecar SOFAMosn 等等”,他也表示这些是已经开源的部分,此外还有大量目前尚未开源的自主研发内容,比如 SOFA 服务注册中心、分布式事务框架 DTX 等等。
总结起来,鲁直认为,我们应该抱着“即使是国产自主研发,也要拥抱开源世界”的态度来看待。
https://www.oschina.net/question/3820517_2286760
SOFA 是 Simple Object-oriented and Flexible Architecture 的简称,是阿里巴巴国际技术事业部自研的专门针对复杂业务场景的应用架构。
该架构主要从以下四个方面对应用进行治理:
扩展点设计:通过业务身份识别和扩展点,为应用提供扩展性,消除 if-else。
面向对象设计:通过遵循 SOLID 原则,采用 DDD 实践,将业务语义显性化。
分层设计:通过清晰的层次划分和组件定义,实现 Module 级别的 SRP。
规范设计:通过 Mvn Archetype 制定应用标准,固化架构。
前言
从业这么多年,接触过银行的应用,Apple 的应用,eBay 的应用和现在阿里的应用,虽然分属于不同的公司,使用了不同的架构,但有一个共同点就是都很复杂。导致复杂性的原因有很多,如果从架构的层面看,主要有两点,一个是架构设计过于复杂,层次太多能把人绕晕。另一个是根本就没架构,ServiceImpl 作为上帝类包揽一切,一杆捅到 DAO(就简单场景而言,这种Transaction Script也还凑合,至少实现上手都快),这种人为的复杂性导致系统越来越臃肿,越来越难维护,酱缸的老代码发出一阵阵恶臭,新来的同学,往往要捂着鼻子抠几天甚至几个月,才能理清系统和业务脉络,然后又一头扎进各种 bug fix,业务修补的恶性循环中,暗无天日!
CRM 作为阿里最老的应用系统,自然也逃不过这样的宿命。不甘如此的我们开始反思到底是什么造成了系统复杂性? 我们到底能不能通过架构来治理这种复杂性?基于这个出发点,我们团队开始了一段非常有意义的架构重构之旅(Redefine the Arch),期间我们参考了 SalesForce、TMF2.0、汇金和盒马的架构,从他们那里汲取了很多有价值的输入,再结合我们自己的思考最终形成了我们自己现在的基于扩展点 + 元数据 + CQRS + DDD 的应用架构。该架构的特点是可扩展性好,很好的贯彻了 OO 思想,有一套完整的规范标准,并采用了 CQRS 和领域建模技术,在很大程度上可以降低应用的复杂度。本文主要阐述了我们的思考过程和架构实现,希望能对在路上的你有所帮助。
复杂性来自哪里
经过我们分析、讨论,发现造成现在系统异常复杂的罪魁祸首主要来自以下四个方面:
可扩展性差
对于只有一个业务的简单场景,并不需要扩展,问题也不突出,这也是为什么这个点经常被忽略的原因,因为我们大部分的系统都是从单一业务开始的。但是随着支持的业务越来越多,代码里面开始出现大量的 if-else 逻辑,这个时候代码开始有坏味道,没闻到的同学就这么继续往上堆,闻到的同学会重构一下,但因为系统没有统一的可扩展架构,重构的技法也各不相同,这种代码的不一致性也是一种理解上的复杂度。
久而久之,系统就变得复杂难维护。像我们 CRM 应用,有 N 个业务方,每个业务方又有 N 个租户,如果都要用 if-else 判断业务差异,那简直就是惨绝人寰。其实这种扩展点(Extension Point),或者叫插件(Plug-in)的设计在架构设计中是非常普遍的。比较成功的案例有 Eclipse 的 plug-in 机制,集团的 TMF2.0 架构。还有一个扩展性需求就是字段扩展,这一点对 SaaS 应用尤为重要,因为有很多客户定制化需求,但是我们很多系统也没有统一的字段扩展方案。
面向过程
是的,不管你承认与否,很多时候,我们都是操着面向对象的语言干着面向过程的勾当。面向对象不仅是一个语言,更是一种思维方式。在我们追逐云计算、深度学习、区块链这些技术热点的时候,静下心来问问自己我们是不是真的掌握了 OOD;在我们强调工程师要具备业务 Sense,产品 Sense,数据 Sense,算法 Sense,XXSense 的时候,是不是忽略了对工程能力的要求。据我观察大部分工程师(包括我自己)的 OO 能力还远没有达到精通的程度,这种 OO 思想的缺乏主要体现在两个方面,一个是很多同学不了解 SOLID 原则,不懂设计模式,不会画 UML 图,或者只是知道,但从来不会运用到实践中;另一个是不会进行领域建模,关于领域建模争论已经很多了,我的观点是 DDD 很好,但不是银弹,用和不用取决于场景。但不管怎样,请你抛开偏见,好好的研读一下 Eric Evans 的《领域驱动设计》,如果有认知升级的感悟,恭喜你,你进阶了。我个人认为 DDD 最大的好处是将业务语义显现化,把原先晦涩难懂的业务算法逻辑,通过领域对象(Domain Object),统一语言(Ubiquitous Language)将领域概念清晰的显性化表达出来。相信我,这种表达带来的代码可读性的提升,会让接手你代码的人对你心怀感恩的。借用 Abelson 的一句话是:
Programs must be written for people to read, and only incidentally for machines to execute
所以强烈谴责那些不顾他人感受的编码行为。
分层不合理
俗话说的好,All problems in computer science can be solved by another level of indirection(计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决),怎样? 是不是感受到间接层的强大了。分层最大的好处就是分离关注点,让每一层只解决该层关注的问题,从而将复杂的问题简化,起到分而治之的作用。
我们平时看到的 MVC,pipeline,以及各种 valve 的模式,都是这个道理。好吧,那是不是层次越多越好,越灵活呢。当然不是,就像我开篇说的,过多的层次不仅不能带来好处,反而会增加系统的复杂性和降低系统性能。
就拿 ISO 的网络七层协议来说,你这个七层分的很清楚,很好,但也很繁琐,四层就够了嘛。再比如我前面提到的过度设计的例子,如果没记错的话应该是 Apple 的 Directory Service 应用,整个系统有 7 层之多,把什么 validator,assembler 都当成一个层次来对待,能不复杂么。所以分层太多和没有分层都会导致系统复杂度的上升,因此我们的原则是不可以没有分层,但是只分有必要的层。
随心所欲
随心所欲是因为缺少规范和约束。这个规范非常非常非常的重要(重要事情说三遍),但也是最容易被无视的点,其结果就是架构的 consistency 被严重破坏,代码的可维护性将急剧下降,国将不国,架构将形同虚设。有同学会说不就是个 naming 的问题么,不就是个分包的问题么,不就是 2 个 module 还是 3 个 module 的问题么,只要功能能跑起来,这些问题都是小问题。是的,对于这些同学,我再丢给你一句名言 “Just because you can, doesn’t mean you should”。就拿 package 来说,它不仅仅是一个放一堆类的地方,更是一种表达机制,当你将一些类放到 Package 中时,相当于告诉下一位看到你设计的开发人员要把这些类放在一起考虑。理想很丰满,现实很骨感,规范的执行是个大问题,最好能在架构层面进行约束,例如在我们架构中,扩展点必须以 ExtPt 结尾,扩展实现必须以 Ext 结尾,你不这么写就会给你抛异常。但是架构的约束毕竟有限,更多的还是要靠 Code Review,暂时没想到什么更好的办法。这种对架构约束的近似严苛 follow,确保了系统的 consistency,最终形成了一个规整的收纳箱(如下图所示),就像我和团队说的,我们在评估代码改动点时,应该可以像 Hash 查找一样,直接定位到对应的 module,对应的 package 里面对应的 class。而不是到 “一锅粥” 里去慢慢抠。
本章节最后,上一张我们老系统中比较典型的代码,也许你可以从中看到你自己应用的影子。
复杂性应对之道
知道了问题所在,接下来看下我们是如何一个个解决这些问题的。回头站在山顶再看这些解决方案时,每个都不足为奇,但当你还“身在此山中”的时候,这个拨开层层迷雾,看到山的全貌的过程,并不是想象的那么容易。庆幸的是我团队在艰难跋涉之后,终有所收获。
扩展点设计
扩展点的设计思想主要得益于 TMF2.0 的启发,其实这种设计思想也一直在用,但都是在局部的代码重构和优化,比如基于 Strategy Pattern 的扩展,但是一直没有找到一个很好的固化到框架中的方法。直到毗卢到团队分享,给了我们两个关键的提示,一个是业务身份识别,用他的话说,如果当时 TMF1.0 如果有身份识别的话,就没有 TMF2.0 什么事了;另一个是抽象的扩展点机制。
身份识别
业务身份识别在我们的应用中非常重要,因为我们的 CRM 系统要服务不同的业务方,而且每个业务方又有多个租户。比如中供销售,中供拍档,中供商家都是不同的业务方,而拍档下的每个公司,中供商家下的每个供应商又是不同的租户。所以传统的基于多租户(TenantId)的业务身份识别还不能满足我们的要求,于是在此基础上我们又引入了业务码(BizCode)来标识业务。所以我们的业务身份实际上是(BizCode,TenantId)二元组。在每一个业务身份下面,又可以有多个扩展点(ExtensionPoint),所以一个扩展点实现(Extension)实际上是一个三维空间中的向量。借鉴 Maven Coordinate 的概念我给它起了个名字叫扩展坐标(Extension Coordinate),这个坐标可以用(ExtensionPoint,BizCode,TenantId)来唯一标识。
有了业务身份这个关键抽象之后,通过身份来获取扩展实现的过程就变得水到渠成了,具体流程如下
扩展点
扩展点的设计是这样的,所有的扩展点(ExtensionPoint)必须通过接口申明,扩展实现(Extension)是通过 Annotation 的方式标注的,Extension 里面使用 BizCode 和 TenantId 两个属性用来标识身份,框架的 Bootstrap 类会在 Spring 启动的时候做类扫描,进行 Extension 注册,在 Runtime 的时候,通过 TenantContext 来选择要使用的 Extension。TenantContext 是通过 Interceptor 在调用业务逻辑之前进行初始化的。整个过程如下图所示:
实例展示
比如在一个 CRM 系统里,客户要添加联系人 Contact 是一个,但是在添加联系人之前,我们要判断这个 Contact 是不是已经存在了,如果存在那么就不能添加了。不过在一个支持多业务的系统里面,可能每个业务的冲突检查都不一样,这是一个典型的可以扩展的场景。那么在 SOFA 框架中,我们可以这样去做。
1.定义扩展点
public interface ContactConflictRuleExtPt extends RuleI, ExtensionPointI { /** * 查询联系人冲突 * * @param contact 冲突条件,不同业务会有不同的判断规则 * @return 冲突结果 */ public boolean queryContactConflict(ContactE contact);}
2.实现业务的扩展实现
@Extension(bizCode = BizCode.ICBU)public class IcbuContactConflictRuleExt implements ContactConflictRuleExtPt { @Autowired private RepeatCheckServiceI repeatCheckService; @Autowired private MemberMappingQueryTunnel memberMappingQueryTunnel; private Logger logger = LoggerFactory.getLogger(getClass()); /** * 查询联系人冲突 * * @param contact 冲突条件,不同业务会有不同的判断规则 * @return 冲突结果 */ @Override public boolean queryContactConflict(ContactE contact) { Set
3.在领域实体中调用扩展实现
@ToString@Getter@Setterpublic class CustomerE extends Entity { /** * 公司ID */ private String companyId; /** * 公司(客户)名字 */ private String companyName; /** * 公司(客户)英文名字 */ private String companyNameEn; /** * 给客户添加联系人 * @param contact */ public void addContact(ContactE contact,boolean checkConflict){ // 业务检查 if (checkConflict) { ruleExecutor.execute(ContactConflictRuleExtPt.class, p -> p.queryContactConflict(contact)); } contact.setCustomerId(this.getId()); contactRepository.create(contact); }}
在上面的代码中,框架在 runtime 的时候之所以可以找到对应的扩展实现,主要是靠 @Extension(bizCode = BizCode.ICBU) 这个 Annotation,因为在系统启动时,Bootstrap 会扫描所有的扩展实现并注册并缓存到 HashMap 里面。
面向对象
面向对象不仅是一种编程语言,更是一种思维模式。所以看到很多简历里面写“精通 Java”,没写“精通 OO”,也算是中肯,因为会 Java 语言并不代表你就掌握了面向对象思维(当然,精通Java也不是件易事),要想做到精通,必须要对 OO 设计原则,模式,方法论有很深入的理解,同时要具备非常好的业务理解力和抽象能力,才能说是精通,这种思维的训练是一个长期不断累积的过程,我也在路上,下面是我对面向对象设计的两点体会:
SOLID
SOLID 是单一职责原则 (SRP),开闭原则(OCP),里氏替换原则(LSP),接口隔离原则(ISP) 和依赖倒置原则 (DIP) 的缩写,原则是要比模式(Design Pattern)更基础更重要的指导准则,是面向对象设计的 Bible。深入理解后,会极大的提升我们的 OOD 能力和代码质量。比如我在开篇提到的 ServiceImpl 上帝类的例子,很明显就是违背了单一职责,你一个类把所有事情都做了,把不是你的功能也往自己身上揽,所以你的内聚性就会很差,内聚性差将导致代码很难被复用,不能复用,只能复制(Repeat Yourself),其结果就是一团乱麻。
再比如在 Java 应用中使用 logger 框架有很多选择,什么 log4j,logback,common logging 等,每个 logger 的 API 和用法都稍有不同,有的需要用isLoggable()来进行预判断以便提高性能,有的则不需要。对于要切换不同的 logger 框架的情形,就更是头疼了,有可能要改动很多地方。产生这些不便的原因是我们直接依赖了 logger 框架,应用和框架的耦合性很高。怎么破? 遵循下依赖倒置原则就能很容易解决,依赖倒置就是你不要直接依赖我,你和我都同时依赖一个接口(所以有时候也叫面向接口的编程),这样我们之间就解耦了,依赖和被依赖方都可以自由改动了。
在我们的框架设计中,这种对 SOLID 的遵循也是随处可见,Service Facade 设计思想来自于单一职责 SRP;扩展点设计符合关闭原则 OCP;日志设计,以及 Repository 和 Tunnel 的交互就用到了依赖倒置 DIP 原则,这样的点还有很多,就不一一枚举了。当然了,SOLID 不是 OO 的全部。抽象能力,设计模式,架构模式,UML,以及阅读优秀框架源码(我们的 Command 设计就是参考了 Activiti 的 Command)也都很重要。只是 SOLID 更基础,更重要,所以我在这里重点拿出来讲一下,希望能得到大家的重视。
领域建模
准确的说 DDD 不是一个架构,而是思想和方法论,关于如何领域建模的详细请参看我另一篇文章领域建模。所以在架构层面我们并没有强制约束要使用 DDD,但对于像我们这样的复杂业务场景,我们强烈建议使用 DDD 代替事务脚本(TS: Transaction Script)。因为 TS 的贫血模式,里面只有数据结构,完全没有对象(数据 + 行为)的概念,这也是为什么我们叫它是面向过程的原因。然而 DDD 是面向对象的,是一种知识丰富的设计(Knowledge Rich Design),怎么理解?就是通过领域对象(Domain Object),领域语言(Ubiquitous Language)将核心的领域概念通过代码的形式表达出来,从而增加代码的可理解性。
这里的领域核心不仅仅是业务里的 “名词”,所有的业务活动和规则如同实体一样,都需要明确的表达出来。例如前面典型代码图中所展示的,分配策略(DistributionPolicy)你把它隐藏在一堆业务逻辑中,没有人知道它是干什么的,也不会把它当成一个重要的领域概念去重视。但是你把它抽出来,凸显出来,给它一个合理的命名叫DistributionPolicy,后面的人一看就明白了,哦,这是一个分配策略,这样理解和使用起来就容易的多了,添加新的策略也更方便,不需要改原来的代码了。所以说好的代码不仅要让程序员能读懂,还要能让领域专家也能读懂。
再比如在 CRM 领域中,公海(PublicSea)和私海(PrivateSea)是非常重要领域概念,是用来做领地(Territory)划分的,每个销售人员只能销售私海(自己领地)内的客户,不能越界。但是在我们的代码中却没有这两个实体(Entity),也没有相应的语言和其对应,这就导致了领域专家描述的,和我们日常沟通的,以及我们模型和代码呈现的都是相互割裂的,没有关联性。这就给后面系统维护的同学造成了极大的困扰,因为所有关于公海私海的操作,都是散落着各处的 repeat itself 的逻辑代码,导致看不懂也没办法维护。
采用领域建模以后,我们在系统中定义了清晰的机会(Opportunity),公海(PublicSea)和私海(PrivateSea)的 Entity,相应的行为和业务逻辑也被封装到对应的领域实体身上,让代码充分展现业务语义,让曾经散落在各处找到了业务代码找到了属于它们自己的家,它们应该在的地方。相信我,这种代码可读性的提升,会让后来接手系统的同学对你心怀感恩。下面就是我们重构后 Opportunity 实体的代码,即使你对 CRM 领域不了解,是不是也很容易看懂。
public class OpportunityE extends Entity{ @Getter @Setter private String customerId; /** * 机会类型 */ @Getter @Setter private OpportunityType opportunityType; /** * 机会来源 */ @Getter @Setter private String origin; /** * 是否可以捡入 * @return */ public boolean canPick(){ return “y”.equals(canPick) && opportunityStatus == OpportunityStatus.NEW || opportunityStatus == OpportunityStatus.ACTIVE; } /** * 是否可以开放 * @return */ public boolean canOpen(){ return (opportunityStatus == OpportunityStatus.NEW || opportunityStatus == OpportunityStatus.ACTIVE) && CommonUtils.isNotEmpty(ownerId); } /** * 捡入机会到私海 * @param privateSea */ public void pickupTo(PrivateSeaE privateSea){ privateSea.addOpportunity(this); } /** * 从私海开放出去 * @param privateSea */ public void openFrom(PrivateSeaE privateSea){ privateSea.removeOpportunity(this); } /** * 机会转移 * @param from * @param to */ public void transfer(PrivateSeaE from, PrivateSeaE to){ from.removeOpportunity(this);//从一个私海移出 to.addOpportunity(this);//添加到另一个私海中 }
如果整个系统都采用 DDD,不仅代码的可读性和系统的可维护性会大大提升,系统之间的边界和交互也会更加的清晰。下图是 CRM 域的简要领域模型,基本上可以完整的表达 CRM 领域的核心概念:
分层设计
这一块的设计比较直观,整个应用层划分为三个大的层次,分别是 App 层,Domain 层和 Infrastructure 层。
App 层主要负责获取输入,组装 context,做输入校验,发送消息给领域层做业务处理,监听确认消息,如果需要的话使用 MetaQ 进行消息通知;
Domain 层主要是通过领域服务(Domain Service),领域对象(Domain Object)的交互,对上层提供业务逻辑的处理,然后调用下层 Repository 做持久化处理;
Infrastructure 层主要包含 Repository,Config,Common 和 message,Repository 负责数据的 CRUD 操作,这里我们借用了盒马的数据通道(Tunnel)的概念,通过 Tunnel 的抽象概念来屏蔽具体的数据来源,来源可以是 MySQL,NoSql,Search,甚至是 HSF 等;Config 负责应用的配置;Common 是一写工具类;负责 message 通信的也应该放在这一层。
这里需要注意的是从其他系统获取的数据是有界上下文(Bounded Context)下的数据,为了弥合 Bounded Context 下的语义 Gap,通常有两种方式,一个是用大领域(Big Domain)把两边的差异都合起来,另一个是增加防腐层(Anticorruption Layer)做转换。什么是 Bounded Context? 简单阐述一下,就是我们的领域概念是有作用范围的(Context)的,例如摇头这个动作,在中国的 Context 下表示 NO,但是在印度的 Context 下却是 YES。
规范设计
我们规范设计主要是要满足收纳原则的两个约束:
放对位置
东西不要乱放,我们的每一个组件(Module),每一个包(Package)都有明确的职责定义和范围,不可以放错,例如 extension 包就只是用来放扩展实现的,不允许放其他东西,而 Interceptor 包就只是放拦截器的,validator 包就只是放校验器的。我们的主要组件如下图:
组件里面的 Package 如下图:
贴好标签
东西放在合适位置后还要贴上合适的标签,也就是要按照规范合理命名,例如我们架构里面和数据有关的 Object,主要有 Client Object,Domain Object 和 Data Object,Client Object 是放在二方库中和外部交互使用的 DTO,其命名必须以 CO 结尾,相应的 Data Object 主要是持久层使用的,命名必须以 DO 结尾。这个类名应该是自明的(self-evident),也就是看到类名就知道里面是干了什么事,这也就反向要求我们的类也必须是单一职责的(Single Responsibility)的,如果你做的事情不单纯,自然也就很难自明了。如果我们 Class Name 是自明的,Package Name 是自明的,Module Name 也是自明的,那么我们整个应用系统就会很容易被理解,看起来就会很舒服,维护效率会提高很多。除了组件和包的命名规范以外,我们对类、方法名和错误码做了如下约定:
类名约定:
方法名约定:
层次 类 方法名约定
App层 接口服务 新增:save
修改:modify
查询:get(单个)、list(多个、分页)
统计:count
删除:remove
Domain层 Domain实体 尽量避免CRUD形式的命名,要体现业务语义
Tunnel层 Tunnel对象 新增:create
修改:update
查询: get(单个) 、list(多个)、page(分页)
删除:delete
统计:count
错误码约定:
异常主要分为系统异常和业务异常,系统异常是指不可预期的系统错误,如网络连接,服务调用超时等,是可以retry的;而业务异常是指有明确业务语义的错误,再细分的话,又可以分为参数异常和业务逻辑异常,参数异常是指用户过来的请求不准确,逻辑异常是指不满足系统约束,比如客户已存在。业务异常是不需要 retry 的。
我们的错误码主要有 3 部分组成:类型+场景+自定义标识。
错误类型 错误码约定 举例
参数异常 P_XX_XX P_CAMPAIGN_NameNotNull: 运营活动名不能为空
业务异常 B_XX_XX B_CAMPAIGN_NameAlreadyExist: 运营活动名已存在
系统异常 S_XX_ERROR S_DATABASE_ERROR: 数据库错误
SOFA 应用架构
经过上面的长篇大论,我希望我把我们的架构理念阐述清楚了,最后再从整体上看下我们的架构吧。我讲这个架构命名为 SOFA,全称是 Simple Object-oriented and Flexible Architecture,是一个轻量级的面向对象的,可扩展的应用架构,可以帮助降低复杂应用场景的系统熵值,提升系统开发和运维效率。
目前框架也准备开源,贡献个社区,让更多的开发者使用,帮助解决他们各自的业务复杂度。
关于框架源码和介绍,请移步:http://gitlab.alibaba-inc.com/b2bcrm/sofa
整体架构
整体架构是遵循高内聚,低耦合,可扩展,易理解的知道思想,尽可能的贯彻 OO 的设计思想和原则。我们最终形成的架构是集成了扩展点 + 元数据 + CQRS+DDD 的思想,关于元数据前面没怎么提到,这里稍微说一下,对于字段扩展,简单一点的解决方案就是预留扩展字段,复杂一点的就是使用元数据引擎。使用元数据的好处是不仅能支持字段扩展,还提供了丰富的字段描述,等于是为以后的 SaaS 化配置提供了可能性,所以我们选择了使用元数据引擎。和 DDD 一样,元数据也是可选的,如果对没有字段扩展的需求,就不要用
https://blog.csdn.net/valada/article/details/80892694
https://blog.csdn.net/gitchat/article/details/80809684
https://tech.antfin.com/products/SOFA
https://gitbook.cn/gitchat/activity/5b066f041b5f14201392f2b6
https://my.oschina.net/saulc/blog/1622695
https://www.cnblogs.com/jiangyaxiong1990/articles/9191636.html