Effective C# 原则34:创建大容量的Web API

JerryXia 发表于 , 阅读 (2,254)

交互协议的开销与麻烦就是对数据媒体的如何使用。在交互过程中可能要不同的使用媒体,例如在交流中要不同的使用电话号码,传真,地址,和电子邮件地址。让我们再回头来看看上次的订购目录,当你用电话订购时,你要回答售货员的一系列问题:

“你可以把第一项填一下吗?”

“这一项的号码是123-456”

"您想订购多少呢?"

"三件"

这样的问题一直要问到销售人员填写完所有的信息为止,例如还要知道你的订购地址,信用卡信息,运送地址,以及其它一些必须的信息来完成这比交易。在电话上完成这样一来一回的讨论还是令人鼓舞的。因为你不会是一个人长时间的自言自语,而且你也不会长时间忍受销售人员是否还要哪里的安静状态。

与传真订购相比,你要填写整个订购文档,然后把整个文档发给公司。一个文件一次性传输完成,你不用很填写产品编号,发传真,然后填写地址,然后再传真,填写信用卡号,然后再发传真。

这里演示了一个定义糟糕的web方法接口会遇到的常见缺陷。当你使用web服务,或者.Net远程交互时,你必须记住:最昂贵的开销是在两台远程机器之间进行对象传输时出现。你不应该只是通过重新封装一下原来在本地计算机上使用的接口来创建远程API。虽然这样是可以工作的,但效率是很低的。\

这就有点类似是用电话的方式来完成用传真订购的任务。你的应用程序大部份时间都在每次向信道上发送一段数据后等待网络。使用越是小块的API,应用程序在等待服务器数据返回的时间应用比就更高。

相反,我们在创建基于web的接口时,应该把服务器与客户端的一系列对象进行序列化,然后基于这个序列化后的文档进行传输。你的远程交流应该像用传真订购时使用的表单一样:客户端应该有一个不与服务器进行通信的扩展运行时间段。这时,当所用的信息已经填写完成时,用户就可以一次性的提交这个文档到服务器上。服务器上还是做同样的事情:当服务器上返回到客户上的信息到达时,客户的手头上就得到了完成订购任务必须的所有信息。

比喻说我们要粘贴一个客户订单,我们要设计一个客户的订购处理系统,而且它要与中心服务器和桌面用户通过网络访问信息保持一致。系统其中的一个类就是客户类。如果你忽略传输问题,那么客户类可能会像这样设计,这充许用户取回或者修改姓名,运输地址,以及账号信息:

public class Customer
{
  public Customer()
  {
  }

  // Properties to access and modify customer fields:
  public string Name
  {
    // get and set details elided.
  }

  public Address shippingAddr
  {
    // get and set details elided.
  }

  public Account creditCardInfo
  {
    // get and set details elided.
  }
}

这个客户类不包含远程调用的API,在服务器和客户之间调用一个远程的用户会产生严重的交通阻塞:

// create customer on the server.
Customer c = new Server.Customer();
// round trip to set the name.
c.Name = dlg.Name.Text;
// round trip to set the addr.
c.shippingAddr = dlg.Addr;
// round trip to set the cc card.
c.creditCardInfo = dlg.credit;

相反,你应该在本机创建一个完整的客户对象,然后等用户填写完所有的信息后,再输送这个客户对象到服务器:

// create customer on the client.
Customer c = new Customer( );
// Set local copy
c.Name = dlg.Name.Text;
// set the local addr.
c.shippingAddr = dlg.Addr;
// set the local cc card.
c.creditCardInfo = dlg.credit;
// send the finished object to the server. (one trip)
Server.AddCustomer( c );

这个客户的例子清楚简单的演示了这个问题:在服务器与客户端之间一来一回的传输整个对象。但为了写出高效的代码,你应该扩展这个简单的例子,应该让它包含正确的相关对象集合。在远程请求中,使用对象的单个属性就是使用太小的粒子(译注:这里的粒子就是指一次交互时所包含的信息量)。但,对于每次在服务器与客户之间传输来说,一个客户实例可能不是大小完全正确的粒子。

让我们来再扩展一下这个例子,让它更接近现实设计中会遇到的一些问题,我们再对系统做一些假设。这个软件主要支持一个拥有1百万客户的在线卖主。假设每个用户有一个订购房子的主要目录,平均一点,去年有15个订单。

每个电话接线员使用一台机器轮班操作,而且不管电话订单者是否回答电话,他们都要查找或者创建这条订单记录。你的设计任务是决定大多数在客户和服务器之间传输的高效对象集合。

你一开始可能消除一些显而易见的选择,例如取回每一个客户以及每次的订单信息是应该明确禁止的:1百万客户以及15百万(1千5百万)订单记录显然是太大了而不应该反回到做一个客户那里去。这样很容易在另一个用户上遇到瓶颈问题。在每次可能要更新数据时,都会给服务器施加轰炸式打击,你要发送一个包含15百万对象的请求。当然,这只是一次事务,但它确实太低效了。

相反,考虑如何可以最好的取回一个对象的集合,你可以创建一个好的数据集合代理,处理一些在后来几分钟一定会使用的对象。一个接线员回复一个电话,而且可能对某个客户有兴趣。在电话交谈的过程中,接线员可能添加或者移除订单,修改订单,或者修改一个客户的账号信息。明显的选择就是取回一个客户,以及这个用户的所有订单。服务器上的方法可能会是这样的:

public OrderData 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, DataTime lastUpdate, AreaCode theAreaCode)
{
  // First, save any updates, marking each update
  // with the current time.

  // 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.
}

但这样可能还是要浪费一些带宽。当每个已知客户每天都有电话时,最后一个设计是最有效。但这很可能是不对的。如果是的,那么你的公司应该在客户服务上存在很大的问题,而这个问题应该用软件是无法解决的。

如何更进一步限制传输大小呢,要求不增加会话次数和及服务器的响应延时?你可以对数据库里的一些准备打电话的客户进行一些假设。你可以跟踪一些统计表,然后可以发现,如果一些客户已经有6个月没有订单了,那么他们很可能就不会再有订单了。这时你就应该在那一天的一开始就停止取回这些客户以及他们的订单。这可以收缩传输的初始大小,你同样可以发现,很多客户在通过一个简短电话下了订单过后,经常会再打电话来询问上次订单的事。因此,你可以修改订单列表,只传输最后的一些订单而不是所有的订单。这可能不用修改服务器上的方法签名,但这会收缩传输给客户上的包的大小。

这些假设的讨论焦点是要给你一些关于远程交互的想法:你减少两机器间的会话频率和会话时数据包的大小。这两个目标是矛盾的,你要在这两者中做一个平衡的选择。你应该取两个极端的中点,而不是错误的选择过大,或者过小的会话。

添加新评论