天天看点

#函数式编程 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),而我们最终得到了高度专业化的函数。