GitHub Repo: https://github.com/wxyyxc1992/RARF前端
本文仅表明我的思考,如有谬误请一笑置之。若能指教一二则感激涕零。另外本文所写是笔者我的的思考,暂未发现有相似的工做。不过估计按照笔者的智商可能世界上已经有不少大神早就解决了笔者考虑的问题,如有先行者也不吝赐教。git
在文章之初,笔者想交代下本身的背景,毕竟下文的不少考虑和思想都是来源于笔者所处的一个环境而产生的,笔者处于一个创业初期产品需求业务需求急速变动的小型技术团队。通常而言软件工程的基本步骤都会是先定义好需求,定义好数据存储,定义好接口规范与用户界面原型,可是笔者所在的环境下每每变动的速度已经超过了程序猿码字的速度N倍。另外,笔者自己也是一名前端开发者(待过创业公司的都知道,啥都得上),笔者不少用于后端开发的思想也受到了前端很大的影响,譬如后端一直遵循的MVC架构模式,而前端已经有了MVC、MVP、MVVM、Flux、Redux等等这一些变迁。前端迎来了组件化的时代,然后端也是微服务的思想大行其道。本文主要受如下几个方法论的影响:github
RESTful数据库
MicroService编程
Reactive Programming后端
Reduxapi
在进行具体的表述以前,笔者想先把一些总结的内容说下,首先是几个前提吧:服务器
不要相信产品经理说的需求永远不变了这种话,too young,too naive网络
在快速迭代的状况下,现有的后台接口僵化、代码混乱以及总体可变性与可维护性变差,不能仅仅依赖于提升程序猿的代码水平、编写大量复杂的接口说明文档或者期望在项目开始之初即能定下全部接口与逻辑。目前的后端开发中流行的MVC架构也负有必定的责任,咱们的目标是但愿可以寻找出在最差的人员条件下也能有最好结果的方式。或者描述为不管程序猿水平如何,只要遵循几条基本原则,便可构造高可用逻辑架构,而且这些规则具备高度抽象性而与具体的业务逻辑无任何关系。前端工程师
任何一个复杂的业务逻辑均可以表示为对一或多种抽象资源的四种操做(GET、POST、PUT、DELETE)的组合。
RARF的全称是Reactive Abstract Resource Flow,字面意思是响应式抽象资源流,笔者把它称为基于抽象资源流的响应式处理的架构风格。通常来讲,Server-Side开发包含接口定义、具体业务逻辑处理方法、数据存储交互(SQL+NoSQL)这三个层次,RARF也是面向这三个方面,提出了一些本身的设想。这边的Reactive表示RFRF同时关注了并发与异步这一方面的问题,并选择了Reactive Programming或者更明确一点的,FPR来做为方法论。
为啥笔者一直在强调本身是个前端工程师,就是在思考RARF的时候,我也从一个前端工程师的角度,来考虑到底前端须要怎样的一种接口方案。
REST风格的一个特征就是资源化的接口描述,譬如:
[GET] http://api.com/books [GET] http://api.com/book/1 [POST] http://api.com/book
这种资源化的接口可读性仍是比较好的,经过动词与资源名的搭配很容易就能知道该接口描述的业务逻辑是啥。不过,在笔者浅薄的认知里,包括REST原版论文和后面看的各式各样的描述,都只针对单个资源进行了描述,即简单逻辑进行了表述。若是如今咱们要添加一个购买书籍的接口,在该接口内包括了建立订单、扣费等一系列复杂逻辑操做,相信程序猿会倾向于使用:
http://api.com/doBookBuy
对于单个接口而言,可读性貌似上去了,可是,这最终又会致使接口总体命名的混乱性。确实能够经过统一的规划定义来解决,可是笔者我的认为这并无从接口风格自己来解决这个问题,仍是依赖于项目经理或者程序猿的能力。
笔者在RARF中定义了一个概念,叫Uniform Resource Flow Path(统一资源流动路径)来增长接口的描述性,同时URFP也是整个RARF的驱动基础。
不管是WebAPP仍是iOS仍是Android,都须要与业务服务器进行交互,咱们通常称之为消费接口,也就是所谓Consume API的感受。笔者一直有个形象的感受,如今先后端交互上,就好像去饭店吃饭,厨师把菜单拿过来,消费者按照菜单定好的来点菜。而笔者但愿能在RARF达成的效果,就是把点菜变成自助餐,也就是所谓的前端能够根据后端提供的基本的资源的四种操做来完成本身复杂的业务逻辑。笔者在这里先描述两个常见的蛋疼场景:
(1)先后之争,该迁就谁?
做为一个前端开发者,每每会以界面为最小单元来进行逻辑分割(不考虑组件化这种代码分割的影响),毕竟每次请求都会引发延迟与网络消耗,因此前端开发者每每但愿我在一个界面上进行一次逻辑请求便可获取全部的待展现参数。可是对于后端开发者而言,是以接口为最小单元来进行逻辑分割,有点相似于那种SRP(单一职责原则),每一个接口作好本身的事情就行了。譬如若是UI为Android端设计了一个界面,该界面上展现了一个电商活动的信息,譬如:
而后呢,UI为iOS端设计了一个界面,展现某个电商活动的全部商品,譬如:
而后,UI为WebAPP设计了一个界面,同时包含了活动介绍和商品列表,大概是这样:
那么如今后端要提供接口了,若是按照逻辑分割的要求,最好是提供三个接口:
/activities ->获取全部的活动列表 /activity/1 ->根据某个活动ID获取活动详情,包含了该活动的商品列表 /goods/2 ->根据商品编号获取商品详情
这样三个原子化的接口是如此的清晰好用,而后后端就会被iOS、Android、WebAPP三端混合打死。那再换个思路呢,按照最大交集,就是WebAPP的需求,我把所有数据一股脑返回了,而后后端就会被产品经理打死吧,浪费了用户这么多流量,顺带着延迟啊服务器负载啊也都上来了。另外呢,譬如商品表中有个描述属性,是个很大的文本,那么咱们在返回简略列表的时候,确定不能返回这个大文本啊,这样的话,是否是又要多提供一个接口了呢?
假设业务逻辑中有$M$个不一样的资源,相互之间有交集,而每一个资源有$N$个属性,那么夸张来讲,若是按照界面来提供接口,大概须要:
$$
\sum_{i=1}^M C_M^i * ( \sum_{j=1}^{i} (C_N^j) )
$$
种搭配,固然了,这绝逼是个夸张的描述。
(2)界面变了/接口变了/参数变了
假设勤恳的后端程序猿按照iOS、Android、WebAPP不一样的要求写了不一样的接口,完美的解决了这个问题,而后UI大手一挥,界面要改,以前作的菜是否是全都得倒了呢?
同理,若是接口变了或者接口参数变了,不管先后端估计都会想杀人的吧?
在目前的前端开发中,状态管理这个概念很流行,笔者自己在前端的实践中也是深感其的必要性。在前端中,根据用户不一样的交互会有不一样的响应,而对于这些响应的管理便是一个很大的问题。笔者在这里是借鉴的Redux,它提出了构造一颗全局的状态树,而且利用大量的纯函数来对这颗状态树进行修正。同时,这颗状态树是可追踪的,换言之,经过对于状态树构造过程的跟踪能够彻底反推出全部的用户操做。
那么这个状态的概念,移植到后端的开发中,应该咋理解呢?首先,后端是对于用户的某个请求的响应,那么就不存在前端那种对于用户复杂响应的问题。直观来看,后端的每一个Controller中的逻辑操做都是顺序面条式的(即便是异步响应它在逻辑上也是顺序的),能够用伪代码描述以下:
RequestFilter();//对于请求过滤与权限审核 a = ServiceA();//根据逻辑须要调用服务A,而且获取数据a if(a=1){ b = ServiceB(a);//根据上一步获得的结果,调用某个特定的Service代码,注意,数据存取操做被封装在了Service中 }else{ b = ServiceC(a);//不然就调用 } c = ServiceD(b) put c
目前不少的Controller里是没有一个全局状态的,咱们习惯了调用不一样的Service而后进行判断处理在最后整合成一个结果真后返回给用户。这就致使了存在着大量的临时变量,譬如上面伪代码中的a,b,c
。笔者认为的状态管理的结果,就是在一个逻辑处理的流程中,不须要看前N步代码便可以判断出当前的变量状态或者可能的已知值,特别是在存在嵌套了N层的条件判断之下。笔者认为的状态混乱的第一个表现便是缺失这样一种全局状态,顺便插一句,若是有学过Redux的朋友确定也能感觉到,在有全局状态树的状况下能够更好地追踪代码执行过程而且重现错误。
另外一个可能引发状态混乱的缘由,便是Service的不可控性。借用函数式编程中的概念,咱们是但愿每一个被调用的Service函数都是纯函数(假设把数据库输入也做为一个抽象的参数),不会存在Side Effect。可是不能否认,现行的不少代码中Service函数会以不可预知的方式修改变量,虽然有时候是为了节约内存空间的考虑,譬如:
ArrayList<Integer> array = new ArrayList(); Service(array);//在Service中向array里添加了数据
而这样一种Side Effect的表现之一就是咱们在调试或者修改代码的时候,须要递归N层才能找到某变量究竟是在哪边被修改了,这对于代码的可维护性、可变性都形成了很大的负担。
由于每个Controller都是面向一条完整的业务逻辑进行抽象,因此在朴素的认知下并不能很好地进行代码分割。在符合天然认知规律的开发模式中,咱们习惯先定义出接口,而后在具体的接口须要的功能时进行抽象,譬如在项目之初,咱们须要一个获取全部商品列表的接口,咱们能够定义为:
[GET]:/goodss -> 映射到getGoodsController
在编写这个getGoodsController方法的过程当中,咱们发现须要去goods表中查询商品信息,那咱们会提出一个selectGoodsAllService
的服务方法,来帮助咱们进行数据查询。而后,又多了一个需求,查询全部价格小于10块的商品:
[GET]:/goods?query={"price":"lt 10"} -> 映射到getGoodsController
这时候咱们就须要在Controller加个判断了,判断有没有附带查询条件,若是附带了,就要调用专门的Service。
或者也能够为这个查询写一个专门的Controller:
[GET]:/getGoodsByPriceLess?price=10 -> getGoodsByPriceLessController
一样须要编写一个根据价格查询的Service。而后,随着业务的发展,咱们须要根据商家、剩余货物量等等进行查询,而后Service愈来愈多,慢慢的,就陷入了抽象漏洞的困境,就是咱们虽然抽象了,可是根本不敢去用下面的代码,由于就怕那是个大坑,比较不知道它的抽象究竟是遵循了什么的规则。
上图描述了一个请求处理过程当中的依赖传递问题,直观的感觉是,当咱们的业务逻辑系统变得日渐复杂以后,不依赖Code Review或者测试,特别是对于小团队而言,不敢去随便乱改一个现有的服务,宁肯去为本身要处理的逻辑写一个新的服务,由于压根不知道现有的服务到底被多少个东西依赖着。牵一发而动全身啊。这就致使了随着时间的增加,系统中的函数方法愈来愈多,而且不少是同构的。举例而言,有时候业务须要根据不一样的年龄来获取不一样的用户,可是程序猿在初期接到的业务需求是查找出全部未成年的人,它就写了个方法getMinorsService()
,这个服务不用传入任何参数,直接调用就好。而后某天,有个新的需求,查找全部的没到20岁的人,因而又有了一个服务getPeopleYoungerThan20()
。这样系统中就多出了大量的功能交叉的服务,而这些服务之间也可能互相依赖,造成一个复杂的依赖网络。若是有一天,你要把数据库的表结构动一下(譬如要把部分数据移到NoSQL中),就好像要踩N颗地雷同样的感受吧。
说到依赖,相信不少人首先想到的就是Spring的DI/IoC的概念。不过笔者认为依赖注入和本文笔者纠结的依赖混乱的问题,仍是有所区别的。依赖注入的原理和使用确实很是方便,可是它仍是没有解决依赖划分混乱的问题,或者须要大量的劳动力去在代码之初就把全部的依赖肯定好,那么这一点和RARF文初假设的场景也是不一致的。
最后,笔者在RARF中,不会解决依赖传递或者多重依赖的问题,只是说经过划分逻辑资源的方式,把全部的依赖项目明晰化了。譬如上面提到的N个具备交叉功能的GET函数,都会统一抽象成对于某个资源的抽象GET操做。换言之,以URFP的抽象原则进行统一调用。
在富客户端发展的大潮之下,服务端愈来愈像一个提供前端进行CRUD的工具。任何一个学过数据库,学过SQL的人,都知道,联表查询比分红几个单独的查询效率要来的好得多,这也是毋庸置疑的。可是,在本文所描述的状况下,联表查询会破坏本来资源之间的逻辑分割。这边先说一句废话:在MVC中,当数据表设计和接口设计定了以后,中间的代码实现也就定了。
举例来讲,咱们存在两个业务需求:获取我收藏的商品列表和我购买过的商品列表,这边涉及到三个表:goods(goods_id),goods_favorite(goods_id,user_id),goods_order(goods_id,user_id)。在MVC中,咱们会倾向于写上三个接口,配上三个Service,这就是典型的提升了查询效率,而且符合人们正常开发顺序的方式。不过,若是单纯从资源的逻辑分割的角度考虑的话,对于商品查询而言,应该只有一个selectGoodsByGoodsId
这个Service,换言之,把原来的单次查询拆分为了:先在其余表中查询出goods_id,而后再用goods_id在goods表中查询商品详情。这样的逻辑分割是否是明晰了一点?
是啊,这确定会影响效率的,笔者也在想着能不能经过某种方法达成一个平衡。
若是你看到这还有兴趣的话,能够看看下面笔者的思考。
万物皆抽象资源
RESTful中也有all is resource
的概念,可是RESTful强调的是像超文本啊、某个音视频啊,这些均可以经过URI访问到,也就是能够当作一个资源而后被前端获取。这一点已经得到了普遍地承认,而在RARF中强调的抽象资源,就好像在电商系统中,咱们都知道goods与user是两种资源,那么在描述用户收藏夹的时候,即对于user_goods这个表,算不算资源呢?固然算啊!换言之,描述两个资源之间关系的,不管是一对一,仍是一对多,只要具有惟一标识的,就是独立的抽象资源。
不过,若是哪一天逻辑设计上,把用户收藏的商品,变成了JSON字段而后存储在user表中,即成了user资源的一个属性,那么此时这种映射关系就不是一个资源了。由于它没有一个惟一标识。
在讲资源的定义以前,首先看看关系型数据库中经典的设计范式:
第一范式(确保每列保持原子性)
第二范式(2NF)属性彻底依赖于主键(消除部分子函数依赖)
第三范式(3NF)属性不依赖于其它非主属性[消除传递依赖]
对于从具体的业务逻辑抽象出相互分割而且关联的资源是RARF的基础,在笔者构思RARF的基本原则时,一开始是想走强制严格化道路,即严格命名,具体而言:
万物皆资源,资源皆平等。
每一个资源具备惟一的不可重复的命名。
任何资源具备一个惟一的标识,即{resource_name_id}在全部表中必须保持一致。譬如咱们定义了一个资源叫user,那么它的标识就是user_id,不能够叫uid、userId等等其余名称。
任何资源的属性由{resource_name_attribute_name}构成,且遵循第二与第三范式。
这一套命名规则,有点像乌托邦吧,毕竟做为一个不成熟的想法,RARF仍是要去切合已经存在的各类各样的数据库设计风格或者方案,不可能让人家把表推倒所有重建,因此呢,最后资源定义的规范就一句话:资源名不可重复且资源属性具备惟一全部性。不过想一想,若是按照严格命名方案的话,会自动化不少。
RARF的基石便是抽象资源(Abstract Resource)与对于资源的操做(Handling),笔者这里暂时借用函数式编程与Redux的概念,ResourceHandler咱们能够把它当作"纯函数对待"。这里的ResourceHandler,咱们能够将用户输入与数据库输入当作两个输入,在输入相同的状况下ResourceHandler的执行结果是一致的,换言之,在RARF中,ResourceHandler自己是无状态的、无反作用的。
而每一个ResourceHandler向外提供的资源的操做,就是GET、POST、PUT与DELETE。这里不进行赘述了。
到这里为止,咱们实际上是针对RESTful原始的譬如:
/book/1
这种逻辑的处理进行了分析,仍是对于单个资源的,在实际的业务场景中,咱们每每是对一或多个资源的组合操做。
学过数学的都知道,两点肯定一条直线,而在后端逻辑开发中,当你的Controller(Controller指响应某个固定业务请求的接口处理函数)与数据表设计定了以后,你中间的代码该怎么写也就定了。换言之,Controller与数据表是主食材,中间代码是调味料。这也就是典型的点菜模式,而RARF中一直强调的资源操做的组合的概念,是但愿前端可以本身用资源和资源的操做来给本身作一盘菜。而URFP正是提供这种灵活性的核心机制。RESTful是推荐使用四个动词来分割一个业务,在非RESTful风格下,若是用户要买个东西,接口应该这么定义:
/doGoodsOrderCreate?goods_id=1
若是是RESTful风格的接口呢:
POST:/goods_order?goods_id=1&...
因而我本身想,按照RARF的资源流动的这个感受,那是否是应该:
POST:/goods/{goods_id}/goods_order?requestData={requestData}
这一套URL的规则笔者称之为Uniform Resource Flow Path(统一资源流动路径),换言之,将与业务逻辑相关的必须性资源放在url上。这样写有啥好处呢,我本身以为啊,一个是可读性更好了,二个呢在代码层次区分的也会更开了。就能很好地把各个独立地ResourceHandler给串联起来啦。
原则:全部URFP的邻接资源之间必存在且只存在ResourceID依赖关系
资源转化是URFP的最核心的思想,感性理解的话,譬如对于下面这个URL,是用来获取某个用户的收藏夹的:
/user/1/goods_favorite/all/goods
当Application接收到这个请求时,会建立一个ResourceBag,就好像一个空的购物篮。而后ResourceBag被传送到UserHandler,根据user_id里面拿到了一个User资源的实例,而后下一步又被传送到了goodsFavoriteHandler,这个Handler看下篮子里已经有了User资源,而后就根据user_id查询出新的GoodsFavorite资源,而后这个篮子再被传递到GoodsHandler,同理,根据篮子里现有的资源来换取得到Goods资源的实例。
这种用已用的资源去获取新资源的方式就是所谓的资源转化,注意,邻接资源之间务必存在主键依赖关系,譬如从goods_favorite转化到goods的时候,在goods_favorite表中就有一列是表示的是goods_id。
资源注入的应用场景能够描述以下:
UI设计了一个界面,同时展现了商品列表和商品的供货方的信息。这就等于要把两种资源合并返回了,在上文先后之争中笔者就已经讨论过,最符合逻辑分割的思想就是让前端先请求一波商品列表,而后根据返回的goods_provider_id来获取供货方的信息,不过估计要是真的这么作了,会被打死的吧。
笔者这边呢,引入了一个resource_integration
关键字,譬如,咱们的这个请求能够这么写:
/goods?requestData={"resource_integration":["goods_provider"]}
那么在ResourceHandler接收到这个请求的时候,会自动根据须要注入的资源,进行资源注入。
这边还有个遗留问题,相似于数据库查询中的左联接和右联接,若是须要注入的资源不存在,咋办呢?我还没碰到过这个业务场景,若是有朋友遇到过,请和我联系。
实际上URFP并不能完美显示全部的业务逻辑,譬如在购买商品时候,咱们是把它看作了对于goods_order资源的一个POST操做,可是实际业务中,购买了商品以后还要对goods进行操做,即把商品数目减一,还要对credit进行操做,即把用户积分加减,或者建立支付订单等等等等。以上这些隐性业务逻辑是与返回结果强相关的,直接写在ResourceHandler当中便可,那还有一种是非强相关的,最典型的例子就是日志功能。在正常的业务逻辑处理时,不可能由于你日志记错了就不让用户去购买东西的吧?
关于这部分隐性业务逻辑的处理,笔者其实不太喜欢AOP这种方式,感受不太可控,因此仍是想借鉴Middleware(两者区别在哪呢?)或者Reducer这种方式。
看到这里,若是还有兴趣的话能够看看笔者Java版本的实现,正在剧烈变更中,不过代码也很少就是了,若是笔者的思想真的可能有些意义的话笔者打算在NodeJs、PHP以及Swift这几个语言中都实现一下,也欢迎大神的加入。
RARF有一个核心理念就是资源的独立性,可是在现有的后端程序开发中,特别是基于关系型数据库的开发时,咱们不可避免的会引入联表查询。譬如系统中使用goods_user这个表来描述用户的商品收藏夹,若是咱们要获取某个用户的收藏的商品列表,最好的方式确定是用goods_user与goods这两个表进行联合查询,可是这样的话势必又没法达成资源分割的目标,那么在RARF中,最极端的方法是:
/goods_user/all/goods
根据上文对于URFP的描述,这个路径首先会传给goods_user的Handler,在该Handler进行一次查询,获取全部的goods_id。而后根据goods_id查询获取全部的商品信息并以列表方式返回。这是一种最符合资源分割原则的方法,可是毫无疑问这样会大大增长数据库交互次数,不符合优化的规则。那该咋办呢,根据URFP的原则一,邻接资源之间存在且只存在ResourceID依赖关系,那咱们引入DeferredSQL的概念,即在goods_user中,我知道我要查询出什么,可是由于我不是URFP的最后一环,我只是传递一个DeferredSQL下去,而不真正的进行数据库查询操做。
另外一种状况,可能存在须要联合查询的就是URFP中描述的资源注入的状况,即存在resource_integration的状况下。实例而言,咱们在获取商品列表的时候同时也要获取商品的提供方的信息,通常状况下须要把goods表与goods_provider表联合查询,那么在RARF中一样也是基于DeferredSQL,以下所示:
DeferredSQLExecutor(DeferredSQLForQueryGoods,DeferredSQLForQueryGoodsProvider)
具体到方法论上,以Java为例,目前流行的数据库辅助框架有Hibernate与MyBatis。Hibernate是全自动的ORM框架,不过它的实体类中强调One-to-One、One-to-Many这样的依赖关系,即经过关联查询把其余资源视做某资源的一个属性,这样无形又根据逻辑把资源混杂了。另外一个是MyBatis,半自动的框架,可是其SQL语句没法自动组装,即没法自动帮你进行联合查询,仍是得本身写基本的SQL模板而后它帮你自动生成,也是没法彻底符合RARF的须要。
缩了那么多,最后,我仍是陈述下我在设计RARF一些莫名其妙的东西时候的愿景吧,其实看到如今机智的同窗,应该可以感受到,这个RARF和MicroService在不少设计理念上仍是很相似的,这里先盗用下MicroService的Benefits:
Microservices do not require teams to rewrite the whole application if they want to add new features.
Smaller codebases make maintenance easier and faster. This saves a lot of development effort and time, therefore increases overall productivity.
The parts of an application can be scaled separately and are easier to deploy.
那么改造一下,RARF的愿景就是:
RARF但愿可以在修改或者增删某些功能时不须要把所有代码过一遍
基于Resource分割的代码库会更小而且更好管理,这会大大节省开发周期,提供产品能力
整个应用程序可以独立扩展、易于部署。就像RARF中,若是发现哪一个ResourceHandler需求比较大,能够无缝扩展出去。
估计这篇文章也没啥人愿意看吧,不过若是哪位大神也有一样相似的思考的欢迎加QQ384924552,能够一块儿讨论讨论。