編者按:本文作者Tomas是F#語言的專家以及導師、計算機科學家,曾出版過有關F#的教程。本文重點介紹了如何設計組合化的庫以及如何避免在庫設計時進行回調。Tomas倡導以庫而不是框架的方式進行開發。以下為譯文。
框架 VS. 庫
框架和庫有什麼區別呢?兩者的主要不同之處在於如何使用它們以及編寫什麼樣的代碼。
框架和庫之間的區別可用上圖表示。框架定義了一個結構,你不得不將其填充好;而庫則需要你圍繞其提供的結構進行編碼。
為什麼不建議使用框架?
框架最大最顯著的弱點是不可組合。如果你正在使用兩個框架,這兩者之間往往是很難兼容的;誰包含誰,誰是誰的外延也是不清晰的。
如果是庫,情況則有所不同。因為你才是決策人,所以能夠同時調用不同的庫,雖然這會增加一定的編程複雜度,但至少是能夠實現的。
框架另一個大問題是很難進行測試和探索。在F#中,載入一個庫並透過不同輸入來檢查輸出和庫的運行是很有用的。例如,可以使用Web開發庫 Suave來啟動一個簡單的Web伺服器,代碼如下:
代碼片中首先載入了庫,然後以默認方式調用startWebServer。該方式是非常有用的,因為可以讓用戶嘗試不同的參數來對輸出結果進行對比。
框架還有個問題是控制了用戶代碼的結構。一個典型的例子是如果你正在使用一個框架,它會要求你繼承一些抽象基類然後運行具體的方法。例如XNA框架中的Game類(雖然XNA框架終止了,但是其模式在另一個框架繼續使用著):
class Game { abstract void Initialize(); abstract void Draw(DrawingContext ctx); abstract void Update();}在Initialize()中,需要對遊戲用到的資源進行預載;Update()在進行狀態刷新時會被反覆調用;Draw()則在更新屏幕時用到。這難道不正是命令式編程嗎?所以我們很可能會寫出類似的如下代碼:
代碼作用是讓人物往右移動。基於框架結構進行編程是有難度的,而這裡我使用了最直接的方法來實現。變量x表示的是人物位置,而mario則用於存放人物圖像資源。
雖然這在C#中或許會更加簡潔,但前提是要忽略全部的檢查。使用option目的是讓代碼更加安全(避免mario沒有定義就在Draw()中使用)。此外,誰能保證Initialize()一定在Draw()執行前就調用完畢?
如何避免框架錯誤
接下來我會講述如何使用庫而不是框架的具體原因。
即使你沒有使用F#來編寫庫,但是F#的交互性仍非常值得一試。F#不但可以用來編寫庫,其強大的交互性更使得庫的運用變得十分簡便。(如果是.NET平臺,可以嘗試 LINQPad)。
請看下面這個例子,它展示了如何使用
F#格式庫來把包含在文件夾中的F#腳本轉為HTML或對某單一文件進行操作。如果是第一次接觸,我會首先看有關庫的說明,然後打開命名空間找到Literate,然後進行嘗試,例如輸入「.」。
我認為良好的庫都支持類似的探索步驟。再看另外一個例子, FunScript ;用於把F#代碼轉為JavaScript。以下生成的JavaScript代碼作用是為異步循環進行計數,在<tiltle>頁面按秒執行:
類似地,我們都可以遵循上一個例子的學習途徑掌握到相關用法。
接下來再看兩個例子。第一個是以標準鍊表的方式對數據進行處理;另一個是使用上述鍊表方式讀取輸入,然後檢查數據,最後再進行處理。
這兩個例子有什麼不同之處呢?對於鍊表List函數,常常是一個單一函數作為一個參數使用。而這個函數是無狀態的。
在第二個例子中,指定了兩個函數。於我而言,這通常預示著有複雜的事情發生。其次,readAndProcess要求我們返回例子1中的字符串狀態,然後把字符串作為下一函數的輸入。這會引起一個潛在的問題。如果例子2需要例子1轉入其它狀態,該如何處理呢?
讓我們進入
readAndProcess來看它執行了什麼操作;首先是進行異常處理,然後對輸入進行檢查。如果對其進行改進,要如何做呢?我們不妨把它分解為兩個函數:
現在,validateInput變得簡化了,如果輸入是有效的則返回Some()的處理結果。而ignoreIOErrors函數仍作為參數使用。結合新函數,可以寫成:
代碼還是三行,但是更加清晰了,雖然比之前長了些。這樣一來程序變得到簡化,方便弄清楚其來龍去脈。
總的來說把函數作為參數使用是可以的,但是要注意儘量做到簡化。特別是牽涉到狀態的多次變化時,換另外一種處理方式或許會更好。
前面我們結合一個簡單的遊戲引擎講述了框架是如何影響我們編程的,如果在不使用可變域和執行指定類的情況下,又該如何處理呢?在F#中,可以嘗試異步工作流和基於事件的編程模型來代替。
其思路是使用觸發事件而不是編寫虛方法。因此,Game的定義變為:結合F#異步處理以及庫的主動控制特長,我們可把代碼改寫為:
代碼中首先對資源和Game對象進行了初始化;然後做了循環處理,使用AwaitObservable對Update或Draw事件進行監聽。雖然我們無法對遊戲狀態和屏幕更新進行控制,但是在初始化時我們是可以做到的,檢查遊戲何時運行以及等待事件的發生。
asnc{..}的使用是關鍵所在。我們可以使用AwaitObservable來實現在更新或重繪需要時恢復計算。這樣做的好處是可以實現更加複雜的操作,具體可參考這個例子Phil Trelford's Fractal Zoom。另外F#的agents代理可以實現類似的邏輯控制。
如果對F#不熟悉,或許會對上述代碼困惑。但我的目的是說明控制權掌握在自己手中的重要性,這樣可以寫出自己的抽象邏輯,這也是接下來要說的。
請再看看前述的Game例子,雖然低階抽象給予了充分的控制權,但是很多時候,我們希望寫出的遊戲是同時具備重繪和更新功能的。
這實現起來也不難,只需把某些部分作為參數使用:startGame抽象實現了在初始化時把兩個函數作為參數使用。Update函數進行狀態刷新,draw函數使用DrawingContext重繪狀態。這樣一來,我們的例子可變為4行代碼:
因此只要仔細閱讀startGame的代碼,按需對其進行改動,便可實現全權控制。對比於建基於一個脆弱庫之上的程序,這種方式難道不更穩定可靠嗎?
對於庫來說,可組合屬性是我們選擇它而不是框架的原因之一。例如FsLab,這是一個用於F#的數據科學庫(包括Deedle,Math.Net Numerics等),以單個腳本的方式連結呢其他的庫(原始碼)。
兩個簡單的例子是矩陣和框架的互轉Matrix.toFrame,Frame.toMatrix。該轉換操作起來是很簡單的,因為Deedle框架和Math.Net矩陣都能轉化為一個2維數組,所以通過數組可方便地實現兩者的互轉。因此,即使是很複雜的庫,我們都應該為用戶保留足夠的庫合成權以實現更強大的功能(或者改寫)。
寫在最後
本文著重從可組合和避免回調方面對庫和框架進行比較。進一步說,框架模式不僅存在於軟體,在日常生活也是經常遇到的。例如參團遊,從一開始,交通、住宿、遊玩行程等都已經被固定了;而自由行則類似於庫的組合,任何細節都需要親力親為,從而實現全權控制。雖然參團遊很方便,但是對於我,特別是軟體開發,我還是更傾向於我的地盤我做主!
本文為CSDN編譯整理,未經允許不得轉載,如需轉載請聯繫market#csdn.net(#換成@)