天天看點

#函數式程式設計 Functional Programming in C# [34]

7.4 建立對部分應用程式友好的 API

  現在您已經了解了部分應用的基本機制,以及如何通過使用 Funcs 而不是方法來解決糟糕的類型推斷,我們可以繼續進行更複雜的場景,在該場景中我們将使用第三方庫和真實的世界要求。

  部分應用程式的一個好場景是,當一個函數需要一些在啟動時可用且不會改變的配置,以及随着每次調用而變化的更多瞬态參數。在這種情況下,引導元件可以提供配置參數,獲得隻需要調用特定參數的專用函數。然後可以将其提供給功能的最終消費者,是以無需了解有關配置的任何資訊。

  在本節中,我們将看這樣一個示例:通路 SQL 資料庫。想象一個應用程式,與大多數應用程式一樣,需要使用不同的參數執行大量查詢,以從資料庫中檢索不同類型的資料。

  讓我們從部分應用的角度考慮這個:

  • 想象一個用于檢索資料的非常通用的函數。
  • 它可以被具體化,以查詢一個特定的資料庫。
  • 它可以被進一步具體化,以檢索特定類型的對象。
  • 它可以通過一個給定的查詢和參數進一步具體化。

  讓我們通過一個簡單的例子來探讨這個問題:想象一下,我們希望能夠通過ID加載Employee,或者通過姓氏搜尋Employee。我們需要實作這些類型的函數:

lookupEmployee : Guid -> Option< Employee >

findEmployeesByLastName : string -> IEnumerable< Employee >

  實作這些功能是我們的進階目标。在底層,我們将使用 Dapper 庫來查詢 SQL Server 資料庫。為了檢索資料,Dapper 公開了具有以下簽名的 Query 方法:

public static IEnumerable<T> Query<T>
	( this IDbConnection conn,
		 string sqlQuery,
		 object param = null,
		 SqlTransaction tran = null,
		 bool buffered = true)
           

  表 7.1 列出了我們在調用 Query 時需要提供的參數,包括通用參數 T。我們不會擔心剩餘的參數,因為預設值就可以滿足我們的目的。

表 7.1 Dapper 的 Query 方法的參數
T 應從查詢傳回的資料填充的類型。在我們的例子中,這将是 Employee——Dapper 自動将列映射到字段。
連接配接 與資料庫的連接配接。 (請注意,Query 是連接配接上的擴充方法,但就部分應用而言,這無關緊要。)
sqlQuery 這是您要執行的 SQL 查詢的模闆,例如“SELECT*[email protected]”——注意@Id 占位符。
參數 一個對象,其屬性将用于填充 sqlQuery 中的占位符。例如,前面的查詢将需要相應的 param 對象包含一個名為 Id 的字段,該字段的值将在 sqlQuery 而不是 @Id 中進行評估和呈現。

  這是一個關于參數順序的很好的例子,因為連接配接和SQLquery可以作為應用程式設定的一部分來應用,而param對象将特定于對Query的每次調用。對嗎?

  呃……好吧,實際上,錯了! SQL 連接配接是輕量級對象,應該在執行查詢時擷取和處理。事實上,正如您在第 1 章中所記得的那樣,Dapper 的 API 的标準使用遵循以下模式:

using (var conn = new SqlConnection(connString))
{   
	conn.Open();
	var result = conn.Query("SELECT 1");
}
           

  這意味着我們的第一個參數連接配接不如第二個參數 SQL 模闆通用。但一切都沒有丢失。請記住,如果您不喜歡現有的 API,您可以更改它!這就是擴充卡函數的用途。接下來,我們将編寫一個更好地支援部分應用程式的 API,以建立檢索我們感興趣的資料的專用函數。

7.4.1 作為文檔的類型

  讀取資料的最通用參數是連接配接字元串。許多應用程式連接配接到單個資料庫,是以連接配接字元串在應用程式的整個生命周期中永遠不會改變,并且可以在應用程式啟動時從配置中一次性讀取。

  讓我們應用第3章中介紹的一個想法,即我們可以使用類型來使我們的代碼更具表現力,并為連接配接字元串建立一個專用類型。

清單 7.5 連接配接字元串的自定義類型
public class ConnectionString {
    string Value {
        get;
    }
    public ConnectionString(stringvalue) {
        Value = value;
    }
    public static implicit operator string(ConnectionString c)  // 與字元串的隐式轉換
    	=> c.Value;
    public static implicit operator ConnectionString(string s)  // 與字元串的隐式轉換
    	=> new ConnectionString(s);
    public override string ToString() => Value;
}
           

  每當一個字元串不僅僅是一個字元串,而是一個DB連接配接字元串時,我們就會把它包裹在一個ConnectionString中。這可以通過隐式轉換來完成,非常簡單。

  例如,在啟動時,我們可以從配置中填充它,如下所示:

ConnectionString connString = configuration
	.GetSection("ConnectionString").Value;
           

  同樣的想法也适用于 SQL 模闆,是以我也按照相同的方式定義了 SqlTemplate類型。大多數強類型函數式語言允許您根據内置類型定義自定義類型,如下所示:

  在 C# 中,這有點費力,但仍然值得付出努力。首先,它使你的函數簽名更能揭示意圖:你正在使用類型來記錄你的函數所做的事情。例如,一個函數可以聲明它依賴于一個連接配接字元串,如下所示。

清單 7.6 使用自定義類型時函數簽名更加明确
public Option<Employee> lookupEmployee
	(ConnectionString conn, Guid id) => //...
           

  這比依賴字元串要明确得多。

  第二個好處是您現在可以在 ConnectionString 上定義擴充方法,這對字元串沒有意義。接下來你會看到這一點。

7.4.2 具體化資料通路功能

  現在我們已經了解了連接配接字元串的表示和擷取,讓我們看看接下來是什麼,從一般到具體:

  • 我們要檢索的資料類型,例如 Employee
  • SQL查詢模闆,如“ SELECT * FROM EMPLOYEES WHERE ID= @Id ”
  • 将用于呈現 SQL 模闆的 param 對象,例如 new{Id=“123”}

  現在是解決方案的關鍵。我們可以定義一個擴充方法 onConnectionString 來擷取我們需要的參數。

清單 7.7 更适合部分應用的擴充卡函數
using static ConnectionHelper;
public static class ConnectionStringExt {
    public static Func < SqlTemplate, object, IEnumerable < T >> Query < T >
    	 (this ConnectionString connString)
    	 	 => (sql, param)
    	 	 => Connect(connString, conn => conn.Query < T > (sql, param));
}
           

  注意,我們依賴于ConnectionHelper.Connect,我們在第一章中實作了它,它在内部負責打開和處理連接配接。如果你不記得實作的細節也沒有關系,隻要注意到這裡一般的、不改變的連接配接字元串是第一個參數,而連接配接對象本身是短暫的,每次查詢都會被建立。

  這是上述方法的簽名:

ConnectionString -> (SqlTemplate, object) -> IEnumerable< T >

  也就是說,一旦我們提供了一個連接配接字元串,我們就會得到一個函數,這個函數在傳回一個被檢索的實體清單之前還在等待兩個參數。 同時注意到,将Query定義為一個擴充方法是一個小技巧,它允許我們在查詢的類型之前指定連接配接字元串。否則就不可能 "推遲 "解決一個方法的類型參數。

  Query 的這個定義是 Dapper 的 Query 函數之上的一個薄墊片。它提供了一個對部分應用程式友好的 API,原因有二:

  • 這次的論點真正從一般到具體。
  • 提供第一個參數會産生一個 Func,它解決了應用後續參數時的類型推斷問題。

  我們現在可以提供零散的參數來獲得我們開始定義的函數:

清單 7.8 提供參數以獲得所需簽名的函數
ConnectionString connString = configuration
	.GetSection("ConnectionString").Value;
SqlTemplate sel = "SELECT * FROM EMPLOYEES", 
	sqlById = $ "{sel} WHERE ID = @Id", 
	sqlByName = $ "{sel} WHERE LASTNAME = @LastName";
// (SqlTemplate, object) -> IEnumerable<Employee>
var queryEmployees = conn.Query < Employee > (); //連接配接字元串和檢索類型是固定的。
// object -> IEnumerable<Employee>
var queryById = queryEmployees.Apply(sqlById); // 要使用的 SQL 查詢是固定的。
// object -> IEnumerable<Employee>
var queryByLastName = queryEmployees.Apply(sqlByName); // 要使用的 SQL 查詢是固定的。
// Guid -> Option<Employee>
Option < Employee > lookupEmployee(Guid id)  // 我們開始實施的功能
	=> queryById(new { Id = id }).FirstOrDefault(); 
// string -> IEnumerable<Employee>
IEnumerable < Employee > findEmployeesByLastName(string lastName) // 我們開始實施的功能
	=> queryByLastName(new { LastName = lastName });
           

  在這裡,我們通過對之前讨論的查詢方法進行參數化來定義queryEmployees,以使用一個特定的連接配接字元串并檢索Employees。它仍然可以進一步參數化,是以我們提供兩個不同的SqlTemplate來獲得queryById和queryByLastName。

  我們現在有了兩個期望得到一個參數對象的單數函數(它包裝了将被用來替換SqlTemplate中的占位符的值)。剩下的就是定義lookupEmployee和findEmployeesByLastName,并使用我們在本節開始時設定的簽名來公開。這些隻是作為擴充卡函數,将其輸入參數轉換為一個适當的參數對象。

  請注意,我們從一個非常通用的函數開始,用于對任何 SQL 資料庫運作任何查詢(它隻是 Dapper 的 Query方法之上的一個擴充卡,為我們提供更适合的 API),而我們最終得到了高度專業化的函數。