函数式风格应用在业务之重构日志函数(1)

本文中提到的库:Github-CkTools 目前正在内部设计中,最新代码在3.1.0.14-FP中
本文源码:源码地址:CkFunction_Log.cs

起因

最近摸鱼时想起了我的FP库(咕了一年多都还没发布正式版..),在搬砖中发现有个记录控制台日志的函数,觉得非常不清真:

public static Action<string> DefaultLog = debugInfo => Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {debugInfo}");
public static Action DefaultLogTime = () => Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");

这是我以前调试时为了记录日志写的委托函数,现在来看FP味道不浓,决定重构一下。

收获

  1. 再次验证函数式开发的好处就是天然拥抱扩展
  2. 锻炼提取核心逻辑的能力
  3. 给FP库又增加几块砖头

开始之前

不熟悉函数式风格的朋友可能疑惑为什么这里全是函数?而且我在文章中不提方法而是通篇函数

OOP中强调的是对象。将代码拟人化处理成字段/属性方法,其它的是这2个事物之间的组合,关心的主要是对象之间的关系。
FP中强调的是函数。没错,就是数学中的函数!关心的是数值数值之间的关系,在数学中不就叫函数么?关心的是数值一路上会被怎样的处理。

这俩没有绝对的谁好谁坏,只有谁更适合什么场景。实际中我喜欢将FP用在具体的战术层面,OOP用的战略层面。说人话就是开发某个具体功能是喜欢用FP,而设计技术框架、讨论系统架构时用OOP

分析

问题1:只能记录控制台日志,模版固定。比如想要加[]或记录事件号。
问题2:有时不想记录到毫秒,只需要记录到秒或天,现在没有地方可以修改。
问题3:模版固定后,写入部分想替换为文件或记到日志中心也不支持。
问题....

重构的时候没有项目经理限制,可以想的太多拉! 但函数式开发的好处就是天然拥抱扩展,在提取核心逻辑时只需要根据经验判断一下兼容性即可,而不需要像oop在开发时要考虑全盘。

1. 先找核心逻辑

日志系统的设计中微软的接口设计比较好,简单优雅,也和我经验中的日志接口差不多。
微软Loggin库的接口设计:

void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter);

他包含了:事件级别事件状态(某对象实例在某个时刻的数据)、异常格式化器
可以看到,只有2部分:要记录的数据、如何格式化,写入部分是利用不同的实现来完成。

日志方法无外乎3个部分: 写入的方式、内容模版、具体的msg,所以先提取这个函数出来:

	//普通版本-初始思路
	public static void Log( Action<string> log,Func<string, string> format,  Func<string> msg)
	{
		log(format(msg()));
	}
	
	//函数式版本
	public static Action<Action<string>,Func<string, string>,Func<string>> Log => (log,format,msg)=>log(format(msg()));
	//函数式+柯里化 FP库中改中这种   为什么这样看着麻烦的写法?后面就知道了
	public static Func<Action<string>, Func<Func<string, string>, Action<Func<string>>>> Log =>
		log =>
		format =>
		msg => log(format(msg()));

log函数:写入函数,执行写入过程。不管是控制台、文件、调用API记到日志中心,核心入口都是传递处理好的string类型。如果想实现一次记录到多处,组装好这个log函数即可。
format函数:格式化函数。控制台或文件一般是按行记录,调用API记录一般是格式化成json然后调用API记录,也属于将msg格式化成日志项的过程。
msg函数:消息函数。获取消息的函数。

2. 评估

我的经验知道的主要是3个日志方式: 1. 控制台 2. 文件 3. 调用http api记录到日志中心。
这3种可以归集为2类:

  1. 记录到本地:考虑格式化、存在何处
  2. 记录到远端:考虑格式化、存放何处、信息格式化、如何传递数据出去

log函数:包含了存放位置、如何传递、存在何处
format函数:包含格式化、格式化模版
目前看起来核心函数没问题了。

3. 先写最简单的-控制台日志

控制台的方法用的最多的就是Console.WriteLine 直接封包起即可。

	public static void ConsoleLog(Func<string, string> msgFormat, Func<string> msg)
	//{
	//    CkFunctions.Log(
	//        Console.WriteLine,
	//        msgFormat,
	//        msg);
	//}
	=> CkFunctions.Log(Console.WriteLine)(msgFormat)(msg)();

先用普通写法去梳理验证下思路,然后改为函数式。
从这里开始,我的其它函数更多是配置出来的。

修改为函数式风格:
控制台日志函数
.)
第345是FP库准备的方便函数,熟悉函数式的人可能更喜欢自己调用第2个函数或最早的Log构造属于自己的日志函数。
写法上变成了多个()来传递参数,这也是函数式风格中柯里化函数的一种用法,他的好处可以在后面提到,现在我相信你还有点蒙,觉得在炫技,向下看就知道了。

4. 再写文件日志

在开写文件日志函数之前,还需要思考2点:

  1. 和控制台日志应该参数类似或味道相同
  2. 文件日志比控制台日志还多了一个“文件判断”

第1点好满足,但是第2点需要专门写一个函数:

	public static Func<Func<string>, Action<string>> LogToFile =
		logFileName =>
			{
				string fileName = logFileName();
				CkFunctions.TryCreateFile(fileName);//一个简单函数,判断文件夹和地址是否存在,不存在则新建
				return msg => File.AppendAllText(fileName, msg);
			};

然后照猫画虎:
文件日志函数

5. 好处与多个括号

好处

从上面的截图中能我圈出来的部分,可以看到大体上都差不多,不同的函数也只是修改调用核心Log函数来构造不同的函数而已。
通常情况下修改参数而不修改代码是风险比较小的修改,这里不同的逻辑部分已经实现了替换参数就修改完毕,函数式里面一大核心是"函数是一等公民",而OOP的一等公民是"字段和方法"

多个括号

函数式的一大特点是"不是在构造函数就是在构造函数的路上",考虑的主要是不同函数的组合。在最后一个()之前的所有括号都是在构造函数,说人话就是调度程序逻辑或是组织程序逻辑,譬如下面代码是等价的:

		public static Func<Func<string>, Func<Func<string, string>, Action<Func<string>>>> FileLog4 =
			plogFileName =>
			pmsgFormat =>
			pmsg => CkFunctions.Log(CkFunctions.LogToFile(logFileName))(msgFormat)(msg);

		public static Func<Func<string>, Func<Func<string, string>, Action<Func<string>>>> FileLog5 =
			plogFileName =>CkFunctions.Log(CkFunctions.LogToFile(logFileName));

6. 缺点

目前能看到的缺点有:

  1. 注释不知道怎么写,有些非常不好翻译成白话
  2. 参数签名非常长,多个<>不容易区分

上面这2点不是函数式的缺点,是C#这种语言的缺点,换成F#或其它函数式语言就没有这种问题了。 C#本身是为OOP设计的,也是强类型的,所以参数多的函数会发现嵌套的非常深不容易阅读。参数多的问题在许多人的VS上面,写为var会好看的多,而函数式风格本身也会淡化类型,强化函数

函数式风格中,我写的这种嵌套多层的叫高阶函数,目的是方便柯里化,就能实现第5段中的那种效果,根据需要构造出不同的函数。 我在FP库也实现了普通函数转换成柯里化函数

对比代码 ```chsarp //柯里化函数 public static Func, Func, Action>>> FileLog4 = plogFileName => pmsgFormat => pmsg => CkFunctions.Log(CkFunctions.LogToFile(logFileName))(msgFormat)(msg);
	//普通函数
	 public static Action<Func<string>> FileLog5(
		Func<string> logFileName,
		Func<string, string> msgFormat,
		 Func<string> msg)
	{
		//do...
		return null;
	}
	//柯里化测试
	public static void CurryTest()
	 {
		Func<Func<string>, Func<string, string>, Func<string>, Action<Func<string>>> fileLog5 = CkFunctions.FileLog5;
		Func<Func<string>, Func<Func<string, string>, Func<Func<string>, Func<Action<Func<string>>>>>>? curry = fileLog5.Currying();
	}
</details>
嵌套非常多非常难阅读,在以后的文章中会利用`管道`来拼出函数优化这个问题,大体长这样:
```csharp
		public static Action<string> FileLog4 = msg=>
			.Pipe(CkFunctions.WriteLine)
			.Pipe(CkFunctions.DefaultLogFormat)
			.Pipe(msg)

总结

函数式风格对代码不一定有帮助,但对梳理背后的逻辑我认为还是非常有帮助的。
上面写完2套日志后,也对应评估中的3点。 以后需要扩充时只需要替换不同的函数即可(比如文件函数就是替换了Console.WriteLine)

posted @ 2022-05-16 18:02  长空X  阅读(104)  评论(0编辑  收藏  举报