REST
在过去几年里,REST成为了一种被广泛使用,甚至被滥用的架构流行语。和SOA一样,不同的人对于REST有不同的理解。有人认为REST就是使用HTTP来直接发送XML的,但并不采用SOAP规范;还有人采用相似的方法来解释道:REST就是用HTTP来发送JSON数据的(刚开始我也这样认为);还有人则认为在使用REST时我们需要将URI查询参数传递给方法。以上所有对于REST的解释都是错误的。但和SOA不同的的是,Roy T.Fielding在他的博士论文中对REST做出了权威的概念定义。
REST是一种架构风格
在使用REST之前,我们首先需要理解什么是架构风格。架构风格之于架构就像设计模式之于设计一样。它将不同架构实现所共有的东西抽象出来,使得我们在谈及到架构时不至于陷入技术细节中。分布式系统架构存在多种架构风格,包括客户端-服务器架构风格和分布式对象风格。Field论文的前几个章节对有些架构风格做了解释,包括强加在每种风格上的各种约束。你可能会认为该论文中对于架构风格的解释和约束有些理论化。在这一点上,你可能是正确的。这些理论构成了Field所提出的REST架构风格的基础。REST本来就应该是属于Web架构的一种架构风格。
当然,Web——体现为URI、HTTP和HTML——先于Field的博士论文而出现。但是,Field是制定HTTP1.1标准的主要贡献者之一,他对Web在发展过程中的设计决策也产生过巨大的影响。这样看来,REST是对Web架构的理论扩展。
那么现在,我们为什么将REST作为构建系统的另一种方式呢?或者更严格地说,是构建Web服务的一种方式。原因在于,和其他技术一样,我们可以通过不同的方式来使用Web协议。有些使用方式符合设计者的初衷,而有些就不见得了。比如,关系型数据库管理系统便是一例。你可以根据原本的架构风格来使用RDBMS,即定义不同的数据库表,再定义不同的列、外键关联、视图和约束等。你也可以只创建一张表,其中只含有两列,一列为表示“键”,一列表示“值”,然后将序列化之后的对象保存在值列中。此时,你依然在使用RDBMS,但是你却使用不到多少RDMBS提供的功能,比如查询、组合、排序和分组等。
同样的道理,Web协议既可以按照它原先的设计初衷为人所用——此时便是一种遵循REST架构风格的方式——也可以通过一种不遵循其设计初衷的方式为人所用。因此,在我们没有获得(或可以说成是“不需要”)由使用“REST”风格的HTTP所带来的好处时,另一种不同的分布式系统架构可能是合适的,就像在保存拥有唯一键的数值时,NoSQL/键值对存储方式是一种更好的选择一样。
RESTful HTTP服务器的关键方面
那么,对于采用“RESTful HTTP”的分布式系统来说,它具有哪些关键方面呢?让我们先来看看服务器端。请注意,在我们讨论服务器端时,无论客户是操作Web浏览器的某个人,还是由编程言开发的客户端程序,对它们都是同等处理的,没有什么区别。
首先,就像其名字所指出的,资源是关键的概念。作为一个系统设计者,你决定哪些有意义的“东西”可以暴露给外界,并且给这些“东西”一个唯一的身份标识。通常来说,每种资源都拥有一个URI,更重要的是,每个URI都需要指向某个资源——即你向外界暴露的“东西”。比如,你可能会做出这样的决定:每一个客户、产品、产品列表、搜索结果和每次对产品目录的修改都应该分别作为一种资源。资源是具有展现和状态的,这些展现的格式可能不同。客户通过资源的展现与服务器交互,格式可以为XML、JSON、HTML或二进制数据。
另一个关键方面是无状态通信,此时我们将采用具有自描述功能的消息。比如,HTTP请求便包含了服务器所需的所有信息。当然,服务器也可以使用其本身的状态来辅助通信,但是重要的是:我们不能依靠请求本身来创建一个隐式上下文环境(对话)。无状态通信保证了不同请求之间的相互独立性,这在很大程度上提高了系统的可伸缩性。
如果你将资源看作对象——这是合理的——那么你应该问问它们应该拥有什么样的接口。这个问题的答案是REST的另一个关键面,它将REST与其他架构风格区别开来。你可以调用的方法集合是固定的。每一个对象都支持相同的接口。在RESTful HTTP中,对象方法便可以表示为可以操作资源的HTTP动词,其中最重要的有GET、PUT、POST和DELETE。
虽然乍一看这些方法将会转化成CRUD操作,但是事实却并非如此。通常,我们所创建的资源并不表示任何持久化实体,而是封装了某种行为,当我们将HTTP动词应用在这些资源上时,我们实际上是在调用这些行为。在HTTP规范中,每种HTTP方法都有一个明确的定义。比如,GET方法只能用于“安全”的操作:(1)它不修改数据;(2)它总是读取数据;(3)它读取到的数据可能被缓存起来。
SOAP风格Web服务的主要推动者之一Don Box曾经说,HTTP的GET方法是分布式系统中最优化的方法。由此可知,Web之所以具有这么好的性能和可伸缩性,恰恰是得益于这种常见的HTTP GET方法。
有些HTTP方法是幂等(意味着对同一URL的多个请求应该返回同样的结果)的,即我们可以安全地对失败的请求进行重试。这些方法包括GET、PUT和DELETE等。
最后,通过使用超媒体,REST服务器的客户端可以沿着某种路径发现应用程序可能的状态变化。这就是Field在他的博士论文中所提到的HATEOAS(Hypermedia as the Engine of Application State)。简单来讲,就是单个资源并不独立存在。不同资源是相互链接在一起的。这并不意外,毕竟,这就是Web被称为Web的原因。对于服务器来说,这意味着在返回中包含对其他资源的链接,由些客户便可以通过这些链接访问到相应的资源。
RESTful HTTP客户端的关键方面
RESTful HTTP客户端可能通过两种方式在不同资源之间进行转移,一种是上面所提到的超媒体,一种是服务器端的重定向。服务器端和客户端将协同工作以动态地影响客户端的分布式行为。由于URI包含了对地址进行解引用的所有信息——包括主机名和端口——客户端可以根据超媒体链接访问到不同的应用程序,不同的主机,甚至不同公司的资源。
在理想情况下,REST客户端将从单个众所周知的URI开始访问,然后通过超媒体链接继续访问不同的资源。这和Web浏览器显示HTML页面是一样的,HTML中包含了各种链接和表单,浏览器根据用户输入与不同的Web应用程序交互,此时它并不需要知道Web应用程序的接口或实现。
然而,浏览器并不能算是一个自给自足的客户端,它需要由人来做出实际决定。但是一个程序客户端却可以模拟人来做出决定的,其中甚至包含了一些硬编码逻辑。它可以跟随不同的链接访问不同的资源,同时它将根据不同的媒体类型发出不同的请求。
REST和DDD
RESTful HTTP是具有诱惑力的,但是我们并不建议将领域模型直接暴露给外界,因为这样会使系统接口变得非常脆弱,原因在于对领域模型的每次改变都会导致对系统接口的改变。要将DDD与RESTful HTTP合并起来使用,我们有两种方式:
第一种方法是为系统接口层单独创建一个限界上下文,再在此上下文中通过适当的策略来访问实际的核心模型。这是一种经典的方法,它将系统接口看作一个整体,通过资源抽象将系统功能暴露给外界,而不是通过服务或者远程接口。
让我们看一个实际的例子。我们创建一个系统来管理工作组,其中包括任务、计划/预约和子工作组管理等。我们将创建一个纯净的、不受架构影响的领域模型,该模型能正确地反映通用语言,并准确地实现业务逻辑。如果要为这个领域模型发布一个接口,我们便可以通过REST资源的形式向外提供一个远程接口。这些资源反映了客户所需的用例,它们和领域模型是存在区别的。但是,每一种资源归根结底都创建自核心域,比如核心域中的聚合等。
当然,我们也可以简单地使用领域对象来作为WebAPI的方法参数,比如我们可以将/user/task路由到getTask()方法,该方法返回一个Task对象。这样看起来是简单的,但却隐藏着一个很大的问题。对Task对象的任何修改(指修改对Task类的定义)都将立即反映到远程接口上,结果有可能使客户端调用失败。而即便我们所做的修改与外界没有任何关系,我们依然不能排除客户端调用失败的可能性。
因此,上一种方式是应该被优先考虑的,因为它在核心域和系统接口模型之间完成了解耦,这使得我们可以先对领域模型进行修改,然后再决定哪些修改应该反映到系统接口模型上。请注意,在这种方法中,系统接口模型通常是根据领域模型来设计的,但是更好、更自然的方法应该是根据用例来设计。另外,我们还可以为这种方式自定义一种媒体类型。
另一种方法用于需要使用标准媒体类型的时候。如果某种媒体类型并不专用于支持单个(某个)系统接口,而是用于一组相似的客户端-服务器交互场景,此时我们可以创建一个领域模型来处理每一种媒体类型。这样的领域模型甚至可以在服务器和客户端之间进行重用,虽然有些REST和SOA的拥护者认为这是一种反模式。请注意:这种方法本质上即为DDD中的共享内核或者发布语言。
这也是一种由外向里的、横切式的方法。在上面提到的工作组例子中,有多种常用的格式可以用于领域模型。比如ical格式(iCal 又称 iCalendar,是一种标准的互联网日历格式,让用户能够在各种计算机和各种程序之间创建和共享电子日历),这是一种通用格式。在本例中,我们首先选择一种媒体类型,即ical,然后再根据这种格式创建领域模型。该模型可以用于任何能够理解ical格式的系统,比如服务器程序,或者Android客户端。在采用这种方法时,服务器需要处理多种类型的媒体类型,而同一种媒体类型又可以用于多个服务器。
如何在以上两种方法之间进行选取呢?这在很大程度上取决于系统设计者对可重用性上的需求。第一种方法比较适合更加专属的系统,而第二种方法更适合那些通用的系统。
为什么是REST?
从经验看来,符合REST原则的系统将具有更好的松耦合性。通常来讲,添加新资源并在已有资源中创建到新资源的链接是非常简单的。要添加新的格式同样如此。另外,基于REST的系统也是非常容易理解的,因为此时系统被分为很多较小的资源块,每一个资源块都可以独立地测试和调试,并且每一个资源块都表示了一个可重用的入口点。HTTP设计本身以及URI成熟的重写与缓存机制使得RESTful HTTP成为一种不错的架构选择,该架构具有很好的松耦合性和可伸缩性。