SOFA企业应用框架

前言

从业这么多年,接触过银行的应用,Apple的应用,eBay的应用和如今阿里的应用,虽然分属于不一样的公司,使用了不一样的架构,但有一个共同点就是都很复杂。致使复杂性的缘由有不少,若是从架构的层面看,主要有两点,一个是架构设计过于复杂,层次太多能把人绕晕。另外一个是根本就没架构,ServiceImpl做为上帝类包揽一切,一杆捅到DAO(就简单场景而言,这种Transaction Script也还凑合,至少实现上手都快),这种人为的复杂性致使系统愈来愈臃肿,愈来愈难维护,酱缸的老代码发出一阵阵恶臭,新来的同窗,每每要捂着鼻子抠几天甚至几个月,才能理清系统和业务脉络,而后又一头扎进各类bug fix,业务修补的恶性循环中,暗无天日!
image.png
CRM做为阿里最老的应用系统,天然也逃不过这样的宿命。不甘如此的咱们开始反思究竟是什么形成了系统复杂性? 咱们到底能不能经过架构来治理这种复杂性?基于这个出发点,咱们团队开始了一段很是有意义的架构重构之旅(Redefine the Arch),期间咱们参考了SalesForce,TMF2.0,汇金和盒马的架构,从他们那里汲取了不少有价值的输入,再结合咱们本身的思考最终造成了咱们本身如今的基于扩展点+元数据+CQRS+DDD的应用架构。该架构的特色是可扩展性好,很好的贯彻了OO思想,有一套完整的规范标准,并采用了CQRS和领域建模技术,在很大程度上能够下降应用的复杂度。本文主要阐述了咱们的思考过程和架构实现,但愿能对在路上的你有所帮助。html

复杂性来自哪里

通过咱们分析、讨论,发现形成如今系统异常复杂的罪魁祸首主要来自如下四个方面:java

可扩展性差

对于只有一个业务的简单场景,并不须要扩展,问题也不突出,这也是为何这个点常常被忽略的缘由,由于咱们大部分的系统都是从单一业务开始的。可是随着支持的业务愈来愈多,代码里面开始出现大量的if-else逻辑,这个时候代码开始有坏味道,没闻到的同窗就这么继续往上堆,闻到的同窗会重构一下,但由于系统没有统一的可扩展架构,重构的技法也各不相同,这种代码的不一致性也是一种理解上的复杂度。长此以往,系统就变得复杂难维护。像咱们CRM应用,有N个业务方,每一个业务方又有N个租户,若是都要用if-else判断业务差别,那简直就是惨绝人寰。其实这种扩展点(Extension Point),或者叫插件(Plug-in)的设计在架构设计中是很是广泛的。比较成功的案例有eclipse的plug-in机制,集团的TMF2.0架构。还有一个扩展性需求就是字段扩展,这一点对SaaS应用尤其重要,由于有不少客户定制化需求,可是咱们不少系统也没有统一的字段扩展方案。git

面向过程

是的,无论你认可与否,不少时候,咱们都是操着面向对象的语言干着面向过程的勾当。面向对象不只是一个语言,更是一种思惟方式。在咱们追逐云计算、深度学习、区块链这些技术热点的时候,静下心来问问本身咱们是否是真的掌握了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 executegithub

因此强烈谴责那些不顾他人感觉的编码行为。算法

分层不合理

俗话说的好,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。而不是到“一锅粥”里去慢慢抠。
image.png
本章节最后,上一张咱们老系统中比较典型的代码,也许你能够从中看到你本身应用的影子。
sample.png设计模式

复杂性应对之道

知道了问题所在,接下来看下咱们是如何一个个解决这些问题的。回头站在山顶再看这些解决方案时,每一个都不足为奇,但当你还“身在此山中”的时候,这个拨开层层迷雾,看到山的全貌的过程,并非想象的那么容易。庆幸的是我团队在艰难跋涉以后,终有所收获。网络

扩展点设计

扩展点的设计思想主要得益于TMF2.0的启发,其实这种设计思想也一直在用,但都是在局部的代码重构和优化,好比基于Strategy Pattern的扩展,可是一直没有找到一个很好的固化到框架中的方法。直到毗卢到团队分享,给了咱们两个关键的提示,一个是业务身份识别,用他的话说,若是当时TMF1.0若是有身份识别的话,就没有TMF2.0什么事了;另外一个是抽象的扩展点机制。数据结构

身份识别

业务身份识别在咱们的应用中很是重要,由于咱们的CRM系统要服务不一样的业务方,并且每一个业务方又有多个租户。好比中供销售,中供拍档,中供商家都是不一样的业务方,而拍档下的每一个公司,中供商家下的每一个供应商又是不一样的租户。因此传统的基于多租户(TenantId)的业务身份识别还不能知足咱们的要求,因而在此基础上咱们又引入了业务码(BizCode)来标识业务。因此咱们的业务身份其实是(BizCode,TenantId)二元组。在每个业务身份下面,又能够有多个扩展点(ExtensionPoint),因此一个扩展点实现(Extension)其实是一个三维空间中的向量。借鉴Maven Coordinate的概念我给它起了个名字叫扩展坐标(Extension Coordinate),这个坐标能够用(ExtensionPoint,BizCode,TenantId)来惟一标识。
image.png

扩展点

扩展点的设计是这样的,全部的扩展点(ExtensionPoint)必须经过接口申明,扩展实现(Extension)是经过Annotation的方式标注的,Extension里面使用BizCode和TenantId两个属性用来标识身份,框架的Bootstrap类会在Spring启动的时候作类扫描,进行Extension注册,在Runtime的时候,经过TenantContext来选择要使用的Extension。TenantContext是经过Interceptor在调用业务逻辑以前进行初始化的。整个过程以下图所示:
image.png

面向对象

面向对象不只是一种编程语言,更是一种思惟模式。因此看到不少简历里面写“精通Java”,没写“精通OO”,也算是中肯,由于会Java语言并不表明你就掌握了面向对象思惟(固然,精通Java也不是件易事),要想作到精通,必需要对OO设计原则,模式,方法论有很深刻的理解,同时要具有很是好的业务理解力和抽象能力,才能说是精通,这种思惟的训练是一个长期不断累积的过程,我也在路上,下面是我对面向对象设计的两点体会:

SOLID

SOLID是单一职责原则(SRP),开闭原则(OCP),里氏替换原则(LSP),接口隔离原则(ISP)和依赖倒置原则(DIP)的缩写,原则是要比模式(Design Pattern)更基础更重要的指导准则,是面向对象设计的Bible。深刻理解后,会极大的提高咱们的OOD能力和代码质量。好比我在开篇提到的ServiceImpl上帝类的例子,很明显就是违背了单一职责,你一个类把全部事情都作了,把不是你的功能也往本身身上揽,因此你的内聚性就会不好,内聚性差将致使代码很难被复用,不能复用,只能复制(Repeat Yourself),其结果就是一团乱麻。
image.png
再好比在java应用中使用logger框架有不少选择,什么log4j,logback,common logging等,每一个logger的API和用法都稍有不一样,有的须要用isLoggable()来进行预判断以便提升性能,有的则不须要。对于要切换不一样的logger框架的情形,就更是头疼了,有可能要改动不少地方。产生这些不便的缘由是咱们直接依赖了logger框架,应用和框架的耦合性很高。怎么破? 遵循下依赖倒置原则就能很容易解决,依赖倒置就是你不要直接依赖我,你和我都同时依赖一个接口(因此有时候也叫面向接口的编程),这样咱们之间就解耦了,依赖和被依赖方均可以自由改动了。
image.png
在咱们的框架设计中,这种对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领域中,公海和私海是很是重要领域概念,是用来作领地(Territory)划分的,每一个销售人员只能销售私海(本身领地)内的客户,不能越界。可是在咱们的代码中却没有这两个实体(Entity),也没有相应的语言和其对应,这就致使了领域专家描述的,和咱们平常沟通的,以及咱们模型和代码呈现的都是相互割裂的,没有关联性。这就给后面系统维护的同窗形成了极大的困扰,由于全部关于公海私海的操做,都是散落着各处的repeat itself的逻辑代码,致使看不懂也没办法维护。因此当尚学把这两个领域概念抽象成实体以后,整个模型和代码都一会儿变清晰不少。在加上上面介绍的把业务规则显现化,极大的提高了代码的可读性和可扩展性。用尚学的话说,用DDD写代码,他找到了创做的感受,而不只仅是码农式Coding。下图是销售域的简要领域模型,但基本上能表达出销售域的核心领域概念。
image.png
关于CQRS简要说一下,咱们只使用了Command,Query分离的概念,并无使用Event Sourcing,缘由很简单---不须要。关于Command的实现咱们使用了命令模式,所以之前的ServiceImpl的职责就只是一个Facade,全部的处理逻辑都在CommandExecutor里面。

分层设计

这一块的设计比较直观,整个应用层划分为三个大的层次,分别是App层,Domain层和Repostiory层。

  • App层主要负责获取输入,组装context,作输入校验,发送消息给领域层作业务处理,监听确认消息,若是须要的话使用MetaQ进行消息通知;
  • Domain层主要是经过领域服务(Domain Service),领域对象(Domain Object)的交互,对上层提供业务逻辑的处理,而后调用下层Repository作持久化处理;
  • Infrastructure层主要包含Repository,Config和Common,Repository负责数据的CRUD操做,这里咱们借用了盒马的数据通道(Tunnel)的概念,经过Tunnel的抽象概念来屏蔽具体的数据来源,来源能够是MySQL,NoSql,Search,甚至是HSF等;Config负责应用的配置;Common是一写工具类;负责message通讯的也应该放在这一层。 image.png这里须要注意的是从其余系统获取的数据是有界上下文(Bounded Context)下的数据,为了弥合Bounded Context下的语义Gap,一般有两种方式,一个是用大领域(Big Domain)把两边的差别都合起来,另外一个是增长防腐层(Anticorruption Layer)作转换。什么是Bounded Context? 简单阐述一下,就是咱们的领域概念是有做用范围的(Context)的,例如摇头这个动做,在中国的Context下表示NO,可是在印度的Context下倒是YES。

规范设计

咱们规范设计主要是要知足收纳原则的两个约束:

放对位置

东西不要乱放,咱们的每个组件(Module),每个包(Package)都有明确的职责定义和范围,不能够放错,例如extension包就只是用来放扩展实现的,不容许放其余东西,而Interceptor包就只是放拦截器的,validator包就只是放校验器的。咱们的主要组件以下图:
image.png
组件里面的Package以下图:
image.png

贴好标签

东西放在合适位置后还要贴上合适的标签,也就是要按照规范合理命名,例如咱们架构里面和数据有关的Object,主要有Client Object,Domain Object和Data Object,Client Object是放在二方库中和外部交互使用的DTO,其命名必须以CO结尾,相应的Data Object主要是持久层使用的,命名必须以DO结尾。这个类名应该是自明的(self-evident),也就是看到类名就知道里面是干了什么事,这也就反向要求咱们的类也必须是单一职责的(Single Responsibility)的,若是你作的事情不单纯,天然也就很难自明了。若是咱们Class Name是自明的,Package Name是自明的,Module Name也是自明的,那么咱们整个应用系统就会很容易被理解,看起来就会很舒服,维护效率会提升不少。咱们的命名规则以下图所示:
image.png

SOFA应用架构

通过上面的长篇大论,我但愿我把咱们的架构理念阐述清楚了,最后再从总体上看下咱们的架构吧。我讲这个架构命名为SOFA,全称是Simple Object-oriented and Flexible Architecture,是一个轻量级的面向对象的,可扩展的应用架构,能够帮助下降复杂应用场景的系统熵值,提高系统开发和运维效率。
image.png
目前框架也准备开源,贡献个社区,让更多的开发者使用,帮助解决他们各自的业务复杂度。
关于框架源码和介绍,请移步:https://github.com/alibaba/SOFA/

总体架构

咱们的架构原则很简单,即在高内聚,低耦合,可扩展,易理解大的指导思想下,尽量的贯彻OO的设计思想和原则。咱们最终造成的架构是集成了扩展点+元数据+CQRS+DDD的思想,关于元数据前面没怎么提到,这里稍微说一下,对于字段扩展,简单一点的解决方案就是预留扩展字段,复杂一点的就是使用元数据引擎。使用元数据的好处是不只能支持字段扩展,还提供了丰富的字段描述,等因而为之后的SaaS化配置提供了可能性,因此咱们选择了使用元数据引擎。和DDD同样,元数据也是可选的,若是对没有字段扩展的需求,就不要用。最后的总体架构图以下:
image.png

提效工具

Archetype

由于框架包含了5个Module,20+的Package,若是手动建立的话很费时,并且很容易出错,因此建立了这个Archetype,能够一键构建框架的全部Artifacts,使用时,只须要将下面的命令中的demo替换成本身应用的名字便可:

mvn archetype:generate  -DgroupId=com.alibaba.crm -DartifactId=demo -Dversion=1.0.0-SNAPSHOT -Dpackage=com.alibaba.crm.demo -DarchetypeArtifactId=sofa-framework-archetype -DarchetypeGroupId=com.alibaba.sofa -DarchetypeVersion=1.0.0-SNAPSHOT

测试容器

无论你是否是TDD吧,写几行代码,而后本地跑下测试验证一下老是个不错的习惯。由于代码仍是热的,出错也容易定位。可是本地启动PandoraBoot可不是个省心的事,我这台2.3G双核平均也要4分钟,严重的影响了效率。因此开发了这个工具,就是等PandoraBoot启动后,将线程Hold住,而后经过Console控制台输入要测试的方法或者类。使用这个工具很简单。
首先依赖crm-test:

<dependency>
   <groupId>com.alibaba.crm</groupId>
   <artifactId>crm-test</artifactId>
   <version>1.0.0-SNAPSHOT</version>
   <scope>test</scope>
</dependency>

 

而后将TestApplication修改以下便可:

public class TestApplication {
    public static void main(String[] args) {
        PandoraBootstrap.run(args);
        SpringApplication.run(Application.class, args);
        TestsContainer.start();//启动测试容器,避免重复启动PandoraBoot
    }
}

 

转载自:http://blog.csdn.net/significantfrank/article/details/79286947

相关文章
相关标签/搜索