函数式风格应用在业务之异常处理(2)

本文中提到的库:Github-CkTools 目前正在内部设计中,最新代码在3.1.0.14-FP中
为什么不是说函数式编程? 因为C#本身不是为函数式设计的,目前仅仅是能在普通业务开发中模仿函数式味道。

起因

今天在调整FP库时想到函数式风格一大特点是支持热拔插,而最需要热插拔的应该是异常处理
毕竟许多人写代码的时候不会先考虑异常,而是先写主体思路,然后调试,然后在可能的地方加。 我知道有许多老手异常代码流考虑的非常完善,但我个人不是处处异常处理的习惯,一般只在入参或可能出错的地方处理。

有点扯远了,今天想分享的不是代码习惯,而是函数式风格下可以非常轻松的将代码替换代码,替换后的代码可能是添加异常处理的函数,也可能是另一个逻辑分支不同版本的代码

下面文章,模拟下载这种带IO的功能分享,模拟后期维护时如何替换代码。

收获

  1. 再次验证函数式风格的强扩展性
  2. 半强迫开发时先开发好基础功能,做好底层再去做上层建筑

开始之前

我们准备一份传统模式的代码(文章最后有完整代码):

            string host = FtpHelper.GetHostFromConfig();//模拟-从配置文件中读取host
            DownArg downArg = FtpHelper.SetDownlodArg(host, "/1.doc");//准备下载参数
            FtpHelper.Downlod(downArg);//开始下载

分析

根据经验可以知道2点肯定带副作用的:

  1. 从配置读取这一步,很可能文件不存在或读取失败。
  2. 真正下载文件,很可能下载到一半中断。

副作用:这是函数式编程里的说法,意思是说一个函数的输入不总是等于输出,放在这里就是偶尔执行时会报错,与预期不符。

1. 评估

主体逻辑是 获取配置->准备下载参数->执行下载
我的目的只是想有可能在简单动下代码的情况下不影响可读性和原有逻辑

2. 开发

假设这个功能已经是函数式风格,那么会是下面这样:

Fp方式 ```csharp #region Fp方式 var setFunc = Currying(FtpHelper.SetDownlodArg);//柯里化后方便函数处理-原生函数式风格不需要这一步 var downLoad = Currying(FtpHelper.Download);//柯里化后方便函数处理-原生函数式风格不需要这一步
        var buildArg = Compose(
             setFunc,
             FtpHelper.GetHostFromConfig);//组合出构建参数的函数

        var downFunction = Compose(downLoad, buildArg);//组合出下载函数

        downFunction("/1.doc");//执行
        #endregion Fp方式
</details>

可以看到,主要是`组合函数`,只有在最后执行时才会真正触发代码执行。

现在将`GetHostFromConfig`替换为`tryGetHostFromConfig`:

<details>
<summary>FP方式-添加Try</summary>
```csharp
            #region FP方式-添加Try

            var setFunc2 = Currying(FtpHelper.SetDownlodArg);//柯里化后方便函数处理-原生函数式风格不需要这一步
            var downLoad2 = Currying(FtpHelper.Download);//柯里化后方便函数处理-原生函数式风格不需要这一步

            Action<Exception> showError = ex => Console.WriteLine("获取host异常");//准备异常时的处理函数
            Func<string> tryGetHostFromConfig = Try2<string>(showError)(FtpHelper.GetHostFromConfig);//添加异常时的处理
            //Func<string> tryGetHostFromConfig2 = TryWithThrow2<string>(showError)(FtpHelper.GetHostFromConfig);//处理完会抛出异常的版本

            var buildArg2 = Compose(
                 setFunc,
                 tryGetHostFromConfig);

            var tryDownFunction = Compose(downLoad2, buildArg2);//组合出下载函数

            tryDownFunction("/1.doc");//执行

            #endregion FP方式-添加Try

可以看到,主体逻辑唯一修改的是替换为tryGetHostFromConfig,而这个新的函数也是组合得到的。
如果程序中有一个公用的处理异常函数(Try2<string>(showError)),那这里就会更加简洁。

3. 测试

正常方式就不看了,直接看异常代码吧:
异常时的表现

这里的我处理函数只是向控制台输出发生错误,如果我想要发生异常时记录完抛出异常时记录完,中断执行,也仅仅只需要用不同的函数与tryGetHostFromConfig组合即可了。

完整代码

这里贴出完整代码,缺点与总结在文章末尾。

完整代码
using System;
using static CkTools.FP.CkFunctions;//这里静态化导入FP扩展

namespace ConsoleApp1
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            #region 传统方式

            string host = FtpHelper.GetHostFromConfig();

            DownArg downArg = FtpHelper.SetDownlodArg(host, "/1.doc");
            FtpHelper.Download(downArg);

            #endregion 传统方式

            #region Fp方式

            var setFunc = Currying(FtpHelper.SetDownlodArg);//柯里化后方便函数处理
            var downLoad = Currying(FtpHelper.Download);

            var buildArg = Compose(
                 setFunc,
                 FtpHelper.GetHostFromConfig);//组合出构建参数的函数

            var downFunction = Compose(downLoad, buildArg);//组合出下载函数

            downFunction("/1.doc");//执行

            #endregion Fp方式

            #region FP方式-添加Try

            var setFunc2 = Currying(FtpHelper.SetDownlodArg);
            var downLoad2 = Currying(FtpHelper.Download);

            Action<Exception> showError = ex => Console.WriteLine("获取host异常");//准备异常时的处理函数
            Func<string> tryGetHostFromConfig = Try2<string>(showError)(FtpHelper.GetHostFromConfig);//添加异常时的处理
            //Func<string> tryGetHostFromConfig2 = TryWithThrow2<string>(showError)(FtpHelper.GetHostFromConfig);//处理完会抛出异常的版本

            var buildArg2 = Compose(
                 setFunc,
                 tryGetHostFromConfig);

            var tryDownFunction = Compose(downLoad2, buildArg2);//组合出下载函数

            tryDownFunction("/1.doc");//执行

            #endregion FP方式-添加Try

            Console.WriteLine("End");
            Console.ReadLine();
        }
    }

    public class DownArg
    {
        public string Host { get; set; }
        public string Url { get; set; }
    }

    public static class FtpHelper
    {
        //为了演示方便,这里改为委托的形式

        //public static Func<string> GetHostFromConfig = () => "localhost";//假设从配置文件中读取host
		public static Func<string> GetHostFromConfig = () => throw new Exception("error");//模拟异常

        public static Func<string, string, DownArg> SetDownlodArg =
            (host, url) => new DownArg()
            {
                //通常情况下还有其它属性设置操作,这里简略了
                Host = host,
                Url = url
            };

        public static Func<DownArg, bool> Download =
            downArg =>
          {
              Console.WriteLine("开始下载,host:" + downArg.Host);
              Console.WriteLine("开始下载,url:" + downArg.Url);

              //... 假设这里是下载文件
              Console.WriteLine("下载完成");

              return true;
          };
    }
}

5. 缺点

根据没有缺点本身就是最大的缺点我们来挑下骨头。

方式上我觉得没有太大问题,但Try2<string>(showError)这个构造过程不太优雅。
我尝试过用其它方式均没有不能实现柯里化优雅这个目标,原因是C#编译器的语法推断导致的:

        public static Func<
            Func<TInput, TOutput>,
            Func<TInput, TOutput>> Try<TInput, TOutput>(
                [NotNull] Action<TInput, Exception> exExp)

        public static Func<
            Func<TOutput>,
            Func<TOutput>> Try2<TOutput>(
                [NotNull] Action<Exception> exExp)

如果调用者的处理函数不想管原始输入参数,按C#中的类型推断是不可能识别到,就必须指明类型。

6. 总结

  1. 函数式风格的特点之一就会将变化点无限抽取,这个过程是潜移默化的,这也是我推崇在业务开发中使用函数式风格的原因,毕竟给的资源有限(比如一个bug只给2小时,但你发现需要因为它调整一下其它地方)时,开发人员往往会倾向于能不动就不动,为了点‘优化’扣绩效不值,许多管理者都觉得可以从管理手段去要求,但你不可能无限给资源阿。。。

  2. 另一个我觉得比较好的是,基于1,代码开发时会有非常多可以公共的点。传统方式需要开发人员在设计阶段就要识别出来公共部分 或 后期有足够资源重构时提取公共逻辑,而函数式风格在这方面有无可比拟的优势。

  3. 在写文章时同时我也在调整FP库中的核心代码(比如柯里化)来适配编译能通过,没有引起大规模报错,符合函数式味道的都没有报错。

posted @ 2022-05-23 11:08  长空X  阅读(94)  评论(0编辑  收藏  举报