孜孜不倦的程序员

多模式 .NET,第 3 部分:过程编程

Ted Neward

上个月,讨论的核心是将软件设计作为通用性和可变性的运用之一(请参阅 msdn.microsoft.com/magazine/gg232770)。讨论的结果是 C# 和 Visual Basic 等软件语言在不同的维度上提供不同的模式来表示这些通用性/可变性概念,并且多模式设计的核心是将域的需求和语言的功能相匹配。

本月,我们开始探讨编程语言较旧的功能之一“过程编程”,有时称为“结构化编程”,但两者有些细微的区别。虽然过程设计模式在现代软件设计中通常被视为一种老套的模式,因而已经过时并且没有用处,但出乎意料的是这种设计模式仍在很多地方得以应用。

老套模式的延续

在结构化编程作为新名词出现时,我们中有些人甚至还未出生。该编程方式的核心原则是在编写的代码中使用某些定义(结构)。在实际应用中,这意味着当时程序集中正在编写的代码块有“单个入口点”和“单个出口点”。这种编程方式的目标回顾起来非常简单:即对零散的重复代码应用某些高级别的抽象。

但是,这些命令(过程)往往需要引入一些变化才有用,而参数(即传入过程以改变执行方式的输入)成为引入变化的一种方式。先是非正式引入(“传递要在 AX 寄存器中显示的字符”),然后是正式引入(以函数参数方式,如 C/C++/C#/Java/Visual Basic 和类似语言中的参数)。过程往往要计算某种返回值,这些值有时派生自传入的输入,有些仅为指示成功或失败(如将数据写入文件或数据库时)。这些值也由编译器指定和处理。

所有这些内容对于多数读者来说只作为知识补习。多模式方法对我们的要求不是回顾历史,而是从通用性分析的角度对其重新进行研究。具体来说,就是过程方法中通用化了哪些内容,以及如何引入可变性?在确定可变性之后,还要知道这种可变性是何种类型,是正可变性还是负可变性?

有了通用性/可变性的视角,过程模式会产生一些有趣的秘密:通用性聚集为过程,本质上称为代码块,可以从任何上下文调用这些代码块。(过程语言严重依赖“作用域”,以将过程内的工作与外部上下文隔离。)在过程中引入可变性的方法之一是通过参数(如指示如何处理剩余参数)引入,这种方法可以实现正可变性或负可变性,具体取决于如何编写过程本身。如果该过程的源代码不可用,或由于某些原因不可修改,则仍可以通过创建新过程实现可变性,新过程可以调用也可以不调用旧过程,具体取决于需要正可变性还是负可变性。

Hello 过程

实际上,过程提供通用行为,该行为可根据输入变化。具有讽刺意味的是,我们见到的过程模式的第一个示例就在多数 Microsoft .NET Framework 程序员都会看到的第一个示例中:

Sub Main() Console.WriteLine("{0}, {1}!", "Hello", "world!") End Sub

在 WriteLine 实现中,开发人员传递一个格式字符串,描述打印的内容及方式,其中包含替换标记中的格式命令,具体如下:

Sub Main() Console.WriteLine("Hello, world, it's {0:hh} o'clock!", Date.Now) End Sub

WriteLine 的实现提供一个有趣的研究案例,因为该实现与其前身(C 标准库中的 printf)有某些区别。 回忆一下,printf 使用不同的格式标记指定相似的格式字符串类型,并直接写入控制台(STDOUT 流)。 如果程序员希望将格式化的输出写入文件或字符串,则必须调用 printf 的不同变体:对于文件输出调用 fprintf,对于字符串调用 sprintf。 但输出的实际格式是相同的,C 运行时库常常利用这一点,创建一个通用的格式函数,然后将结果发送到最终目标,这是通用性的一个完美示例。 但是,此种格式设置行为被一般的 C 开发人员视为封闭的行为,该行为不能进行扩展。 .NET Framework 又向前迈进了一步,使开发人员能够创建新的格式设置标记,方法是将责任转移给在格式字符串之后传递到 WriteLine 的对象。 如果对象实现 IFormattable 接口,则需负责找出格式设置标记,并返回一个格式正确的字符串以进行处理。

可变性也可能隐藏在过程方法的其他位置。 对值进行排序时,qsort(一种 Quicksort 实现)过程需要帮助才能知道如何比较两个元素,以确定两个元素的相对大小。 在源不可修改时要求开发人员编写自己的 qsort 包装,这是一种传统的可变性机制,但这样做非常笨拙和困难。 幸运的是,过程模式提供了另一种方法,该方法就是后来称为“控制反转”的早期形式:C 开发人员将指针传入函数,qsort 在运行时调用该函数。 这其实是参数作为可变性方法的一种变体,这是一种开放的可变性方法,因为可以使用任何过程(只要该过程与参数及返回类型预期相符)。 此种方法最初应用甚少,但随着时间的推移,此模式的方法(通常称为“回调”)变得日益流行。到 Windows 3.0 发布时,该方法成为主流方法,也成为编写 Windows 程序时的必用方法。

Hello 服务

最有趣的部分是,过程模式取得广泛成功的地方(当然是在忽略 C 标准库无处不在、取得巨大成功的前提下)都是在面向服务的领域。 (我在此处使用“服务”一词表示软件的更广泛的集合,而不是传统的狭义上的基于 WS-* 或 SOAP/Web 服务描述语言 [WSDL] 的服务;基于 REST 的实现和 Atom/RSS 实现也十分符合这个定义)。

根据 msdn.com 以前登载的内容,如“面向服务的设计原则”(msdn.microsoft.com/library/bb972954),服务遵守四个基本原则:

  • 边界是显式的。
  • 服务具有自治性。
  • 服务共享架构和合约,但不共享类。
  • 服务兼容性基于策略。

这些原则可能在无意中强调了服务的以下特性,即服务属于过程设计模式而非面向对象的模式。 “边界是显式的”强调的概念是:服务是独立的实体,区别于调用它的系统;此观点被以下概念强化:“服务是自治的”,所以服务间互相独立,最好在基础结构管理级别也是如此。 “服务共享架构和合约,但不共享类”表示服务根据发送给自身的参数(表示为 XML 或 JSON 结构)进行定义,而不是特定编程语言或平台的具体运行时类型。 最后,“服务的兼容性基于策略”表示服务必须基于策略声明兼容,策略声明提供有关调用上下文的更多信息,过程模式已经从周围环境中假设了这些信息,因此不用显式定义这些信息。

开发人员可能很快指出:在经典的基于 WSDL 的服务中,创建可变性将更加困难,因为服务会绑定到输入类型的架构定义。 但这是最基本的(或可生成代码的)服务的情况,这些情况下,输入和结果类型可以(并常常)在不同服务定义间重用。 其实,如果将服务的概念加以扩展,使其包含基于 REST 的系统,则服务可以接受任何数量的各种输入类型(实际上,过程参数是开放和可以解析的,这一点在传统的静态类型过程中并不多见),从而产生不同的行为,继而再次突出服务的可变性。 当然,该行为本身需要具有某些可验证性,因为服务的 URL(其名称)不会永远适合抛给它的任何类型的数据。

从消息系统的角度看待服务时(如 BizTalk、ServiceBus 或其他企业服务总线),其过程性仍然存在,但整个可变性将依赖于传递的消息,因为整个消息只携带调用上下文,甚至不含调用的过程的名称。 这还意味着可变性机制(我们使用该机制在新过程中包装其他过程,以引入或限制可变性)也不再存在,因为我们通常不会控制消息在总线中传递的方式。

过程模式获得成功

过程模式展示出了最早赋予通用性/可变性的性质:

  • 名称和行为。 名称传递着意义。 我们可以使用名称的通用性将具有相同意义的项目(如过程/方法)分为一组。 其实,“现代”语言已经允许我们更加正式地捕获这种关系,因为我们可以为参数数量和/或类型不同的不同方法指定相同的名称,这就是方法重载。 C++、C# 和 Visual Basic 也可以利用恰当命名的方法,采用在代数学上易于理解的名称创建方法,这就是运算符重载。 F# 进一步发挥了此功能,允许开发人员创建新的运算符。
  • 算法。 算法不仅仅是数学计算,更是对执行步骤的重复。 如果自上而下观察整个系统(而不是观察单个层),即开始出现有趣的过程/代码段(实际为用例)并形成系列。 标识这些步骤(过程)之后,在输入不同类型的数据/参数时,算法/过程的运行方式发生变化,系列会产生可变性。 在 C#、F# 和 Visual Basic 中,可以将这些算法置于基类中,然后通过继承基类,替换基类的行为而获得算法的可变性,这就是方法重载。 您也可以自定义算法行为,方法是不指定而是传入某些行为,即使用委托作为控制反转或回调。

结束本文之前,指出一个最后需要注意的问题。 过程模式和面向服务的世界不是一对一的关系。其实,很多面向服务体系结构的推广者和支持者会尽可能拒绝与过程模式产生任何关系,因为他们担心这会影响他们的既得利益。 撇开政治因素,经典的服务(如基于 REST 的服务或基于 SOAP/WSDL 的服务)与经典的过程模式有惊人的相似之处。 因此,在服务设计过程中使用相同的通用性分析有助于创建可接受的粒度级别,但设计者必须注意确保不会轻易忽略(假定的)网络遍历,以便在服务主机位置执行服务。 请特别注意,使用过程模式直接实现服务时可能会尝试使用“传递回调”可变性方法,虽然这不完全是很糟糕的做法,但它可能带来重大瓶颈和性能问题。

至今,过程模式仍出现在大量编程过程中,但它隐藏在表面之下,使用假定的名称避开开发人员。 我们的下一个主题,即面向对象编程的风格将迥然不同,它开放、随和,不像前面的过程模式那样忧郁、情绪化和常常被忽略。 下个月的文章中,我们将开始分析对象的通用性/可变性方面,我们的发现将给您带来惊喜。

同时,作为一项智力测验,建议您注意一下使用的各种工具,并判断其中哪些工具在很大程度上采用了过程方法。 (提示:其中有两个工具您在编写软件时每天都使用:编译器和 MSBuild,MSBuild 是隐藏在 Visual Studio 中的“生成”按钮背后的生成系统。

像往昔一样,快乐编程!

Ted Neward 是 Neward & Associates 的负责人,这是一家专门研究企业 Microsoft .NET Framework 系统和 Java 平台系统的独立公司。他曾写过 100 多篇文章,是 C# 领域最优秀的专家之一;是 INETA 发言人;并且著作或合著过十几本书,包括《Professional F# 2.0》(Wrox,2010 年)。此外,他还定期提供咨询和指导。您可以通过 ted@tedneward.com 向他提问或咨询,也可以访问他的博客 (blogs.tedneward.com)。

衷心感谢以下技术专家对本文的审阅:Anthony Green