函数式风格应用在业务之重构日志函数(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味道不浓,决定重构一下。
收获
- 再次验证函数式开发的好处就是
天然拥抱扩展
。 - 锻炼提取核心逻辑的能力
- 给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类:
- 记录到本地:考虑格式化、存在何处
- 记录到远端:考虑格式化、存放何处、信息格式化、如何传递数据出去
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点需要专门写一个函数:
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. 缺点
目前能看到的缺点有:
- 注释不知道怎么写,有些非常不好翻译成白话
- 参数签名非常长,多个
<>
不容易区分
上面这2点不是函数式的缺点,是C#这种语言的缺点
,换成F#或其它函数式语言
就没有这种问题了。 C#本身是为OOP设计的,也是强类型的,所以参数
多的函数会发现嵌套的非常深不容易阅读。参数多的问题在许多人的VS上面,写为var
会好看的多,而函数式风格本身也会淡化类型
,强化函数
。
函数式风格中,我写的这种嵌套多层的叫高阶函数
,目的是方便柯里化
,就能实现第5段中的那种效果,根据需要构造出不同的函数。 我在FP库也实现了普通函数
转换成柯里化函数
:
对比代码
```chsarp //柯里化函数 public static Func //普通函数
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
)