原则28:创建大粒度的网络服务 APIs
通信协议的花费和不便决定了你将会怎么使用这些媒介。使用电话,传真,信件和电子邮件交流是不同的。回想上次你从商品目录中下单。当你用电话下单,你和售货员进行问-答的交互:
“我能知道你第一选项么?”
“我选的序号是123-456。”
“你需要多少份?”
“3。”
这段交流直到售货员知道整个订单,你的账单地址,你的信用卡信息,你的送货地址,和其他完成交易的信息才终止。在电话上来回讨论是非常方便的。你不会一直自言自语而没有反馈。你不能忍受长时间的沉默如果销售人员仍然还在。
用传真订单会不一样。你会填满整个文档然后在把整个文档传真给公司。一个文档,一次交易。你不会填一栏,传真一次,填写地址,又传真一次,添加信用卡新,又传真一次。
这说明了缺乏定义的服务接口常见的误区。不管你使用 web 服务,.NET 远程,或基于 Azure 编程,你必须记住在两台机器传递对象的操作的昂贵的花费。你必须停止创建的远程 API 只是对本地接口的一个封装。这就像是用打电话处理本要用传真处理的订单。你的程序每次都要等待在网络管道传递信息往返的时间。更大粒度的 API ,程序的更高比例时间花在等待数据从服务器返回。
相反,在客户端和服务器之间创建基于 web 的接口要基于一系列文档和对象集。你的远程交流应该向像用传真向商品公司下订单一样工作:客服端机器应该能在没有和服务器交流的时间工作。然后,等所有交易信息填写完毕,客服端发送整个文档给服务器。服务器用同样的方式工作:从服务器发送信息到客服端,客服端接受所有必要信息并接着完成所有任务。
接着消费者下单的比喻。我们设计客户下单处理系统,它由中心服务器和桌面客服端,彼此通过 web 服务访问信息。系统中的一个类就是 Customer 类。如果你忽略了运输问题,客户类可能是这样子的,它允许客户端代码检索或修改的名称,送货地址,和帐户信息:
public class Customer
{
public Customer()
{
}
// Properties to access and modify customer fields:
public string Name { get; set; }
public Address ShippingAddr { get; set; }
public Account CreditCardInfo { get; set; }
}
Customer 类没有包含会被远程调用的 API 。调用远程的 Customer 会导致在客服端和服务器之间过度的交流:
// create customer on the server.
Customer c = Server.NewCustomer();
// round trip to set the name.
c.Name = dlg.Name;
// round trip to set the addr.
c.ShippingAddr = dlg.ShippingAddr;
// round trip to set the cc card.
c.CreditCardInfo = dlg.CreditCardInfo;
取而代之,我们会创建一个局部 Customer 对象,并且当这个对象所有域都设置好之后传递给服务器:
// create customer on the client.
Customer c2 = new Customer();
// Set local copy
c2.Name = dlg.Name;
// set the local addr.
c2.ShippingAddr = dlg.ShippingAddr;
// set the local cc card.
c2.CreditCardInfo = dlg.CreditCardInfo;
// send the finished object to the server. (one trip)
Server.AddCustomer(c2);
这个客户的例子说明一个明显又简单的例子:在客服端和服务器之间来回传递整个对象。但是为了编写高效的程序,你需要扩展这个简单例子以囊括相关的对象集。远程调用一个对象的单个属性粒度太小了。但是一个客户也可能不是在服务器和客服端交易合适的粒度。
扩展这个例子到实际的设计问题是你会在程序中遇到的,我们对系统进行些假设。这个软件系统支持主要在线厂商和一百万客户的交易。想象下大多数客户都在家里下单,去年平均15单。每个电话操作员每次都要同一台机器上下切换记录客户回答的信息。你的设计任务是决定更多高效对象集在客服端机器和服务器之间传递。
你开始就可以消除一些明显的选择。检索每个客户和每个订单是很明确被禁止的:一百万的客户和一千五百万的订单记录数据大到不可能传递给每个客户端。你需要为一个瓶颈去权衡其他。为了不每个可能的数据更新不断轰击你的服务器,你向服务器发送一个超过一千五百万的对象请求。当然,这只是一个交易,但这是一个非常低效的交易。
相反,考虑如何最好的检索一组对象集,操作者接下来的几分钟必须使用一个近似的对象的数据集。操作者会接电话并和客户交流。在通话过程中,操作者可能添加和移除订单,改变订单,或者修改客户的账户信息。显而易见的选择是一次检索整个客户的所有订单。服务器的方法是下面这样的:
public OrderDataCollection FindOrders(string customerName)
{
// Search for the customer by name.
// Find all orders by that customer.
}
这样就正确了?那些已经发货或已经被签收的订单大多数情况下客户端几乎不需要。一个请求客户更好的检索只是开放的订单。服务器的方法会改成这样:
public OrderData FindOpenOrders(string customerName)
{
// Search for the customer by name.
// Find all orders by that customer.
// Filter out those that have already
// been received.
}
你仍然在每个客户电话的开始就请求数据。有没有方法优化下载客户端订单信息的通信。我们继续添加一些业务流程的假设,你会得到一些想法。假设呼叫中心划分使每个工作团队只会接到一个区域代码。现在你可以修改你的设计优化传播得更多。
每个操作者会在开始切换的区域代码而检索被更新的客户和订单信息。每次电话后,客户端会将修改数据推送会服务器,服务器会对后面的客户端请求推送改变的数据。这样的结果是每次电话后,操作者会发送修改的数据给服务器并从服务器获得同组的其他操作者修改的数据。这个设计意味着每个电话只能进行一个交易,每个操作者在回答电话时总是获得正确的数据集。这就每个电话只有一个来回。现在服务器包含下面两个方法:
public CustomerSet RetrieveCustomerData(AreaCode theAreaCode)
{
// Find all customers for a given area code.
// Foreach customer in that area code:
// Find all orders by that customer.
// Filter out those that have already
// been received.
// Return the result.
}
public CustomerSet UpdateCustomer(CustomerData updates, DateTime lastUpdate, AreaCode theAreaCode)
{
// First, save any updates.
// Next, get the updates:
// Find all customers for a given area code.
// Foreach customer in that area code:
// Find all orders by that customer that have been
// updated since the last time. Add those to the result.
// Return the result.
}
但是你可能还会浪费一些带宽。当每个已知客户每个都打电话来下单时,你最后的设计作品效果最好。这是不正确的。如果是,你的公司已经远远超出一个软件项目范围的客户服务问题。
在不增加事务请求数量或服务响应客户的延迟的情况下,我们怎样才能进一步限制每笔交易的数据的大小?你可以对数据库中的客户进一步假设。你跟踪的一些统计并发现如果客户六个月不订购,他们不可能再次订购。所以从那日期你可以停止这些客户和他们的订单。这会缩小初始化交易的大小。你还发现很多客户在下了单之后在打电话来通常是询问上一次订单。所以你修改发送给客户端的订单信息只是上一次的而不是所有的订单列表。者不需要改服务器方法的前面,但是会减少发给客户端数据包的大小。
这一假设的讨论集中在让你们去思考远程机器之间的通信:你要最小化机器之间通信的频率与传输的大小。这两个目标是相违背的,你需要在它们之间做出权衡。你放弃以两个极端的中心的做法,但是大通信量会有相对更少的负面影响。
小结:
这个原则介绍的如果服务器和客户端高效的通信,数据量和频率——其实就是一次数据请求的完整性和网络带宽的权衡,这么大的问题,放在一个原则来讲,有点华而不实。