本文章是由機器翻譯。

程式設計師雜談

多範型 .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