Restful Objects 是关于领域对象模型的超媒体 API 的公共规范。该规范的 1.0.0 版本刚刚发布并提供下载,并且目前已经出现了两个实现了该规范的开源框架——一个基于Java 平台,另一个基于.NET 平台。
Restful Objects 和其他 RESTful 标准有什么区别呢——比如说和 Java 平台的 JAX-RS (JSR311) 相比较或者和.NET 平台的微软Web API 相比较?答案是其他RESTful 框架主要用来抽象纯粹的网络问题,它们并不保证所定义的和各种领域对象类型交互的资源结构的统一性。并且它们没有或者很少支持RESTful 体系中被认为最重要的原则:“将超文本作为应用程序状态的引擎”,或者说 HATEOAS 。平白地说这意味着访问系统的所有功能是可行的,只要跟随着来自资源主站的超媒体控制(链接)就行了。和 Restful Objects 意图最相近的框架或许就是 OData 。但两者间还是存在很大差别的,Odata 主要专注于 CRUD(Create, Read, Update, Delete)功能,而 Restful Objects 提供了访问对象所有行为(方法)的能力。
实现了 Restful Objects 规范的新框架不仅使得在领域对象模型上编写符合 HATEOAS(超媒体即应用状态引擎)标准的 API 更加容易——事实上它们消除了这些工作量!你可以用简单的词法编写领域对象模型——按传统的 Java 或 C#对象编写——然后在几分钟之内在其上创建完整的 RESTful API。
另外,因为这些框架提供的资源和表述(representations)符合公共规范,这意味着所编写的客户端如果能和运行其中一种框架的服务器交互,那么就可以和运行另一种框架的服务器交互。我们希望在接下来的几个月里能看到更多符合 Restful Objects 规范的服务端实现(我们已经知道至少有一个第三方已计划这么做)。同时,我们也已看到使用 Restful Objects API 的标准客户端框架逐渐涌现。这些我们后面再谈。
可应用性和优势
Restful Objects 规范最有可能的受众是采用领域驱动的开发者,已经实现或者正在实现领域对象模型的开发者,以及那些想在该模型上提供 RESTful API 之类特性的人员。我们自己的领域驱动开发(DDD)经验包括了非常大范围的领域对象模型设计 [1, 4, 5],包括了被封装为领域实体上的方法的大多数行为。我们希望能使用 Restful Objects 来更深入地挖掘领域模型资产。
除了能巨大地减少工作量,使用实现了 Restful Objects 规范的框架还有另外几项优势:
- 测试。所有业务逻辑都只在领域类型中表示,因此可以在内存中快速而经济地将它们进行单元测试。相对的,如果 RESTful API 是根据特定用例自定义编写的,则只有通过跨服务器的(缓慢的)集成测试才能有效地将其进行验证。
- 文档。Restful Objects 是由领域对象模型所驱动的,因此可以使用已有的成熟技术(例如 UML 或者简单的 Javadoc/Sandcastle)将其文档化。这比创建一些新的标记以显示地文档化手工编写的 RESTful API 更加可取。
在继续深入之前,我们想说 Restful Objects 是和领域对象模型的语法不相关的。所以,不管你是认为领域对象应该表示具有业务逻辑的实体,还是认为应该有单独的“资源模型”以表示用例实体,在这两种情况下 Restful Objects 规范都同样适用。为了简单明了,在本文中我们会优先使用和前者(领域对象作为实体)相关的例子,但在本文的后面我们还会继续讨论这个主题。
资源
在 Restful Objects 规范中每个领域对象就是一项资源;例如下面的 URI 指向 Id 为 31 的客户:
~/objects/customers/31
其中 ~ 是服务器地址(比如http://myserver.com
),customers
定义了对象的类型,而31
则是该类型的“实体标识”。该规范并没有定义标识的格式,任意的唯一字符串都是可以的。为什么需要/objects
呢?这是为了和其他顶级资源区分开了,包括services
(例如 repositories 和 factories 之类具有方法但没有属性的单件对象)以及domain-types
(类型元数据);这些顶级资源都具有各自的子资源结构。
每个对象资源都具有一些子资源以表示该对象的成员。所以:
~/objects/customers/31/properties/dateOfBirth
指向一个特定客户的生日,而:
~/objects/customers/31/collections/orders
指向和该客户相关联的订单的集合。下面的表格显示了和各种 HTTP 方法相对应的对象、属性以及集合资源。如果不能赋予某个 HTTP 方法有效的意义,则指定为 405 错误。
对象
资源
对象
属性资源
对象
集合资源
GET
对象概述
属性详细及值
集合详细及内容
PUT
更新或清除多个属性值
更新或清除值
添加对象(如果集合具有 Set 语义)
DELETE
删除对象
清除值
从集合中移除对象
POST
n/a - 405
n/a - 405
添加对象(如果集合具有 List 语义)
以上的分析并非什么创新的东西——很多预定的 RESTful API 都具有类似的特性。但 Restful Objects 规范也算开辟了一片新天地,在对象的“action”上应用了同样的方式——“action”是指可以通过 RESTful 接口访问的对象上的方法。扩展些说,在其他设计(例如 [2])中这些问题已有解决方案,通常是将对 action 的调用映射到 HTTP POST 上。我们的目标使我们将提供对象的 action 信息(这需要参数集合)和实际调用 action 区分开来。因此,URL:
~/objects/customers/31/actions/lastOrder
描述了获取 Id 为 31 的客户的最后一条订单的 action,而:
~/objects/customers/31/actions/lastOrder/invoke
描述了调用该 action 的资源。
同时,我们意识到,要求所有 action 都通过 POST 进行调用并不是使用 HTTP 的正确方式:比如幂等 action 或者仅仅用于获取其他对象的查询 action(有时被描述为“没有副作用的(side-effect free)”)还要 POST 吗?下面的表格描述了我们如何应对这些问题:
对象 Action 资源
(信息)
对象 Action 调用资源
GET
action 的详细信息(比如调用的参数和 HTTP 方法)。
调用——如果 action 被认为是只读的(没有副作用)
PUT
n/a - 405
调用——如果 action 被认为是幂等的,但不是只读的。
DELETE
n/a - 405
n/a - 405
POST
n/a - 405
调用被认为是非幂等且非只读的 action
在一些领域模型中,大多数行为(业务逻辑)是通过服务的形式实现的,其中领域实体仅仅是作为数据结构在服务的 action 上双向传递。Restful Objects 规范也适用于这种模式;唯一的小区别是 URL 被用来标识服务实体而不是领域对象实体。与此同时,该规范也适用于在“行为复杂(behaviourally-rich)”的领域对象上以方法实现大多数行为的模式;这种模式下,服务仅仅是作为提供对象访问(查找已存在的对象或者是创建新对象)的次要角色。
表述(Representation)
每个资源返回一个表述;Restful Objects 规范将这些表述定义为 JSON(JavaScript Object Notation)格式。对于使用 PUT 或者 POST 方法访问的资源,在请求体中也必须提供表述,以描述要怎样修改资源。
规范定义了若干个主要表述:
object
(代表任意领域对象或服务)list
(访问其他对象的链接)property
collection
action
action result
(一般包含一个object
或者list
,或者只是反馈信息)
同时,规范还描述了几个次要表述,比如home
和user
等。每个表述都具有一个正式媒体类型,其包含在 HTTPContent-Type
头部中。对于领域对象而言,其具有以下格式:
application/json;profile="urn:org.restfulobjects:repr-types/object"; x-ro-domain-type="http://~/domain-types/customer"
正式地,这是具有两个可选参数profile
和x-ro-domain-type
的application/json
类型。这两个参数为客户端提供了额外语义,可以想象成将其中一个层叠在另一个之上。在最底层,application/json
仅仅告诉客户端负载为 JSON 格式。对于某些客户端而言,比如浏览器的开发者插件,这就是所需的所有信息了。在上面一层,profile
参数指明该表述是领域对象形式的。最后,x-ro-domain-type
参数指明对象的类型:在本例中为Customer
。客户端可以使用这个参数来自定义用户界面,或者校验返回的表述是否为所期望的。另外要注意的是,该规范使用.x-ro-
前缀以便在没有现行标准或者草案标准可以利用时将命名空间冲突最小化。这种格式必然产生了一些自定义参数和查询字符串;然而 Restful Objects_ 没有 _ 专门自定义 HTTP 头部。
链接
与 HATEOAS 原则一致,每个表述包含了到其他资源的链接,而每个链接具有rel
参数以定义关系的性质。该规范重用了几个 IANA 定义的rel
值(例如: self、up、describedby
),另外还加上了几个自定义的 rel 值。请看下面的例子:
urn:org.RESTfulobjects:rels/details;action="placeOrder"
定义了一个在对象表述中指向该对象的 action 资源的链接。前缀:
urn:org.restfulobjects:rels/details
对一般客户端而言用其已够用了,而额外的参数 action——在本例中其指明了该链接指向placeOrder
action——也许对定制的客户端有用。
综上所述,下面的表格描述了通过资源集合完成用户目标的典型流程。
描述
方法
URL
主体
返回的
表述
跳到主页
GET
http://~/
主页
链接到提供的服务列表
GET
http://~/services
列表(服务的链接集合)
链接到产品库存服务
GET
http://~/services/ProductRepository/
服务
链接到‘Find By Name’ action
GET
http://~/services/ProductRepository/
actions/FindByName
action(在界面上以对话框呈现)
调用(只读)action 并传递参数"cycle"
GET
http://~/services/ProductRepository/
actions/FindByName/`` invoke/?Name=cycle
action 结果,包含匹配 Product 对象的链接列表
链接到集合中的一个 Product 对象
GET
http://~/objects/product/8071
对象(代表一个产品)
调用对象上的(不带参数)‘AddToBasket’action
POST
http://~/objects/product/1234/<br></br> actions/AddToBasket/invoke
调用 BasketService 上的‘ViewBasket…’action
GET
http://~/services/BasketService/<br></br> actions/ViewBasketForCurrentUser/<br></br> invoke
action 结果:包含指向 Item 对象的链接的列表
修改刚新增的 Item 上的 Quantity 属性
PUT
http://~/objects/orderitem/1234/<br></br> properties/Quantity
属性名及其值 3
从购物篮中删除(之前添加的)一个 Item
DELETE
http://~/objects/orderitem/517023
服务端实现
前面我们提到两个实现了 Restful Objects 规范的独立开源框架。其中, Restful Objects for .NET 完整实现了规范,但其目前还处于 beta 版本,因为它使用了 Microsoft Web API 框架(ASP.NET MVC4 的一部分——在写本文时 MVC4 正处于‘RC’阶段)。第二个框架 Restful Objects for Isis 运行于 Java 平台之上;该框架已经可以使用,但其目前仅实现了 Restful Objects 规范的前期草案,在正式发布前还需要继续开发和测试。我们一直在积极地参与这两个框架的开发。
使用这两个框架,你能够根据领域对象模型分别编写 POCOs 和 POJOs 代码,然后创建完整的符合 Restful Objects 规范的 RESTful API,而不用编写任何其他深入的代码。这个录制的在线视频(使用.NET 框架)演示了上述工作如何在仅仅几分钟内就可以完成。
这是可能的,因为这两个框架都是建立在实现了 naked objects 模式——根据领域对象模型利用反射自动创建面向对象的用户界面,并(默认)提供用户活动的公共方法——的现行框架之上的。新的 Restful Objects 框架以相似的方式反射领域对象模型,但以 RESTful API 的形式呈现对象的功能,而不是以用户界面的形式。两个新的框架都将反射、对象持久以及其他横向关注点(cross-cutting concerns)的职责委托给了已有框架(分别为 Naked Objects for .NET 以及 Apache Isis )。
上述的新框架能够识别一些简单的领域对象代码规范以及标示法(在.NET 中为‘attributes’)。例如:对象上的任何公有方法都默认会在 Restful Objects API 中以 action 提供出来,但允许通过将方法标示为Hidden
以重写。如果某个对象定义了公有方法foo([params])
和另一个公有方法validateFoo([params])
,则后者会被认为是用来在前者执行前为传递给前者的参数提供验证逻辑的。
这两个框架还提供了细粒度的基于用户身份和 / 或角色的授权机制。对于给定的领域类型,如果用户没有被授权查看某个给定的属性、集合或者活动(action),则在相应的表述中指向该对象成员的链接就永远不会呈现给该客户。当用户试图通过直接构造指向该资源的 URL 以进行访问时,他们将接收到 404 错误;而如果用户拥有查看该对象成员的权限,但没有编辑的权限,则当试图进行编辑时就会接收到 403 错误。
Restful Objects for .NET 框架的源代码可以从 Codeplex 网站下载或者以 NuGet 的形式安装;Restful Objects for Isis 框架的源代码可以以源代码的形式下载或者使用 Maven 原型安装。
对于已经使用过 Naked Objects for .NET 或者 Naked Objects for Apache Isis 的开发者,使用新的框架意味着他们能够在几分钟之内(形象地说)在他们已有的领域模型上创建 RESTful API。但我们的意图是将新框架推荐给没有 naked objects 模式知识同时也对其不感兴趣的开发者,事实上我们已经看到这些开发者逐渐感兴趣了。
客户端
目前我们大部分开发工作都聚焦在服务器端——根据领域对象模型产生 Restful Objects API 的相关代码。不过与此同时,我们也在使用这些 API 的客户端应用程序上做了一些工作。
在其中一个例子中,客户端是一个规范的 web 应用程序(使用 ASP.NET MVC 编写),该应用中的控制器方法调用了在另一台服务器上实现的 Restful Objects API。我们希望在将来开发一个小的客户端应用程序库,用来调用 Restful Objects 服务端以及将返回的 JSON 表述转换为能够由 C#或者 Java 操纵的对象。
在另外一个例子中,我们编写的客户端是一个‘单页面应用程序’,其只由一个页面组成,仅包含了若干行静态 HTML 代码,主要由使用了 JQuery 的众多 JavaScript 函数支撑。JavaScript 负责调用服务器上的 Restful Objects API,然后将返回的结果以 HTML 在浏览器中呈现。再次,我们希望在将来能编写一个小的 JavaScript 应用程序库,专门用来消费 (consuming)Restful Objects API,计划该库主要使用 JQuery 编写——也或许由其他者接棒。
Restful Objects 规范的一般性产生了另一个可能性:存在能在不必修改的情况下通过 RESTful API 和任意领域模型交互的 _ 通用 _ 客户端。我们已获知有三个不同的开源通用客户端正处于开发当中,它们在 Restful Objects 网站上被罗列出来了。它们都是单页面应用程序,使用已有的程序库和 JavaScript 编写。这些通用客户端中的 JavaScript 代码也能被作为创建一个或多个自定义客户端的基础。这三个通用客户端在用户交互风格上差别很大。下面展示了其中之一的截图——由 Adam Howard 开发的 Restful Objects 工作空间(AROW):
(点击图片以放大浏览)
以牙还牙
通过Restful API 暴露领域实体的想法受到了很多争议。有些评论者认为这是一个糟糕的想法,认为它产生了安全漏洞;其他一些人甚至认为不可能通过该方式创建真正的Restful API。现在让我们来详细审视下这些争论。
Rickard Öberg声称将领域实体作为资源暴露出来“不可能是HATEOAS 的,因为不存在在资源之间创建链接以暴露应用程序状态的合理方式”[3]。其实显然不是这样的:Restful Objects 将实体作为资源暴露出来是完全HATEOAS 的,同时对链接来说也是相当合理的。
Jim Webber声称即使这是可能实现的,也还是一个糟糕的创意,因为这导致了客户端和服务端的紧耦合,然而在Restful 体系中,客户端和服务端是应该能够独立改进的[6]。他以及其他人争论说应该只暴露视图模型和/ 或表示用例的对象,这两者都应该是版本化的,它们能使客户端不受领域模型变化的影响。
我们认为这些论点即使不是完全错误的,也是不分清红皂白的。我们认为真实的情况和他们所说的有很多微妙的差别:在一些场景下这些论点是有效的,但在大多数场景下则并不是。我们需要考虑到以下两个因素:
第一个因素是:客户端和服务端是否都在同一方的控制之下。对于可公共访问的Restful API——比如Twitter 或者Amazon 的Restful API——也就意味着服务端和客户端分别属于不同的独立组织,在这种情况下我们同意暴露领域实体不是好的方式。但是,在这种情况下,Restful Objects 也能够完美地跟视图模型和/ 或用例对象协同,这些模式在规范中有详细的描述。
反过来,事实上在很多潜在的Restful API 用例中,客户端和服务端的变化发展是由同一方控制的——比如主要在内部使用的企业应用程序。在这些情况下,暴露领域实体不仅安全,而且还是个好创意。这些属于‘主权’系统(此概念由 Alan Cooper 提出)一类的应用程序,相对于面向公众的(或者‘短暂交互的’)应用来说,特别需要赋予用户访问更大范围数据和功能的权利。
第二个因素是:客户端是‘定制的’(专门用于和特定的领域模型协同工作)还是‘通用的’(能在不必修改的前提下和任意领域模型协同工作)。目前,不管是在内部网络中还是在开放的因特网中,几乎所有消费 Restful API 的客户端都是定制的——所以我们需要将它们同领域模型的变化隔离开来。但如果存在能够自动响应领域模型变化的通用客户端,则不需要这样做了。目前,很少有人关注通用客户端的可能性,因为没有综合的标准来指导怎样创建这样的应用程序。我们认为,事实上通用客户端的概念相比定制客户端的概念更加符合 REST 的精神(以及 REST 的语义)。要知道,网页浏览器就是在一定层级上访问 restful 接口的通用客户端,其工作于文档层级之上。而 Restful Objects 使得工作于更高一层抽象(即领域对象模型)的通用客户端的概念成为可能。
把这两个因素描绘成 2x2 的分析表,如下面所示,你可以看到如果你需要应对内部网络应用程序和 / 或通用客户端,则暴露通过 RESTful API 领域实体是安全和有效的。只有当你需要应对公开的因特网应用程序 _ 且是 _ 定制的客户端时才需要关心以下建议:领域实体应该用视图模型和 / 或用例对象掩藏起来,以将客户端和服务端的变化隔离开来。
部署环境:
客户端形式:
内部网
因特网
通用
可以暴露领域实体
可以暴露领域实体
定制
可以暴露领域实体
只能暴露版本化的视图模型和 / 或用例对象
再强调一遍:Restful Objects 在该表格中的任意位置都同样适用。我们认为,目前右下角受到最多关注的事实更多地反映了构建定制的 Restful API 的复杂性,而不是反映了固有的局限性。另一个常见的谬论——认为 Restful API 只能在小的严格定义的状态转换表之上创建——进一步证明了很多人的思维狭隘地集中在右下角的单元格中。
Öberg 进一步争论,因为授权问题,以 RESTful API 暴露领域实体的方式不适用于公共应用程序,并举例:访问重置密码的 action 可以由角色管控,但修改密码的 action 必须限定在某个具体的用户。以上面讨论的两个 Restful Objects 框架实现来说,这个特例对代码的影响事实上是微不足道的。我们认为能阐明他的论点的更好的例子是:访问某个特定的实体对象,而不是某个方法。这是面向公众的系统的常见需求:比如用户必须能够访问他们自己的购物篮,但不能通过猜测 URL 访问任意其他人的购物篮。
虽然目前上述的两个 Restful Objects 实现 _ 本身 _ 都没有支持基于实体的授权,但该问题存在完全可行的解决方案。在前面我们已经谈到,Restful Objects 规范中没有指定 URL 中实体标识符的形式。另外,URL 中实体标识符还可以被服务端加密,比如使用会话产生的私钥。调用服务的 action 资源以返回我的购物篮,可以使用一个对象表述,其“自身”加密的链接是:
~/objects/baskets/xJDgTljGjyAAOmvBzIci9g
接着,该购物篮上的 Total 属性对应的 URL 是:
~/objects/baskets/xJDgTljGjyAAOmvBzIci9g/properties/Total
所以,在这种情况下仍然可以直接了当地解析资源标识符,但要直接获取另一个购物篮就很可能不行了。这显然不够美观,但值得一提的是 REST_ 不是 _ 用来创建‘美观’的链接的。就像 Tim Berners-Lee 所强调的,除了服务器地址之外,应该以不透明的形式对待URL ;"rel"的值才是关键。这是HATEOAS 的其中之一点。
在本文所论述的两个Restful Objects 框架中,我们计划让用session-key 加密的实体标识符成为一个可配置项。
结论
目前,在大型企业系统上构建RESTful API 是一项非常昂贵的活动,从术语角度说,这需要在设计、开发、测试和文档上花费大量资源。而使用实现了Restful Objects 规范的框架意味着所有工作会变得微不足道,这让开发者能够将精力集中在最需要的地方——领域对象模型。
另外,Restful Objects 规范所激励的资源和表述的标准化为客户端应用程序库开启了巨大机会,这样的程序库可以和任意的Restful Objects 服务协同工作,若是完全通用的客户端,则可以和任意符合标准的应用交互。
我们希望读者能深究Restful Objects 规范以及相关框架实现,同时,如果有人有兴致于编写新的符合Restful Objects 规范的开源框架或程序库,我们将乐于倾听他们的声音。
参考
[1] Haywood, D, “Domain-driven design using Naked Objects”, 2009, Pragmatic Bookshelf
[2] Masse, M. “REST API Design Rulebook”, 2011, O’Reilly
[3] Öberg, R, “The Domain Model as REST Anti-pattern”
[4] Pawson, R. “Case Study: Large-scale pure OO at the Irish Government”, QCon London 2011 presentation
[5] Pawson, R, “Naked Objects”, 2004, PhD thesis, Trinity College, Dublin.
[6] Webber, J, “Rest and DDD”.
关于作者
Richard Pawson拥有 35 年的 IT 经验——据说是欧洲最先使用微软的编程语言编写和执行应用程序的先驱之一。除了从事过先进的机器人技术、玩具设计和技术报刊三方面的工作,他在计算机科学技术企业当任了 14 年的研究主管。他 2004 年的博士论文是关于‘Naked Objects’架构模式的完全指南,也是从那时起他担任了 Naked Objects 项目组的主管。Richard 住在英国的 Henley-on-Thames,他使用 Christopher Alexander 的模式语言(A Pattern Language)设计了他的房子。 Dan Haywood是一名自由顾问、开发者、作家和培训老师,专注于领域驱动设计、敏捷开发以及 Java 和.NET 企业架构。他是一位出名的 Naked Objects 倡导者,也是 Apache Isis 项目的主要参与者。另外,Dan 还是 Restful Objects 规范的作者,并且创作了几本有关 DDD 和 OO 的书籍。他住在英国牛津大学附近。
查看英文原文: Introducing: Restful Objects
感谢侯伯薇对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论