要设计一个成功的通用框架,在设计时就一定要考虑到有着不同需求、技能和背景的开发者。框架设计者所面临的巨大挑战之一是:既要满足多样化的用户群体的功能需求,又要保持框架本身的简单性。

  框架设计者的另一个重要目标是提供统一的编程模型——无论开发者编写的是哪种类型的应用(举例来说,如果一个框架组件是可用的,那么无论是在控制台应用、Windows Forms应用还是在ASP.NET应用中,它都应该具有相同的编程模型。),或者,如果框架在运行时支持多种语言,则无论开发者使用的是哪种编程语言,都应该具有统一的编程模型。

  通过采用已被广泛接受的设计原则和遵循本章所描述的准则,你可以创造出一个功能一致的框架,其可以满足使用不同编程语言构建不同类型应用的开发者的需求。

  √DO要设计功能强大且易于使用的框架。

  设计精良的框架使得简单的应用场景变得容易。同时,它不会妨碍用户进一步实现更复杂的场景,尽管可能会更困难一些。正如Alan Kay所说:“让简单的事情保持简单,让复杂的事情成为可能。”

  这一准则同样与Pareto原则(即“二八定律”)相关。Pareto原则表示,在任何情况下,只有20%是重用的,剩下的80%则是微不足道的。在设计一个框架时,应该专注于重要的20%的场景和接口。换言之,在框架设计中,我们应该把功夫花在框架中最常被使用的那部分功能上。

  √DO要了解具有不同编程风格、需求和技能水平的开发者,并明确地为他们进行设计。

&&PAUL VICK 为Visual Basic(VB)开发者设计框架没有“银弹”。我们的用户范围很广,从第一次使用编程工具的小白到构建大规模商业应用的行业老手。设计一个吸引VB开发者的框架,关键是要让他们能够以最少的麻烦和困扰完成工作。设计一个使用概念最少的框架是一个好主意——不是因为VB开发者不能处理概念,而是因为他们不得不停下来思考与手头工作无关的概念,从而导致工作流程被打断。VB开发者的目标通常不是学习一些有趣或令人兴奋的新概念,也不是要去欣赏你的设计在智力上的纯粹和简单性,他们的目的是要完成工作并继续前进。
&&KRZYSZTOF CWALINA 为像你一样的用户做设计很容易,而为与你不同的人做设计则非常困难。有太多的API是由领域专家设计的,坦率地说,它们只对领域专家有利。问题是,大多数开发者不是、永远不会是、也不需要成为现代应用中所有技术领域地专家。
&&BRAD ABRAMS 尽管惠普公司著名地座右铭“为下一个工作台地工程师而构建”对于推动软件项目地质量和完整性是有用的,但对于API的设计来说,它却具有误导性。例如,Microsoft Word 团队的开发人员清楚地知道,他们自己不是Word的目标客户。我的母亲才是目标客户。因此,Word团队放入了更多我的母亲可能认为有用的功能,而不是开发团队认为有用的功能。尽管这在Word这样的应用程序中是显而易见的,但我们在设计API时往往会忽略这个原则。我们倾向于只为自己设计API,忽视了客户的应用场景。

  √DO要了解各种编程语言,并为之做设计。

  许多编程语言都实现了对通用语言运行时(CLR)和.NET的支持,其中的一些语言和你用来实现API的语言之间可能会有很大的差别,通常需要额外的考量来保证你的API在这些语言上能正常运行。

  举例来说,开发者在使用能够和.NET交互的动态类型语言(如PowerShell和IronPython等)时,如果所使用的API要求他们创建某些具有特性(Attribute)的自定义类型,这时候他们就极有可能无法正常使用该API。

  另一个例子是,F#语言不支持用户自定义的隐式转换运算符。因此,如果API使用隐式转换运算符来简化其调用模式,那么在F#中,该API可能并不见得会易于使用。

&&JAN KOTAS 为现有编程语言的“最小公分母”做设计阻碍了.NET平台的发展。近年来,为了实现创新,人们不再强调这一点,如特性Span<T>的引入。C#和F#引进新的语言特性以启用Span<T>,而其他编译到.NET的语言(如Visual Basic)则尚未支持它,因此这些语言无法使用新的基于Span<T>的API。
&&JEREMY BARTON 虽然我们确实在没有VB支持的情况下添加了Span<T>,但是建议使用基于数组的替代方法(请参见9.12节)。尽管这在一定程度上是基于可用性给出的建议,但是最终仍然回归到本准则,多种语言都可以与.NET CLR进行交互。

  本书还介绍了其他关于不同编程语言支持的特别注意事项。

2.1渐进式框架

  为了广大开发者、应用场景和编程语言设计出一个唯一的框架是困难且代价昂贵的。过去,框架供应商为特定的开发者群体提供了若干产品来应对特定的场景。例如,微软提供的VIsual Basic API 面向简单性和相对受限的场景进行了优化,Win32 API则针对功能和灵活性进行了优化,即使这意味着牺牲了使用上的便利性。其他框架,如MFC和ATL,也是针对特定的开发者群体和应用场景的。

  景观这种多框架模式已被证明是提供API的一种成功方式,这些API对特定的开发者群体来说功能强大且易于使用,但是它有着明显的缺点。主要缺点(其他缺点包括:那些基于其他框架进行包装的框架上市时间会更慢、工作重复、缺乏通用工具。)是,众多的框架使得使用其中某个框架的开发者很难将他们的知识转移到下一个技能水平或应用场景中(通常需要使用不同的框架)。例如,当开发者需要使用更强大的功能来实现另一个不同的应用时,他们将面临非常陡峭的学习曲线,因为他们需要学习一种几乎全新的编程模式,如图2.1所示。

&&ANDERS HEJLSBERG 在Windows早期,你可以直接使用WindowsAPI。要编写一个应用程序,你只需要启动C语言编译器,#include windows.h,创建一个winproc,再处理窗口消息——基本上老式Petzold风格的Windows编程就是如此。尽管这种方式可以工作,但它既不是特别具有生产力,也不是特别容易使用。
  随着时间的推移,基于Windows API的各种编程模型相继出现。VB选择拥抱快速应用开发(Rapid Application Development,RAD),利用VB,你可以实例化一张表单,将组件拖拽表单上,然后为之编写事件处理器。你的代码通过委托来运行。
  在C++的世界中,我们使用WFC和ATL,采用了完全不同的方式。这里涉及的关键概念是子类化,开发者从大型的面向对象库中派生出子类。尽管这可以给你带来更多的功能和更强大的表达能力,但是和VB的组合模型相比,它并不具备同等的易用性和生产力。
  如果你看看上面这张图就会发现,其中一个问题是,你在编程模型上的选择必然成为你对编程语言的选择。这很糟糕。如果你是一个经验丰富的MFC开发者,现在你需要用VB编写几行代码,你已有的经验并不能直接被拿过来使用。同样,即使你非常了解VB,你的那些知识也不能帮助你使用MFC。
  面对不同的编程模型,API同样不具备一致的可用性。面对不同的问题,每个模型都凭空捏造出它们自己的解决方案,但是实际上其可能是所有模型共同面临的核心问题。例如,如何处理文件I/O,如何处理字符串格式化,如何处理安全性、线程,等等。
  .NET框架所做的就是统一所有的这些模型。无论你使用的是哪种编程语言,也无论你面向的是哪种编程模型,在任何地方,它都可以为你提供可用且一致的API。
&&PAUL VICK  值得注意的是,这种统一是有代价的。在编写框架时,有一个无法解决的矛盾——是应该暴露大量的功能给用户,使得用户可以对其行为表现进行各种控制,还是应该保持概念的简单,只为用户提供相对来说更有限的功能。在大多数情况下,没有“银弹”,在功能和简单性上做出取舍是不可避免的。在设计.NET框架的过程中,大量的工作被投入到实现二者之间的平衡上。我认为,我们今天仍然在继续做这件事情。

  更好的办法是提供一个渐进式框架,它是一个面向广大开发者的框架,允许开发者由浅入深地拓展他们的知识。.NET框架正是这样一个框架,它提供了平滑的学习曲线,如图2.2所示。

   实现这样一个有着较低七点的平滑学习曲线的框架是困难的,但也绝非是不可能的。困难在于,它要求我们使用一种全新的框架设计方法,需要更深厚的设计知识,也有着更高的设计成本。幸运的是,本章和本书中所描述的准则都旨在指导你完成这个困难的设计过程,并最终帮助你设计一个出色的渐进式框架。

  你应该始终牢记开发者社区是非常庞大的,从录制宏指令的办公室白领到底层设备驱动的作者,不一而足。任何试图去服务所有这些用户的框架最终都会变得一团糟,到头来甚至无法满足任何一个用户。渐进式框架的目标是在广大的开发者中尽量去拓展它的用户群体,但不是去满足每一个潜在的开发者。这意味着那些不属于该目标群体的开发者将需要特定的API。

2.2框架设计基本原则

  提供一个功能强大且易于使用的开发平台是.NET的首要目标之一,如果你正在扩展它的话,这也应该是你的目标之一。第一版.NET框架实际上已经为开发者提供了功能强大的API,但是一些开发者还是觉得框架中的某些部分很难使用。

&& RICO  MARIANI  另一方面,不仅要使API本身易于使用,还要确保开发者能按照API所设计的正确方式来使用它。想清楚你应该提供怎样的模式,确保开发者可以在最自然的方式下使用你提供的系统并得到正确的结果,即使它受到攻击也能保证安全,还要能提供出色的性能,确保它不会被开发者错误使用。几年前我曾写过:
    成功之坑
  
与通过反复尝试,在旅途中遭遇各种“惊喜”,最后登上顶峰,或者横穿沙漠的那种成功形成鲜明对比,我们希望用户可以通过使用我们的平台和框架更简单地获得成功。反之,如果我们使它变得更容易导致问题地话,则是我们地失败。
  真正的生产力是使开发者能够容易地创造出优秀的产品——而不是能够容易地生产垃圾。要构建成功之坑。

   用户反馈和可用性调研表明,很大一部分VB开发者在学习VB.NET时会遇到问题。部分问题源自一个简单的事实——.NET和VB 6.0库是不同的,但是也有一些是API设计导致的可用性问题。解决这些问题,成为微软在.NET Framework 2.0开发中的首要事项。

  本节中描述的这些原则以标签“框架设计原则”来标识,旨在解决上述问题。它们主要是为了帮助框架设计者避免那些会造成严重不良后果的错误设计,这些错误是从许多的可用性调研和用户反馈中总结而来的。我们相信,这些原则是设计任何通用框架的核心所在。一些原则和建议有重叠,这也从另一个角度证明了其正确性。

2.2.1场景驱动设计原则

  框架往往包含了大量的API,这对于实现具有强大功能和表现力的高级场景是必要的。然而,绝大多数开发实际上都围绕着一组常见的场景展开,只依赖完整框架中相对来说比较核心的一部分。为了提升框架使用者整体的生产力,应该将大量的投入集中于那些在最常见的场景所使用的API的设计中,这至关重要。

  为此,框架设计应该侧重于一组通用场景,使整个设计过程都由场景来驱动。我们建议,框架设计者首先把自己当作框架使用者,为主要的使用场景写一些代码,然后再设计对象模型来支撑这些代码片段(这与测试驱动开发(TDD)或基于用例的流程是相似的,但是仍有一些区别。TDD更为重量级,因为除了驱动API设计,它还有其他目标。相较于一个个独立的API调用,用例通常被用来抽象更上层的问题)。

   #框架设计原则

  框架设计必须从一组使用场景和实现这些场景的代码示例开始。

&&KRZYSZTOF  CWALINA  我想刚才阐释的原则上补充一点,“要设计出一个出色的框架,根本没有其他方法”。如果我只能挑选一条设计原则放到本书中,那么就只能是它了。如果我不是写一本书,而只是写一篇简要的文章来介绍在API设计中什么是重要的,我依然会挑选这一原则。

  框架设计者经常犯这样的错误,首先(运用各种设计原则)设计对象模型,然后根据最终实现的API来编写示例代码。问题是,许多设计原则(包括最常用的那些面向对象设计原则)都是为了最终实现的可维护性来优化的,而不是为了所产出API的可用性。它们最适合框架内部的架构设计,但对于大型框架的公开API来说并不合适。

  在设计一个框架时,首先应该产出场景驱动的API规范(参考“附录C”)。这个规范可以独立于功能规范,也可以是一个较大的规范文档的其中一部分。对于后者来说,API规范在位置和时间上都应该先于功能规范。

  这个规范应该包含场景,在给定的技术领域内,将前5~10个场景列出来,并给出实现这些场景的代码示例。当你的API或者代码示例使用了新的或其他不常用的语言特性时,你应当考虑使用至少一种别的语言再写一遍示例,因为有的时候,使用不同语言编写的代码会有非常大的差异。

  使用不同的代码风格(使用语言独有特性)来编写这些场景相关代码也很重要。例如,VB.NET对大小写不敏感,所以示例也应该反映这一点。C#代码应该遵循第3章中所描述的标准。

  √DO要确保API设计规范是任何功能(包括公开可访问的API)设计的核心。

  “附录C”中包含了满足该准则的设计规范示例。

  √DO要为主要功能领域定义主要使用场景。

  API规范应该包含描述主要场景的内容,并给出实现相应场景的代码示例。该内容应该紧跟在执行概览的后面,平均每个功能领域(如文件I/O)应该有5~10个主要场景。

  √DO要确保场景切合适当的抽象水平。它们应该和终端用户的使用情况大致一致。

  例如,从文件中读取数据是一个很好的场景,但是打开文件、从文件中读取一行文本或者关闭文件都不是好的场景,它们的粒度太细了。

  √DO要先为主要场景编写代码示例,再定义对象模型来支持这些代码示例。

  例如,要设计一个API来测量代码运行的时间,你可能会写出如下场景代码示例:

复制代码
//场景一:测量经过的时间
Stopwatch watch =Stopwatch.StartNew();
DoSomething();
Console.WriteLine(watch.Elased);

//场景二:重用Stopwatch
Dim watch As Stopwatch = Stopwatch.StartNew();
DoSomething();
Console.WriteLine(watch.ElapsedMilliseconds)

watch.Reset();
watch.Start();
DoSomething();
Console.WriteLine(watch.Elapsed);

通过这些代码示例可以得到如下对象模型:
public class Stopwatch{
  public static Stopwatch StartNew();
  
  public void Start();
  public void Reset();

  public TimeSpan Elapsed{get;}
  public long ElapsedMilliseconds{get;}
  ...
}
复制代码

 

&& JOE DUFFY 作为软件开发者,我们乐于创造强大且有意思的新功能,然后把它们分享给其他开发者,这正是API设计的乐趣之一。但是,退后一步,客观评估你所热衷的某个新功能在现实世界中是否真正发挥作用是机器困难的。要鉴别一个新功能是否被需要并确定期理想的使用方式,“使用场景”是我所知道的最佳方法。发掘场景实际上是非常困难的,因为它需要将技术能力和对客户需求的理解进行独特的结合。当你完成之后,你也只能基于本能的感受和直觉做出一系列决定,或许可以交付一些有用的API,但是仍然有风险做出令你自己后悔的决定。当有疑虑时,最好的方式就是把这个功能先拿出来,等更好地理解需求之后,再添加回去。
&&  STEPHEN TPUB  添加新的API很有趣,但是添加每一个API都是有一定的代价的。有时候,代价“只是”设计、开发、测试、写文档和维护功能(包括功能与运行时的结合)等工作。然而,遗憾的是,在通常情况下,当下添加的API实际上限制了将来某人添加其他更受欢迎的或更具影响力的API的能力,因为它的功能可能会和该API冲突。为此,我们在选择添加什么样的新API时需要深思熟虑,因为这有可能阻碍了属于未来的创新。如果一定要问“我希望我们从未添加这个API”这个问题,我敢说,我们中的许多人都能举出自己的例子。至少我就有一些。

  √DO要至少两种不同的语言来编写主要场景代码示例(如C#和F#)。

  最好确保所选择的语言有明显不同的语法、风格和能力。

&&  PAUL VICK 如果你正在编写一个可以供多种语言使用的框架,那么实际了解几种编程语言(都是C语言风格的编程语言不算数)是非常有益的。我们发现,有时候一个API只能在某一种编程语言中正常工作,因为涉及(和测试)这个API的人只懂得那一种编程语言。请多了解几种.NET语言,并按照设计好的正确方式来使用它们。在像.NET 框架一样的多语言平台上,期待全世界都只用你的语言是行不通的。

  √CONSIDER 建议使用动态类型语言如PowerShell或IronPython来编写住哟啊场景代码示例。

  设计不适合动态类型语言的API是很容易的。这类语言在处理泛型方法,以及依赖特性或需要创建强类型的API时通常会遇到问题。

  ×DON'T在设计框架的公开API时,不要只依赖标准设计方法。

  标准设计方法(包括面向对象设计方法)都是针对最终实现的可维护性来优化的,它们并没有针对所产出API的可用性进行优化。场景驱动设计结合原型设计、可用性调研和一定次数的迭代优化是一种更好的方式。

&&  CHRIS ANDERSON 每一个开发者都有属于自己的原则,使用其他建模方式也并没有什么根本性错误,问题往往是在于结果。框架设计最好的开始方式是编写那些你希望开发者去编写的代码——把它当成某种形式的测试驱动开发。你编写了最佳代码,它会反过来为你指出你期望构建的对象模型。

2.2.1.1  可用性调研

  在广大开发者中进行框架原型的可用性调研时场景驱动设计的关键。使用为主要场景而设计的API,对它们的作者来说可能很简单,但是对其他开发者来说并非一定如此。

  理解开发者会如何处理每一个主要场景可以帮助我们洞察框架的设计,了解它应该如何很好地满足所有目标开发者的需求。出于这个原因,进行可用性调研——以正式或非正式的方式——是框架设计流程的重要组成部分。

  如果在调研中发现大多数开发者都不能实现其中的某个场景,或者他们采取的方式和设计者期待的方式不一致,则表明该API应该被重新设计。

&& KRZYSZTOF   CWALINA  在.NET  Framework 1.0发布之前,我们没有对命名空间System.IO中的类型进行可用性测试。所以在发布后不久,我们收到了很多关于System.IO可用性的负面反馈。我们非常意外,并决定在8个随机用户中进行可用性调研,这8个人没有一个能成功在30分钟内从文件中读取出文本信息。我们认为,部分原因是文档搜索引擎存在问题,以及样本覆盖率不足。不过,更明显的是,API本身的可用性有问题。如果在产品发布前做过调研,我们就能够消除一大部分用户的不满,且能够避免在不引入破坏性变更的前提下试图修复主要功能区的API所带来的开销。
&& BRAD ABRAMS  对于API设计者来说,没有什么经历比坐在单向镜的后面,看着一个又一个开发者被自己设计的API挫败,最终无法完成任务,更能使他们深刻认识到其API的可用性问题。在1.0版本发布后的针对System.IO的可用性调研中,我自己经历了各种情绪。当看着一个又一个开发者不能完成这个简单的任务时,我的情感从傲慢到怀疑,然后是沮丧,最后下定决心去解决API中存在的问题。
&& CHRIS  SELLS  可用性调研可以正式的,前提是你有钱,有时间。实际上,通过找到一些接近这个库目标用户的开发者,让他们运行一下你提出的API,就可以得到80%的反馈结果。不要让“可用性调研”吓到你,以至于什么都不去做。你只需要把它当成这类“嗨,请帮忙看一眼这个”的调研来看待。
&&  STEVEN CLARKE 我们发现,与其花费大量精力来计划、设计和进行需要大量参与者的大规模调研来试图覆盖尽可能多的方面,不如在整个API开发过程中进行一系列较小的、更专注的调研。在每项调研中,我们只需要少量的参与者,专注于API的一个设计问题或一个领域。我们利用从中学到的经验知识对设计进行迭代,然后,在一两个星期后再针对更新的设计或API的其他领域发起另一项调研。这种持续学习的方式意味着,在整个设计过程中,有持续不断的源于客户的反馈在为我们提供信息,而不是在整个过程中的某个特定节点上一次性传递大量信息。

  理想的API可用性调研应该基于广大目标开发者群体所使用的真实的开发环境、编译器和文档,在现实情况下,最好是在产品周期的初期而不是后期进行可用性调研,所以不要因为产品还没准备好就推迟调研。

&&  STEPHEN TOUB  你甚至不需要真正的实现来了解API的可用性。然而,最好还是让开发者可以运行一些东西来查看他们的实验结果,早期的设计反馈可以通过对其进行编码和编译的API来获得,这意味着所有的实现都可以是no-ops或者throws;这不重要,因为它们不会被调用。开发者是否直观地找到了所涉及地相关类型?他们是否能够识别用于访问功能的模式?IntelliSense能否以有意义地方式帮助指导他们使用API?他们处理问题的方式是否和你假设的一样?他们是否经常搜索一些名字不同的东西?

  通常来说,正式的可用性调研对于小的开发团队和那些面向少量开发者的框架来说是不现实的。在这种情况下,非正式的调研是行之有效的方案。将一个初具雏形的库提供给不熟悉其设计的开发者,要求他们用30分钟写一个简单的程序,观察他们如何应对,这是找出那些最令人烦恼的API设计问题的有效方法。

  √DO要组织可用性调研来测试主要场景API。

  应该在开发周期的初期组织这些调研,因为严重的可用性问题往往需要进行大量的设计修改。在理想的情况下,大多数开发者都应该重新设计相关API。尽管重新设计是一种代价高昂的做法,但是我们发现,从长远来看,它实际上可以节省更多的资源,因为在不破坏现有代码的情况下修复不可用的API的成本是巨大的。

  下一节将介绍设计API的重要性,以便在最初接触的时候不会让人感到沮丧。这就是所谓的低门槛原则。