可控函式(Controllable Function),是指那種可以通過改變函式的引數或者簡單的修改函式在執行過程中的外部依賴的而產生確定結果的一類函式,這是我根據純函式的概念推廣出來的一個新的名詞(大概是新的吧?)。
為什麼需要可控函式
純函式在函數語言程式設計的世界中非常的普遍,而且最近它也逐漸在向物件導向的世界中展現出越來越多的魅力,你甚至可以在一些框架或者技術的文件中看到文件作者對純函式的推崇。在我看來,純函式能夠吸引到更多開發者的關注的一個重要的原因就是它更容易測試,因為我們可以非常肯定的確定在給定輸入的情況下純函式的輸出。但是在物件導向的世界中,因為有類的存在,所以我們經常編寫的函式(或者說是方法)很難設計成為純函式 —— 畢竟我們總是會在類的方法中訪問類的其他成員。
而可控函式則在對外界環境的訪問以及修改的限制較為寬鬆。不知你是否還記得人教版高中生物書中提到的“控制變數法”,在設計實驗的時候,我們需要儘可能地做到僅改變單個對試驗結果產生影響的變數。編寫單元測試也是這樣,如果我們能夠非常簡單的控制某個函式的全部依賴(包括函式引數以及外部依賴,例如,類的其他成員)的變化,那麼我們就可以很容易地使用控制變數法設計我們的測試用例,並簡化劃分等價類的過程,這對於編寫白盒測試來說,好處不言而喻。
是什麼讓你的函式不可控
如果函式依賴“系統當前時間”這樣的無法使用程式碼來控制的外部變數的話,那麼這個函式常常會變得不可控。因為在大多數的程式語言或者技術框架中,修改當前系統時間或者 Mock 獲取系統時間的全域性變數往往是很困難的。像 JS 可以非常容易的 Mock window.Date
類,但是在 C# 中,Mock DateTime
會非常的艱難。
另一種讓函式成為脫韁野馬的情況很可能是它依賴了過於複雜的外部物件,例如 EF Core 的 DbContext
。DbContext
很難被完美的 Mock,不管是 InMemory
還是 SQLite InMemory
,都有其限制所在。就更不要說查詢姿勢(是否 AsNoTracking
,是否使用了第三方 EF 擴充)會對真正的執行過程產生的影響了。本來沒有那麼複雜邏輯,卻因為引入的複雜的外部物件而變得難以預測了。
重新掌控你的函式
在這裡我想先介紹一款程式語言 —— Elm
,在 Elm 的基礎類庫中,它很反常的沒有提供直接獲取系統時間的函式,相反地,Elm 的設計者認為獲取系統時間並不是一個純函式,因為我們沒辦法在不同時刻讓這個函式返回相同的值。為了描述這一現象,Elm 將這個函式實現為了一種副作用,簡單來說就是開發者需要是使用類似於 Callback 的機制來獲取系統當前時間,這樣就強迫我們解除了對系統當前時間的強依賴,因為我們只能通過 Callback 的引數來獲取真正的系統時間。
模仿 Elm 的理念,在使用我們無法使用程式碼來控制的外部變數的時候,我們可以使用“包裝一層”的方法來解決強依賴的問題。例如,在物件導向的世界中,可以通過使用一個 ISystemTimeProvider
介面來獲取當前的系統時間,它就像 Elm 中的 Callback 一樣,躲在一個抽象層後面,默默無聞地為我們的程式碼提供當前系統的準確時間。
除了上面提到的我們無法控制的外部依賴之外,還有一些外部依賴是我們難以控制的,它們往往來自於我們所使用的第三方類庫,並且它們的組成也通常是比較複雜的,例如 AspNetCore 中的 HttpContext
以及 DbContext
。可以想象,如果一個函式接收這種型別的引數,那麼如何去建立一個期望的輸入就變成了一件困難的事了。這種時候我們可以試著 Keep it simple and stupid。例如,當我們只需要使用 DbContext
的查詢結果的時候,直接讓查詢函式返回來自 BCL 的型別而不是 IQueryable<T>
,這樣,依賴查詢結果的函式就可以擺脫對 DbContext
的依賴了。