标签:des winform c class blog code
ServiceStack是一个开源的、支持.NET与Mono平台的REST Web Services框架。InfoQ有幸与Demis Bellot深入地讨论了这个项目。在这篇两部分报道的第1部分中,我们主要谈论了ServiceStack项目建立的原动力,以及项目中的各种设计方案选择。
InfoQ: 你是否认为微软对服务的实现方式有什么问题?ServiceStack又是怎样解决这些问题的呢?
Demis: 有一些问题是由于微软一直以来自认为良好的框架设计的方式所造成的常见问题,另一些问题的根源是由于微软的设计总是倾向于满足设计器优先的工具,并且试图为使用设计器作为向导的开发者提供一套熟悉的API,而这种设计倾向造成了一些副作用:
由于微软过于追求为开发者提供一套熟悉的RPC API,并在VS.NET中提供丰富的UI工具(例如“添加服务引用”对话框)以保证上手的简易性,其代价就是推广了远程服务中的各种反模式,从长期来看将导致不必要的不协调与脆弱性。不幸的是,微软长期以来在其发布的每个web service框架中都持续使用这种设计方式,这种方式提倡创建RPC服务API的设计方式,造成了开发者在意识中将远程服务也当作本地方法一样调用。这种结果的害处是多方面的,远程服务意味着它包装了一个外部依赖,对它的调用要比本地方法慢上几百万倍,并且更容易受外界的影响而发生错误。将隐式的服务契约与服务端RPC方法签名绑定在一起,就意味着你的API服务契约和服务端的实现紧密耦合在一起了。由于没有规定一个定义良好的边界,导致了糟糕的关注分离,并且出现了将繁重的ORM数据模型通过网络返回给客户端的糟糕实践。从定义上来说,这种数据模型的关系型结构与循环式关系对于数据迁移对象(DTO)来说并非一种好的选择,它使调用可能偶尔会失败。这种方式还将隐式的服务契约与你的底层关系型数据库(RDBMS)数据模型耦合在一起,这样在进行修改时就会产生额外的冲突。而且远程API与调用它们的服务端网站是无连接的,而这些调用的方法在客户端代理部署后也会不断地演化。理想的框架应该能够提倡可演化的、灵活的API设计,以避免在服务端改变时产生运行时错误。
这些有一张关于WCF提倡的细粒度的RPC API方式与ServiceStack所鼓励的基于消息的API方式的不同之处的对比图。此外,这个用ServiceStack重写Web API的入门教程的示例则展示了基于消息的API如何减少细粒度调用,增加了服务的可重用性。
ServiceStack采用了Martin Fowler所推荐的远程服务最佳实践,因此避免了之前困扰着.NET web service开发者的很多潜在问题:
远程门面提倡使用基于消息的、粗粒度的批量调用接口。它能够将通信次数减至最低,并促进了创建更少但是可用性更好、并且支持版本化的服务接口。ServiceStack采取了基于消息的设计确保开发者始终创建粗粒度的API设计。
数据迁移对象要求使用特定用途的POCO以创建web service响应的数据格式。ServiceStack一直鼓励使用DTO,它本身与它的实现完全解耦,并且存在于一个没有外部依赖也不需要具体实现的程序集中。这种策略允许将定义服务端的服务类型共享给所有.NET客户端,因而提供了一个端到端的类型化API,并且无需使用代码生成。
网关的作用是将所有服务端通信包装起来,隐藏在一个显式定义并且可重用的客户端网关之后。这种最佳实践和代码生成的代理(例如WCF)有所不同,后者将代码生成的类型与底层的服务客户端揉合在一起,使得生成结果难以模拟(mock)、测试以及替换为不同的实现,也经常导致对现有服务的改变会造成客户端代码的编译错误。这一点对ServiceStack来说很少会成为问题,因为它能够重用通用的服务客户端,因此在接口方面唯一需要改变的东西仅限于类型本身而已。并且由于基于消息的设计的优势,无论新增或移除任何功能都不会影响到现有的客户端。
ServiceStack在它的通用并且可重用的类型化.NET服务客户端采用了网关模式。我们同时支持Silverlight、JavaScript、Dart甚至是MQ(消息队列)的客户端。让所有.NET服务客户端都共享相同的接口,这使测试(通过注入或者写日志方式)变得简单,而且能够方便地在JSON、XML、JSV、MessagePack以及ProtoBuf等现有的客户端之间进行切换,而无需改动应用程序的代码。这就允许你在开发时使用类似于JSON这样便于调试的文本格式,随后在部署构建时切换为使用.NET上速度最快的二进制格式,例如Message Pack或ProtoBuf,以降低数据传输量并实现最高的性能。
以当前的情况来看,SOAP这种技术是不应该继续存在的,或者说应该仅仅限制在那些它还能够带来一些好处的地方使用。它的存在本身就是基于一个错误的假设,即为了实现数据的互交换性,必须表现出复杂性,要尽可能的严格与明确,这使得基于它的所有现实都必须实现一个复杂的schema以进行通信。而事实上,之与相对设计方式反而被证明是正确的,如果使用了类似于JSON、CVS、Protocol Buffers、MessagePack、及BSON等最小化并且灵活的格式,就会大大减少实现的障碍与复杂性,结果的生成会更快,互操作性与版本能力也会更强。
我总是觉得这件事很难想象,尽管SOAP是建立在HTTP基础之上的,但它的设计者们在开发出WSDL时似乎完全没有吸取那些宝贵的经验。WSDL所引入的类型、消息、操作、端口、绑定以及服务这些概念真的能够取代HTTP中简单的URL标识符,以及Accept/Content-Type这些头信息吗?我觉得这一点也不难判断啊。
虽然它名为SOAP(简单对象访问协议),但它既不简单,也不是什么对象访问协议,而且对于开发服务来说,它在许多方面都显示出它是个糟糕的选择,例如:
由于SOAP明显背离了服务的核心优势,所以它能够流行起来实在是一个令人吃惊的事实。我曾有许多年为政府项目与大型企业开发服务的经验,很不幸的是其中的许多项目都有一个必需实现的要求,即把他们的服务暴露为SOAP终结点。不过许多互联网公司与专注于创造价值的创业公司都不太会使用这门技术了,他们更多地选择创建简单的HTTP API,返回单纯的XML或者JSON格式的响应。
虽然在多数情况下SOAP是一个糟糕的选择,但ServiceStack还是为你创建的服务提供了对SOAP终结点的支持,因为如今仍然有许多现存的企业系统只能够与SOAP终结点进行通信。这一点也和ServiceStack的核心目标相一致,即为你的服务提供最大化的功能性、可访问性以及连接方式。
SOAP在设计上是一种非常脆弱的格式,如果某个服务稍稍偏离了WSDL,而客户端又是由这个WSDL生成的,那你通常就会看到运行时错误的发生。虽然在静态类型系统中,快速失败被视为一种优势,但对于远程服务来说就不是一种理想的方式了,因为你通常希望能够实现向前兼容与向后兼容,因此服务端的API改变不会破坏现有的客户端。这一目标显然被WCF忽略了,它提倡RPC方法签名、SOAP格式以及代码生成,这可以说是当今的所有web service实现技术中最为脆弱的组合了。
将SOAP与Google的Protocol Buffers进行一下对比是很有趣的,它是Google推出的一种简单的接口描述语言(IDL),Google在其内部的所有RPC协议与文件格式中几乎都使用了该技术:
MessagePack RPC以及用于开发Facebook的Apache Thrift是另外两个流行的开源选择,它们都提供了快速的,简单的IDL,并且为多数主流平台提供了绑定。
虽然是由不同的公司开发,但以上每个IDL都比SOAP更简单、快速并且易于使用。从SOAP的规格说明书的厚度来看,它应该是由委员会所开发的,但显然完全没有考虑过性能,大小以及易于实现。
虽然在大小与性能方面有优势,但Protocol Buffers与MessagePack都有一个问题,即它们使用了二进制格式。由于基于文本的格式与协议更易于创建、维护及调试,它们经常成为整个开放web的更流行的选择。近年来,在基于文本的数据互交换格式上的赢家显而易见是JSON,这是一个简单的、紧凑的自描述文本格式,它的主要优势是能够在所有支持JavaScript的浏览器上进行直接应用,只需使用浏览器内置的“eval()”语句就能够立即将其转换为JavaScript对象了。由于它的流行性,主流浏览器如今都加入了对JSON的原生支持,它们为eval()方法提供了一个更安全的替代方案,可以在老浏览器上提供一个基于JavaScript的备选实现方式。如果想更多的了解JSON以及它的好处,我推荐你去看看Douglas Crockford这个有趣的演讲——“Heresy & Heretical Open Source”。
由于它的普遍性、功能多样并且易于使用,JSON在多数平台上都得到了原生的支持,并且很快地成为了开发Web API的首选数据格式,尤其是使用在Ajax客户端的时候。
从我自身的经验来看,我认为JSON对于实现服务是一种优秀的格式。它是一种更精简、灵活、容忍度高并且适应性更强的格式,并且它易于创建、调用以及调试,比起SOAP减少了冲突并提高了生产力。当我们开始ServiceStack项目时,JSON的唯一问题就是.NET Framework自带的序列化器比XML序列化器还要慢。出于对性能上的严谨态度,我们并不愿意接受这点不足,因此我们发布了我们自己实现的序列化器,它比任何其它.NET JSON序列化器都要快上3倍以上,并且比.NET Framework中的任何序列化器(包括二进制序列化器)都要快上2.5倍以上。
继承了JSON保持体积最小的精神,我们也开发了JSV格式,它类似于JSON,但使用了CVS风格的转义功能,使得它比JSON略微加快,更精简并且更便于人类阅读,它不仅能够用于.NET与.NET服务的交互,而且能够用以解析查询字符串,在ServiceStack的GET请求中是能够允许传递复杂的对象图的。
虽然WCF与ServiceStack在实现方式上是完全不同的服务框架,但它们却有着非常类似的目标。ServiceStack在创建服务上采取了一种非常面向服务的方式,它的设计经过了做好,能够以最大程度重用的方式达到服务的实现。通过代码优先、类型化设计的方式,我们能够为你的服务提供更强大的智能推断,以允许你自动地生成XSD、WSDL文件,自动生成元数据页,并且自动暴露预定义的路径。每个新加入的功能、特性、终结点与Content-Type都是围绕着你的现有模型创建的,并且不需要任何额外的操作以及对应用程序代码的任何改动就可以立即获得新功能。我们将这种方式理解为从一个代码优先的模型开始,作为最权威的部分,随后再将其功能暴露出来。
WCF的目标也是提供一个服务框架,以支持在多种终结点上运行服务,但他们站在一个不同的视角,并且为所有网络终结点提供了一个统一的抽象,你必须对服务进行配置以绑定到终结点。作为WCF的主要目标之一,他们是少数几个在最终实现中对WS-*系列技术提供了深入支持的框架。
我们最终的目标都是提供一个易于使用的服务框架,我们只是在如何取得简便性上有着完全不同的想法。
WCF看起来比较喜欢繁重的抽象,用复杂的运行时XML配置进行管理,以及用大型工具为开发者提供端到端的连接能力。这个抽象层包含了许多复杂的技术:经过抽象的人工服务端对象模型很复杂、配置很复杂、WSDL很复杂、SOAP/WS-*也很复杂。最终结果是,为了对这些主题有一个良好的理解,对每个主题都需要看好几本书才行。
有一种陈旧的观念认为处理复杂性的解决之道就是添加更高层的抽象。这种方式的问题在于,只有这种抽象的设计很完美,它才能够显出效果。(例如编程语言比机器代码更合适)否则当你遇到了一些预料之外的行为,或者是配置、整合与互操作性上的问题是,你需要去理解在这些抽象层之下到底发生了什么。在这种情况中添加更多的抽象实际上只会增加开发者必须去熟悉的概念,以及当他们针对高级别抽象层进行开发时额外的认知。这也使得理解你的代码库更加困难,因为你必须理解每一个之下都发生了些什么。WCF的服务端对象模型是全新的、并且不够自然,这进一步放大了问题,新的开发者们对此一无所知,因为这种模型是独一无二的,并且它完全没有对服务或HTTP领域中的概念知识作出任何指导。因此当开发者们之后转而使用最好的服务框架时,他们从WCF中学到的东西完全没有用武之地。与其投入时间去阅读那些如何使用WCF的书籍,还不如把时间花在学习HTTP与TCP/IP上,因为即使你转而使用其它web service框架,这些知识依然能起到作用,它们是完全独立于编程语言或平台的。
从技术实现的层面来说,过多的抽象会导致不必要的性能负担,并且这种性能问题很难优化。因为当你尝试直接与底层API打交道时,这些抽象会产生阻碍以及各种强制性的阻力。而我们的方式是只在绝对必要时才会添加抽象,例如我们的IHttpRequest与IHttpResponse封装对于为ASP.NET与自托管的HttpListener提供一个通用的API就是必需的。相比起添加抽象层的方式,我们更倾向于添加一些非强制性的特性,它们只是将一些底层接口与一些常见功能包装起来,以实现DRY。这种方式带来的好处就是允许终端用户自由地调用各种方式,并在他们自己开发的类库中加入自己定义的功能增强,以促进他们实现精益的、DRY以及可读性良好的代码库。暴露底层API能够确保我们保持框架的灵活性,用户可以通过多种方式进行访问而得已完整地控制系统的输出,也因为用户可以定制返回的响应,因此他们不会受到任何限制,而得已控制返回的每一个字节。
WCF为服务采取了一种统一的方式,它促进所支持的所有终结点都标准化为一个单一的人为抽象模型。这种方式也遇到了抽象的通用原则所产生的副作用,即一个统一的抽象模型要为每一种试图抽象化的实现承担所有的复杂性,因而它们只能够处理各种终结点的特性的交集部分,而且为每个结终点实现最低程度的通用功能。结果就是生成了一个不完整的外部API,并且由于抽象的存在阻碍了对底层终结点的访问,使得这种API的可访问性与可配置能力都大大削弱了。
要彻底了解WCF的功能需要强大的技术能力,需要你投入巨大的精力研究各种技术资源。但本质上来看这种精力投入是一种浪费行为,因为WCF与其它平台上的一些采用了标准方式开发的框架相比,生产力与功能都有所欠缺。对于终端用户来说,哪怕是为了稍稍掌握一些WCF的使用经验,也需要投入大得多的精力。出于以上原因,我不希望看到有人去使用类似于WCF之类的框架,或者是它那种统一的终结点的抽象方式。由于它的复杂性与庞大的技术实现细节,我也很怀疑它是否能够保持演化,以适应新的开发范式或是服务开发模式。我怀疑WCF会遭受到与WebForms相同的命运,退化为一个过时的技术,最终被微软下一代框架取而代之。
除了现有的REST、SOAP、MQ以及RCON之外,我们也计划为ServiceStack加入更多的终结点。但由于我们采取了一种基于约定的方式,我们会避免使用繁重的配置。我们所采用的基于消息的设计方式会避免使用大型工具,并简化必须实现的外部接口部分。首先从C#开始,逐渐移植其它语言,这使得终端用户可以立即使用新功能,而无需任何额外的投入。我们的松耦合架构不存在统一的抽象模型,这可以允许我们加入互不冲突的新终结点与特性,而不会为现有的组件带来强制的复杂性。
总而言之,我们提供了一个特性完整、性能更好并且更易于理解的服务框架,它的代码库也更加精益和灵活,并且使用它进行开发和维护所需的技术投入要小的多。
WCF所推崇的另一个概念是XML配置,而这也是我们无法苟同的,它妨碍了测试,并且需要投入更多精力进行维护。只有你的应用程序中真正可配置的部分才应该放在你的应用配置文件中,定义及组织你的服务依赖应该交给代码去做,这种方式的额外优点在于它便于调试,而且可以在编译时静态地进行验证。WCF需要大量的配置,在ServiceStack中创建一个完整的应用程序所需的代码量比起一个的WCF Service所需的XML WCF 配置还要小。如果把配置信息放在IOC(控制反转)这一怪,那代码会更干净与整洁,因为它直接就可以引用你所需的特性的.NET API了。
大型工具的问题在于,当你打算在其中尝试些新功能,或者是某些不在其设计初衷里的功能时,它就无能为力了。也就是说你成为了这个工具的奴隶,只能提供一些它所支持的功能。而且框架一旦进行了重写,大型工具就无法继续使用,因为它们所提倡的繁重的、复杂的代码库是很难演化或者是重构的。我推测这就是微软为什么总是重写新的服务框架,而不是改进现有的框架的原因。也是为什么Web API不能够重用MVC现有的抽象方式,并且不支持SOAP的缘故,尽管它们都采用了相同的RPC API设计方式,并且它的自托管功能选项也是基于WCF创建的。
由于ServiceStack是基于ASP.NET的最底层创建的,因此它提供了对ASP.NET MVC良好的集成能力,也能够方便地重用MVC中许多组件现有的功能实现,例如Authentication filter attribute、Caching与Session provider等等。你也可以在MVC中方便地调用ServiceStack服务,比起C#方法直接调用只是稍稍麻烦一点。
ServiceStack与WCF相反,它看起来要简单许多。例如ServiceStack的架构只要一页纸就能画下。启用它只需在web.config中加入一行,告诉ASP.NET将所有请求转发给ServiceStack即可。
ServiceStack与微软开发类库与框架所采用的方式,其观念上的最大不同或许就是在如何最好地处理复杂性这一点上了。微软倾向于明确性,XML方式的可配置能力,引入繁重的抽象层(为了前瞻性而支持所有潜在的用例),依赖于抽象层将简单的面向用户的门面经过改头换面后暴露出去,为了方便使用设计器工具及新手开发者进行优化。微软的主要动力是为开发者提供一个熟悉的编程模型,并且通过使用大型工具来简化模型的上手难度。从WebForms中就可以看出这一点,它为WinForm开发者提供了一个基于事件的编程模型来开发网站,WCF则让开发者使用传统的C#方法与接口方式去创建远程服务。它们采取的方式通常会导致完成功能需要投入大量的技术精力去研究巨大的代码库。
与之相反,我们将大型的代码库当作代码的最大敌人,并且避免引入人为的复杂性,这是我们的主要目标之一。比方说我们强烈反对引入新概念、抽象或编程模型。我们崇尚小巧的低级别接口,它与底层领域1对1进行映射,以实现灵活性的最大化,减少冲突,并且在需要进一步定制化时将转换的复杂性降至最低。通过可重用的工具类与扩展方法保证了DRY与高级别的功能。我们采取一种代码优先的开发模型,通过代码去捕捉用户意愿的本质,这促进了一种更优雅、不妥协的设计方式,避免了使用设计器工具时所带来的转换的复杂性。我们的所有类库都使用POCO,以获得最大的可重用性,并且我们内置的转换器可以简化在领域特定模型中进行转换所需的精力。
我们为用户展现了一种基于消息设计的最佳实践,为从头开始开发的服务提供了一种最优的方式。服务就是普通的C#类,无需关注任何终结点的问题,因为这种依赖是可以自动解析的,这种方式会促使用户采用良好的代码实践。
约定以及减少对外部知识的了解是我们处理复杂性最好的武器。相比明确的指定实现方式,更好的办法是提供一个基于约定的默认行为,使它按预计的方式运行。这就让用户不必一定要学习你的框架API,因为我们可以假定预计的标准行为的结果,并允许我们随时提供更多的能力,比方说,它能够自动支持外部所调用的所有内置终结点与格式。你也可以让你的服务返回任意的响应,它会自动序列化为所请求的Content-Type。
我们相信,达成简单性的最好方式就是在第一时间避免各种复杂性,我们采取了以下方式:
我们并不喜欢代码生成,因为我们相信它为项目带来了不必要的麻烦,我们也反对在开发工作的核心部分依赖于任何大型工具。
由于你的ServiceStack服务的服务契约在设计上是由你的请求与响应DTO维护的,我们就能够提供一个类型化的端到端的API,只需要你用来创建服务的那些类型以及那些通用的、可重用的.NET客户端就可以了。这允许我们为任何.NET上的服务框架提供一个最简洁的、类型化的以及端到端的API。
基于WCF的设计方式以及它对配置的严重依赖,看起来WCF在设计时完全没有考虑到可测试性。而这一点是ServiceStack的核心目标之一,我们默认提供了一个内置的(并且可重写的)IOC容器,鼓励在一开始就遵循良好的开发实践。开发一个服务只需实现一个不包括任何方法的IService接口,或者从其它现有的Service类、或ServiceBase类继承即可,这些类都是可独立测试的,并且完全可以mock。这一系列努力的成果就是,你所实现的单元测试可以用作XML、JSON、JSV以及SOAP的集成测试。我们所提供的自托管HttpListener类型使得进行内存中的集成测试非常方便。
性能是ServiceStack的首要目标之一,我们将性能视为最重要的特性。基于消息的设计提倡更少的网络调用,我们也很小心地保证只暴露那些运行快速的面向用户的API。一般情况下我们不接受来自外部的贡献代码,因为它们会包含速度较慢的代码。我们对自己的实现是全力以赴的,我们没有使用任何运行时的反射或者正规表达式匹配,而是采用其它更快的解决方案。
我们开发并维护着.NET上最快的JSON、JSV以及CSV文本序列化器,同时允许以插件的方式使用Message Pack与Protocol Buffers这两个.NET上最快的二进制序列化器。
由于缓存是创建高性能服务不可或缺的技术,我们提供了一个丰富的Caching Provider模型,包含了对各种内存缓存、Redis、Memcached、Azure以及Amazon后台的实现。缓存API会保证使用最优化的格式,比方说,如果客户端支持,我们会将压缩后的JSON输出缓存起来,并在之后发生调用时直接将缓存中的内容输出到响应流中,这就保证了托管代码能产生最快的响应速度。
每次我们找出微软的类库中的瓶颈时,我们都会使用更快速的实现去替代它。我们已经实现了自己的JSON序列化器,作为插件的Gzip和Deflate压缩类库,并且提供了自己的Session实现,它能够与以上任意一种Caching provider相配合使用,它的出现也避免了困扰ASP.NET开发者已久的严重性能问题。
出于我们对开发高性能服务的技术追求,我们为最快的分布式NoSQL数据库Redis开发并维护着一个.NET上最好的C# Redis客户端。
采访ServiceStack的项目领导Demis Bellot——第1部分(网摘),布布扣,bubuko.com
采访ServiceStack的项目领导Demis Bellot——第1部分(网摘)
标签:des winform c class blog code
原文地址:http://www.cnblogs.com/whosedream/p/3768400.html