RARF
Why RARF?
本文仅代表个人思考,若有谬误请付之一笑。若能指教一二则感激不尽。另外本文所写是笔者个人的思考,暂未发现有类似的工作。不过估计按照笔者的智商可能世界上已经有很多大神早就解决了笔者考虑的问题,若有先行者也不吝赐教。
Make everything as simple as possible, but not simpler —
Albert Einstein 把每件事,做简单到极致,但又不过于简单- 阿尔伯特· 爱因斯坦
在文章之初,笔者想交代下自己的背景,毕竟下文的很多考虑和思想都是来源于笔者所处的一个环境而产生的,笔者处于一个创业初期产品需求业务需求急速变更的小型技术团队。一般而言软件工程的基本步骤都会是先定义好需求,定义好数据存储,定义好接口规范与用户界面原型,但是笔者所在的环境下往往变更的速度已经超过了程序猿码字的速度
- RESTful
- MicroService
- Reactive Programming
- Redux
在进行具体的表述之前,笔者想先把一些总结的内容说下,首先是几个前提吧:
-
不要相信产品经理说的需求永远不变了这种话,too young,too naive
-
在快速迭代的情况下,现有的后台接口僵化、代码混乱以及整体可变性与可维护性变差,不能仅仅依赖于提高程序猿的代码水平、编写大量复杂的接口说明文档或者指望在项目开始之初即能定下所有接口与逻辑。目前的后端开发中流行的
MVC 架构也负有一定的责任,我们的目标是希望能够寻找出在最差的人员条件下也能有最好结果的方式。或者描述为无论程序猿水平如何,只要遵循几条基本原则,即可构造高可用逻辑架构,并且这些规则具有高度抽象性而与具体的业务逻辑无任何关系。 -
任何一个复杂的业务逻辑都可以表示为对一或多种抽象资源的四种操作
(GET 、POST、PUT、DELETE) 的组合。
Motivation
面向用户的接口可读性与可用性( 可组合性) 的提升
为啥笔者一直在强调自己是个前端工程师,就是在思考
接口的可读性
[GET] http://api.com/books
[GET] http://api.com/book/1
[POST] http://api.com/book
这种资源化的接口可读性还是比较好的,通过动词与资源名的搭配很容易就能知道该接口描述的业务逻辑是啥。不过,在笔者浅薄的认知里,包括
http://api.com/doBookBuy
对于单个接口而言,可读性貌似上去了,但是,这最终又会导致接口整体命名的混乱性。确实可以通过统一的规划定义来解决,但是笔者个人认为这并没有从接口风格本身来解决这个问题,还是依赖于项目经理或者程序猿的能力。笔者在
前端定制的接口(Frontend-Defined APIs And URFP-Driven)
无论是



那么现在后端要提供接口了,如果按照逻辑分割的要求,最好是提供三个接口:
/activities ->获取所有的活动列表
/activity/1 ->根据某个活动ID获取活动详情,包含了该活动的商品列表
/goods/2 ->根据商品编号获取商品详情
这样三个原子化的接口是如此的清晰好用,然后后端就会被
逻辑处理函数中的状态管理
在目前的前端开发中,状态管理这个概念很流行,笔者本身在前端的实践中也是深感其的必要性。在前端中,根据用户不同的交互会有不同的响应,而对于这些响应的管理即是一个很大的问题。笔者在这里是借鉴的
RequestFilter();//对于请求过滤与权限审核
a = ServiceA();//根据逻辑需要调用服务A,并且获取数据a
if(a=1){
b = ServiceB(a);//根据上一步得到的结果,调用某个特定的Service代码,注意,数据存取操作被封装在了Service中
}else{
b = ServiceC(a);//否则就调用
}
c = ServiceD(b)
put c
状态混乱
目前很多的a,b,c
。笔者认为的状态管理的结果,就是在一个逻辑处理的流程中,不需要看前
ArrayList<Integer> array = new ArrayList();
Service(array);//在Service中向array里添加了数据
而这样一种
抽象混乱
[GET]:/goodss -> 映射到getGoodsController
在编写这个selectGoodsAllService
的服务方法,来帮助我们进行数据查询。然后,又多了一个需求,查询所有价格小于
[GET]:/goods?query={"price":"lt 10"} -> 映射到getGoodsController
这时候我们就需要在
[GET]:/getGoodsByPriceLess?price=10 -> getGoodsByPriceLessController
同样需要编写一个根据价格查询的
依赖混乱
上图描述了一个请求处理过程中的依赖传递问题,直观的感受是,当我们的业务逻辑系统变得日渐复杂之后,不依赖getMinorsService()
,这个服务不用传入任何参数,直接调用就好。然后某天,有个新的需求,查找所有的没到getPeopleYoungerThan20()
。这样系统中就多出了大量的功能交叉的服务,而这些服务之间也可能互相依赖,形成一个复杂的依赖网络。如果有一天,你要把数据库的表结构动一下
说到依赖,相信很多人首先想到的就是
最后,笔者在
挣脱数据库表结构/ 文档结构的束缚
在富客户端发展的大潮之下,服务端越来越像一个提供前端进行selectGoodsByGoodsId
这个
如果你看到这还有兴趣的话,可以看看下面笔者的思考。
Solution( 解决方案)
关于抽象资源
万物皆抽象资源
all is resource
的概念,但是
资源的定义
在讲资源的定义之前,首先看看关系型数据库中经典的设计范式:
第一范式
( 确保每列保持原子性) 第二范式(2NF) 属性完全依赖于主键( 消除部分子函数依赖) 第三范式(3NF) 属性不依赖于其它非主属性[ 消除传递依赖]
对于从具体的业务逻辑抽象出相互分割并且关联的资源是
- 万物皆资源,资源皆平等。
- 每个资源具有唯一的不可重复的命名。
- 任何资源具有一个唯一的标识,即
{resource_name_id} 在所有表中必须保持一致。譬如我们定义了一个资源叫user ,那么它的标识就是user_id ,不可以叫uid 、userId 等等其他名称。 - 任何资源的属性由
{resource_name_attribute_name} 构成,且遵循第二与第三范式。
这一套命名规则,有点像乌托邦吧,毕竟作为一个不成熟的想法,
资源的操作:GET 、POST、PUT、DELETE + ResourceHandler
到这里为止,我们其实是针对
/book/1
这种逻辑的处理进行了分析,还是对于单个资源的,在实际的业务场景中,我们往往是对一或多个资源的组合操作。
业务逻辑与资源流动: 用URFP 来描述业务逻辑,去Controller
学过数学的都知道,两点确定一条直线,而在后端逻辑开发中,当你的
/doGoodsOrderCreate?goods_id=1
如果是
POST:/goods_order?goods_id=1&...
于是我自己想,按照
POST:/goods/{goods_id}/goods_order?requestData={requestData}
这一套
资源转化
原则:所有
/user/1/goods_favorite/all/goods
当
资源注入
资源注入的应用场景可以描述如下resource_integration
关键字,譬如,我们的这个请求可以这么写:
/goods?requestData={"resource_integration":["goods_provider"]}
那么在
隐性业务逻辑的处理
实际上
看到这里,如果还有兴趣的话可以看看笔者
关于数据库设计
/goods_user/all/goods
根据上文对于
DeferredSQLExecutor(DeferredSQLForQueryGoods,DeferredSQLForQueryGoodsProvider)
具体到方法论上,以
Pursuit( 愿景)
缩了那么多,最后,我还是陈述下我在设计
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 希望能够在修改或者增删某些功能时不需要把全部代码过一遍- 基于
Resource 分割的代码库会更小并且更好管理,这会大大节省开发周期,提供产品能力 - 整个应用程序能够独立扩展、易于部署。就像
RARF 中,如果发现哪个ResourceHandler 需求比较大,可以无缝扩展出去。
估计这篇文章也没啥人愿意看吧,不过如果哪位大神也有同样类似的思考的欢迎加
Introduction
AARF 是适用于现代复杂多变的业务模型,同时支持异步编程与分布式扩展的架构风格,Inspired By REST,Flux And Functional Reactive Programming。
。很多的看上去很傻逼的错误,我们可能寄希望于程序员开发时候的标准的规范。简单来说,我们可能在
为了方便理解,我们也可以借用函数柯里化与反柯里化的概念。我们提供一系列适用范围广,但是适用性较低的接口来代替原先适用性较高,但是适用范围极低的接口。而柯里化的这一步即交由前端来完成,也就是下文中会提及的
笔者的联系方式:QQ(384924552),Mail(384924552@qq.com)
Why AARF?( 为什么是AARF)
AARF 代表着在后端开发中从面向逻辑到面向资源,从以业务/ 页面为驱动到以状态/ 数据为驱动的设计理念的变化。
目前流行的复合
在
另一方面,以
在对大量复杂的业务后台的分析之后,发现如果我们深入到某个具体的业务流设计中,会发现不同的
这些所谓的原则还处于修订中,不建议用于生产环境
PS:上面这句话只是单纯的为了装逼
- 约定优于配置,配置优于硬编码,这一条算是
AARF 的核心理念,一方面表现在前端可见的资源组合搭配上,另一方面表现在对于隐性
- 请求与响应的一致性。就好像
Redux 宣称的Predictable State 一样,之前客户端是在请求之后对于返回数据进行 - 尽可能地资源隔离。不强求资源的完全隔离,也不支持把对于资源的处理混杂在一起。至少在
ResourceHandler 这一层,不建议有对于其他资源的操作。在Model 层,可以对于确定性的关联操作。在Relation 层,可以随便混杂资源的操作。
举个具体的例子,在我们的电商模型中,我们最常用的接口是返回商品
api.com/goods
这会根据我们的需要自动地返回一些商品列表,如果我们需要根据不同品类、属性等进行查询,则只需要加一个查询参数等,譬如:
api.com/goods?requestData={category:1}&sort=rating
随着业务需求的进一步完善,我们现在需要返回用户的订单商品信息,这里我们假设有一个专门的订单表
api.com/getGoodsInOrderByUserId?user_id=123
而如果按照
api.com/goods_order?requestData={user_id:123}
注意,无论是哪种
当然,我们可以在该处理函数中用一个简单的
api.com/getGoodsInCartByUserId?user_id=123
api.com/getGoodsInFavoriteByUserId?user_id=123
当然,也可以用等效的
不过真实情况却是,即使是自己也不乐意去看那一大堆好久不碰的代码,或者换了个新人,压根不愿意去看前辈的代码
以上的方式就是面向业务逻辑的接口,我们有一个固定的
api.com/getGoodsThemeAndGoodsList?goods_theme_id=1
当前端请求该接口时,就会返回如下格式的数据:
{
"goods_theme":{goods_theme_detail}
"goodss":[{goods_detail}]
}
绝大部分人都会把主题活动和商品当做两个存在一定关联的不同资源吧,不会认为这两货是同一个东西的吧?如果按照第一种方式,一旦设计同学修正了前端页面,譬如不展示商品列表详情了,或者后台的商品表发生变化,那么整个接口就要改或者添加新的接口。与接口相关联的
如果我们按照纯粹的资源的方式考虑呢,即完全的
api.com/goods_theme/1
获得一系列的商品列表
api.com/goods/[1,2,3,4,5]
这种划分方式,可以达成逻辑上的明晰,不过前端估计会疯掉。并且用户体验会非常不好,同时也会耗费大量无谓的资源。
综上所述,我们需要是是一种抽象的,即独立于具体业务逻辑的资源划分与处理方式,这种方式能够灵活组合并且适应快速变化的前端界面与业务需求,最终在前端的请求次数与逻辑的划分之间达成较好的平衡。
而
个人感觉,
MicroServices( 微服务)
近年来微服务概念的兴起也是为了进行这样的解耦合,只是微服务和
- 微服务和
AARF 在某种意义上都是推崇SRP 原则,不过微服务是从传统的SOA 架构衍变而来,而AARF 是借鉴了以状态/ 数据为驱动的这样一种开发方式。 - 微服务更多的是在功能上的,面向于整体后台架构的解耦合。而
AARF 关注的是偏向于业务逻辑的组织方式。 - 微服务往往强调的是不同功能间的物理隔离,这是其与传统的巨石
(Monolith) 应用程序的一个很大的区别。而AARF 依赖的是逻辑间的隔离,相对于功能领域会是一个更加抽象的概念。 - 微服务是去除了
ESB 的SOA ,即一种去中心化的分布式软件架构。而AARF 面向每个具体业务而言的去Controller 化的基于抽象资源流的架构风格。
实际上,现在也有很多微服务的设计模式上,在保证隔离性的基础上,以


REST & Software Architecture Style
在理想的情况下,每个开发者都能具有高度一致的编码风格与能力,并且对整个项目了如指掌,这样才能够快速地无冗余地根据业务需求来修改代码。并且辅助以大量的回归测试来保证代码的可用性,毕竟
Request per thread is an old beast and should be avoided as much as possible. It not only affects an application’s scalability but can also lead to cascading failures.
Flux & Redux
Reactive Programming & Actor
在笔者的基于
Reactive programming is an emerging discipline which combines concurrency and event-based and asynchronous systems.Reactive programming can be seen as a natural extension of higher-order functional programming to concurrent systems that deal with distributed state by coordinating and orchestrating asynchronous data streams exchanged by actors.
Reactive Programming Principles (via manifesto)
- Responsive: The application should be quick to reacts to users, even under load and in the presence of failures
- Resilient and Scalable: The application should be resilient, in order to stay responsive under various conditions. They also should react to changes in the input rate by increasing or decreasing the resources allocated to service these inputs. Today’s applications have more integration complexity, as they are composed of multiple applications.
- Message Driven: A message-drivenarchitecture is the foundation of scalable, resilient, and ultimately responsive systems.
参考文献
What’s AARF?( 什么是AARF)
AARF is an architectural style, it’s not only specific set of technologies, but a design concept throughout the develop stack.
笔者将
AARF 称为一种架构风格,它不仅仅是一系列具体的技术的集合,更重要的是一种贯穿开发全栈的设计理念。AARF 继承并个性化地实现了REST 的基本思想,借鉴了Flux 以及Redux 的单向数据流的思想,最终使用Reactive Programming 作为实现手段。
最直观的感受上,传统的
后端开发不应该再根据前端的某个需求编写相应的
URI 与处理函数,而应该从自身资源分割的视角,为前端提供一系列原子操作,并且在保证权限控制与数据验证的基础上允许前端自由组合
对于
Twelve-Factors & Cloud Native Principles
AARF Principles
-
Front-end Friendly
- flexibility
- readability
- Consistency
- Response comply with request
-
High Availability
- Responsive & Fault-Tolerant
-
Develop Simplicity
Modifiability
可变性包括对内的可变性与对外的可变性。
extensibility
Reusability
Reusability is a property of an application architecture if its components, connectors, or data elements can be reused, without modification, in other applications. The primary mechanisms for inducing reusability within architectural styles is reduction of coupling (knowledge of identity) between components and constraining the generality of component interfaces. The uniform pipe-and-filter style exemplifies these types of constraints.
-
Configurability
Configurability is related to both extensibility and reusability in that it refers to post-deployment modification of components, or configurations of components, such that they are capable of using a new service or data element type. The pipe-and-filter and code-on-demand styles are two examples that induce configurability of configurations and components, respectively.
-
Visibility
Styles can also influence the visibility of interactions within a network-based application by restricting interfaces via generality or providing access to monitoring. Visibility in this case refers to the ability of a component to monitor or mediate the interaction between two other components. Visibility can enable improved performance via shared caching of interactions, scalability through layered services, reliability through reflective monitoring, and security by allowing the interactions to be inspected by mediators (e.g., network firewalls). The mobile agent style is an example where the lack of visibility may lead to security concerns.
-
Scalability: 架构的可扩展性Scalability refers to the ability of the architecture to support large numbers of components, or interactions among components, within an active configuration. Scalability can be improved by simplifying components, by distributing services across many components (decentralizing the interactions), and by controlling interactions and configurations as a result of monitoring. Styles influence these factors by determining the location of application state, the extent of distribution, and the coupling between components.
Scalability is also impacted by the frequency of interactions, whether the load on a component is distributed evenly over time or occurs in peaks, whether an interaction requires guaranteed delivery or a best-effort, whether a request involves synchronous or asynchronous handling, and whether the environment is controlled or anarchic (i.e., can you trust the other components?).
Case Model( 案例模型)
- User
- Book
- Comment
Relation
FriendShip
Resource( 资源)
在传统的
任何一個
譬如
也可以来自于内部的
资源的规范性,包括分割以及命名的规范性是整个
Abstract Resource
Everything is Abstract Resource
具有独立的属性或者属性组合的逻辑对象即为抽象资源
最常见的,我们可以将商品的所有属性组合为商品资源。
而对于发邮件这个动作而言,它的属性可能有收件人、邮件正文等等,那么这些属性的组合也就可以构成邮件资源。
实际上,对于资源的定义与划分具有很大的随机性,并且不同的划分方案会极大的影响后期的组合操作。对于如何合理划分资源来保证整个系统的最优性,还需要在后面详细讨论。
Attribute( 资源属性)
- 资源唯一标识,譬如对于用户资源的唯一标识就是
user_id 。 - 外键依赖,外键依赖是表征资源之间显性关系的特征。注意,任何一个资源的标识名具有全局唯一性,譬如
use_id ,那么所有的资源中都应该叫user_id ,而不应该使用uid 、id 等等缩写或者别名。另一方面,可能某个资源中的两个外键依赖都指向user_id ,但是表示两个不同的含义。譬如如果我们需要表征用户之间的互相关注的行为,一个表中可能有两个user_id ,第一个表示关注者,第二个表示被关注者。那么在命名时务必保证前缀不变,即皆为user_id ,可以通过by 关键字添加后缀的方式,即user_id_by_following 、user_id_by_followed。 - 值属性
Entity
不是很建议资源的嵌套,即将某整个其他的资源打包成某个资源的一个属性。
建议以使用基本类型加上
String
Integer
JSONObject
JSONArray
Model
在
Select
- 利用
foreach 构造多查询的in 条件查询语句时候,注意容错
Request & Response
Uniform Resource Flow Path( 统一资源流动路径)
Resource Quantifier( 资源量词)
量词分为 单个值、多个值
all,不一定就代表着全部。譬如
/user/{user_id}/book/all/comment
这边的
换言之,所有与当前业务逻辑相关的处理,都放在
ResourceBag( 资源包)
资源包是唯一可以跨资源流动的对象
Immutable Object( 不可变对象)
笔者觉得,没必要为每个
Advanced Request
Resource Filter
包括条件查找、聚合操作、筛选操作
如果是聚合操作:aggreation
Resource Integration( 资源注入)
MapReduce For Multiple Request Once( 单次多请求处理)
Non-Resourceful Response( 非资源化响应)
{
"code": 404, //Required
"desc": "Description Message", //Optional
"subCode": "Sub Code to specific error" //Optional
}
Code And HTTP Status( 响应码与HTTP 状态)
虽然
Empty Resource Is Error( 空资源即为错误)
ResourceHandler( 资源处理)
Dispatcher
为了保证多线程下的安全性,要保证每个
为了避免因为复杂的异步数据流而导致逻辑的混乱,做以下约定:
Get
SetResult
本步骤主要在根据
Post
Update
Delete
获取用户评价过的书
/user/{user_id}/comment/all/book
获取用户好友评价过的书
/user/[{user_id}]/comment/all/book
/user/{user_id}/comment/all/book
/user/{user_id}/book
/user/{user_id}/comment/[“1”,“2”]/book
get getAll getSingle getMultiple
post postSingle postMultiple
update updateSingle updateMultiple
delete deleteSingle deleteMultiple deleteAll
Relation( 混合关联层)
资源之间的关联一般分为两种,显性关联与隐性关联。譬如主键依赖这样的,往往称为是显性关联,一般也都会在
对于隐性关联,一般也会分为必然关联与可变关联。譬如我们往往有一个推送模块,而用户在执行某个操作之后是否发送通知,或者发送什么样的通知,这个在业务初期是不可预知的。因此在
对于某个资源的某个操作不可避免地会引起其他资源的操作,而在定义
特别是在协同项目或者后期的迭代开发中,如果以面向逻辑的方式,那么往往会选择根据新的逻辑需求添加新的功能函数,而尽可能地避免修改之前的函数。但是这种盲目地堆叠方式只会导致代码的可维护性更差,最终积重难返。这样一个问题,看起来可以通过严格的
一方面,我们希望能保证代码的简洁性,另一方面,我们又希望能保证代码的灵活性与可变性。笔者一再强调的资源的单一职责问题,并不排斥在资源的
public boolean insertSingleGoodsOrder(GoodsOrderResource.Entity goodsOrderEntity) {
return this.transactionWrapper(() -> {
//首先插入订单
goodsOrderMapper.insertSingleGoodsOrder(goodsOrderEntity);
//将资源封装到ResourceBag中
final ResourceBag resourceBag = new ResourceBag.Builder(ResourceBag.Action.GET, new ArrayList()).build();
GoodsOrderResource goodsOrderResource = new GoodsOrderResource(goodsOrderEntity);
resourceBag.getResources().put("goods_order", goodsOrderResource);
if (!this.relationProxy.proxy("goods_order", "insertSingleGoodsOrder", resourceBag).toBlocking().first()) {
//如果操作失败,则回滚
throw new Exception("附加操作失败");
}
});
}
笔者的建议是,对于必然的操作,直接放置在该功能函数中,对于可选操作,放置在
Filter/Interceptor
Authority Control( 权限控制)
JWT Based User Authentic( 用户认证)
笔者在最初设计
/user/{user_token}
这样一种直接把用户令牌与用户资源的唯一标识混为一谈的方式就是典型的错误。笔者比较推出基于
基于
错误还是空?譬如用户请求一个不存在的商品,这是一个错误吗?请求一个不存在的资源,特别是在发生资源串联时,因此,在
Data Verification( 数据验证)
数据验证一个是数据格式的验证,包括
请求数据格式的验证,放在
返回数据格式的
Deferred SQL
笔者在文章开头就提及