c#函数式编程 Functional Programming in C# [5]

1.5 使用 HOF 避免重复

  例如,与数据库的交互需要一些设置来获取和打开一个连接,并在交互之后进行一些清理以关闭连接并将其返回到底层连接池。在代码中,它看起来像下面这样。

清单1.12 连接到一个数据库需要进行一些设置和拆解

string connString = "myDatabase";
//设置:获取并打开连接
var conn = new SqlConnection(connString));
conn.Open(); // interact with the database...
//拆解:关闭并释放连接
conn.Close();
conn.Dispose()

  无论你是读还是写数据库,或执行一个或多个操作,设置和拆除都是相同的。 前面的代码通常是用一个using块来写的,像这样:

using (var conn = new SqlConnection(connString))
{   
	conn.Open();
	// interact with the database...
}

  这既短又好,但本质上仍然相同。考虑以下简单 DbLogger 类的示例,该类具有几个与数据库交互的方法:Log 插入给定的日志消息,GetLogs 检索自给定日期以来的所有日志。

清单 1.13 设置/拆卸逻辑的重复

using Dapper; //将 Execute 和 Query 作为连接上的扩展方法公开
// ...
publicclass DbLogger {
    string connString; //假设这是在构造函数中设置的。
    publicvoid Log(LogMessage msg) {
        using(var conn = new SqlConnection(connString)) {// 设置
        	//将日志消息持久化到数据库
            int affectedRows = conn.Execute("sp_create_log", 
            	msg, commandType: CommandType.StoredProcedure);
        }//拆解作为 Dispose 的一部分执行
    }
    public IEnumerable < LogMessage > GetLogs(DateTime since) {
        var sqlGetLogs = "SELECT * FROM [Logs] WHERE [Timestamp] > @since";
        using(var conn = new SqlConnection(connString)) {// 设置
        	//查询数据库并反序列化结果
            return conn.Query < LogMessage > (sqlGetLogs, new {
                since = since
            });
        }//拆解
    }
}

  注意到这两个方法有一些重复,即setup和tealdown逻辑。我们能不能去掉这些重复?
  与数据库交互的具体细节与本次讨论无关,但如果你感兴趣,代码使用了Dapper库(记录在GitHub上:https://github.com/StackExchange/dapper-dot-net),它是ADO.NET之上的一个薄层,允许你通过一个非常简单的API与数据库交互。

  • Query 查询数据库并返回反序列化的 LogMessages。
  • Execute 运行存储过程并返回受影响的行数(我们忽略)。

  这两种方法都被定义为连接上的扩展方法。更重要的是,注意在这两种情况下,数据库交互如何依赖于获取的连接并返回一些数据。这将允许您将数据库交互表示为从 IDbConnection 到“某物”的函数。

异步I/O操作在现实世界中,我建议你总是异步执行I/O操作(所以,在这个例子中,GetLogss应该真正调用QueryAsync并返回一个Task<IEnumerable< LogMessage > >)。
但是,当你试图学习FP已经具有挑战性的思想时,异步性增加了一个复杂的层次,这对你没有帮助。 出于教学目的,我将等到第13章再来讨论异步。

  如您所见,Dapper 公开了一个令人愉快的 API,它甚至会在必要时打开连接。但是你仍然需要创建连接,一旦你完成它,你应该尽快处理它。结果,您的数据库调用的内容最终夹在执行设置和拆卸的相同代码段之间。让我们看看如何通过将设置和拆卸逻辑提取到 HOF 中来避免这种重复。

1.5.1 将设置和拆卸封装到 HOF 中

  您正在寻找一个函数来执行设置和拆卸,并且参数化了在两者之间做什么。这是 HOF 的完美场景,因为您可以用函数表示其间的逻辑。从图形上看,它看起来像图 1.8。
在这里插入图片描述
  因为连接建立和拆除比 DbLogger 更通用,所以它们可以被提取到一个新的 ConnectionHelper 类中。

清单 1.14 将数据库连接的设置和拆卸封装到 HOF 中

using System;
using System.Data;
using System.Data.SqlClient;
public static class ConnectionHelper {
    public static R Connect <R> (string connString, Func <IDbConnection, R> f) {
        using(var conn = new SqlConnection(connString)) {//设置
            conn.Open();
            return f(conn);//中间发生的事情现在被参数化了。
        }//拆解
    }
}

  Connect函数执行设置和拆除,它的参数是它在两者之间应该做什么。函数主体的签名很有意思;它接收一个IDb-Connection(通过它与数据库交互),并返回一个通用对象R。在我们看到的用例中,R在查询的情况下是IEnumerable< LogMessage >,在插入的情况下是int。现在你可以在DbLogger中使用连接函数,如下所示:

using Dapper;
using static ConnectionHelper;
publicclass DbLogger {
    string connString;
    public void Log(LogMessage message) => Connect(connString, c => c.Execute("sp_create_log", message, commandType: CommandType.StoredProcedure));
    public IEnumerable < LogMessage > GetLogs(DateTime since) => Connect(connString, c => c.Query < LogMessage > (@ "SELECT *         FROM [Logs] WHERE [Timestamp] > @since", new {
        since = since
    }));
}

  您摆脱了 DbLogger 中的重复,DbLogger 不再需要了解有关创建、打开或处置连接的详细信息。

1.5.2 将 using 语句转换为 HOF

  前面的结果是令人满意的。但是,为了把HOF的想法更进一步,让我们更激进一点。using语句本身不就是一个setup/teardown的例子吗?毕竟,一个using块总是做以下事情:

  • 设置——通过评估给定的声明或表达式来获取 IDisposable 资源
  • Body——执行块内的内容
  • Teardown - 退出块,导致在设置中获取的对象上调用 Dispose

  所以…是的,它是! 至少可以这么说。 设置并不总是相同的,所以它也需要被参数化。然后,我们可以写一个更通用的设置/拆解的HOF,执行使用仪式。
  这就是属于库中的那种广泛可重用的函数。在本书中,我将向你展示许多这样的可重用结构,这些结构已经进入了我的LaYumba.Functional库,使你在进行功能编码时有更好的体验。

清单1.15 一个可以用来代替using语句的HOF

using System;
namespace LaYumba.Functional {
    public static class F {
        public static R Using < TDisp, R > (TDisp disposable, 
        	Func < TDisp, R > f) where TDisp: IDisposable {
            using(disposable) return f(disposable);
        }
    }
}

  前面的列表定义了一个名为F的类,它将包含我们函数库的核心函数。我们的想法是,这些函数应该通过使用static来实现,正如下一个代码示例中所示。
  这个Using函数需要两个参数:第一个是 disposable 的资源,第二个是在资源被disposed前要执行的函数。有了这个函数,你可以更简洁地重写Connect函数:

using static LaYumba.Functional.F;
public static class ConnectionHelper {
    public static R Connect < R > (string connStr, Func < IDbConnection, R > f) => Using(new SqlConnection(connStr), conn => {
        conn.Open();
        return f(conn);
    });
}

  第一行的 using static 使你能够调用 Using 函数,作为 using 语句的一种全局替代。注意,与using语句不同,调用Using函数是一个表达式。

  • 它允许你使用更紧凑的以表达式为主体的方法语法。
  • 一个表达式有一个值,所以Using函数可以与其他函数组合。

  我们将在第5.5.1节中更深入地探讨构成和语句与表达式的概念。

1.5.3 HOF 的权衡

  让我们看看你通过比较DbLogger中的一个方法的初始版本和重构版本所取得的成果:

// initial implementation
public void Log(LogMessage msg) {
    using(var conn = new SqlConnection(connString)) {
        int affectedRows = conn.Execute("sp_create_log", msg, commandType: CommandType.StoredProcedure);
    }
} 
// refactored implementation
public void Log(LogMessage message) => 
	Connect(connString, c => c.Execute("sp_create_log", message, commandType: CommandType.StoredProcedure));

  这是一个很好的例子,说明你可以从使用以一个函数为参数的HOF中得到好处:

  • 简洁性 —— 新版本显然更加简洁。 一般来说,越是错综复杂的设置和需要的范围越广,你把它抽象成一个HOF就越有好处。
  • 避免重复 —— 整个设置/卸载逻辑现在在一个单独的地方执行。
  • 分离关注点 —— 你已经设法将连接管理隔离到ConnectionHelper类中,所以DbLogger只需要关注特定的日志逻辑。

  让我们看看调用栈是如何变化的。在原来的实现中,对Execute的调用发生在Log的堆栈帧上,而在新的实现中,它们相隔四个堆栈帧(见图1.9)。

在这里插入图片描述
  当Log执行时,代码调用Connect,将回调函数传递给它,以便在连接准备好时调用。Connect反过来将回调重新打包成一个新的回调,并将其传递给Using。
  所以,HOFs 也有一些缺点:

  • 你增加了堆栈使用。有性能影响,但可以忽略不计。
  • 由于回调,调试应用程序会更复杂一些。

  总的来说,对DbLogger所做的改进使其成为一个值得权衡的选择。
  你现在可能已经同意,HOF是非常强大的工具,尽管过度使用会使人难以理解代码在做什么。在适当的时候使用HOF,但要注意可读性:使用短的lambdas,清晰的命名,以及有意义的缩进。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值