• C#函数式程序设计初探——重构应用篇


    篇首语
      在基础理论篇当中已经向大家介绍了Func类、函数闭包及函数柯里化等内容,进而介绍了函数式编程在Linq当中的运用。本文将延续这一话题,继续讨论函数式在重构等方面的一些技巧,希望能对大家的工作带来一些启发。

      本文面向有一定基础的读者,如果在阅读过程中您看不懂某些术语或代码,请移步《C#函数式程序设计初探——理论基础篇》。注意,本文提供的一些思路仅供参考,切勿盲目模仿,否则后果自负。

    主要内容

      利用闭包缓存数据,令方法按需执行,提炼重复参数

    第一部分 利用闭包缓存数据

      首先来看一段简单的示例代码:

    class Program
    {
        static void Main(string[] args)
        {
            int num = 10;
            int result1 = Calculator.Square(10);
            int result2 = Calculator.Square(10);
    
            Console.WriteLine(result1);
            Console.WriteLine(result2);
            Console.ReadKey();
        }
    }
    
    public class Calculator
    {
        private static Dictionary<int, int> SquareResult = new Dictionary<int, int>();
        public static int Square(int x)
        {
            if (!SquareResult.ContainsKey(x))
            {
                Console.WriteLine("缓存计算结果");
                SquareResult[x] = x * x;
            }
            return SquareResult[x];
        }
    }

    Square是一个带有缓存功能的求平方算法,它将计算的结果缓存在了一个词典当中防止重复计算,这个技巧在进行很复杂的计算(比如求正弦)当中是比较有用的(空间换时间)。

      在我们的日常工作当中相信大家都写过或者遇见过类似的代码:一个词典被放置在工厂类当中作为单例容器,也就是所谓的“池模式”。

      这里的求平方只是一个为了说明问题的简单例子,现实需求往往更加复杂,而使用设计模式是需要结合实际需求场景的。设想这样的情景:如果这个计算并不像计算平方这样长期通用,而是希望“缓存词典”中的内容仅仅是在这个方法(算法)的内部多次使用(即离开了Square的调用函数Main,我就想要释放这个词典),那么我就毫无必要为了解决一个算法的时间性能优化的具体问题点,而引入一个新的静态类来污染整个面向对象结构,一方面这样做导致了类数量的膨胀,另一方面调用函数Main与静态类Calculator发生了强耦合调用关系。如果我们的系统中到处都充满了Calculator这样的类,就大大增加了理解、维护和接手的成本。

      在这种情况下,很容易我们就能想到把这个函数定义到调用函数的内部,这个思路和《重构》当中的“提炼方法”是完全相反的(恐怕是因为在Java里没法这么搞),代码如下:

    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<int, int> SquareResult = new Dictionary<int, int>();
    
            Func<int,int> Square = x => {
                if (!SquareResult.ContainsKey(x))
                {
                    Console.WriteLine("缓存计算结果");
                    SquareResult[x] = x * x;
                }
                return SquareResult[x];
            };
    
            int num = 10;
            int result1 = Square(10);
            int result2 = Square(10);
    
            Console.WriteLine(result1);
            Console.WriteLine(result2);
            Console.ReadKey();
        }
    }

    这样一来,我们就从系统当中“干掉”了一个扎眼的静态类,再者,我们发现在后续的调用代码中并没有使用SquareResult这个集合变量,那么我们可以说这个变量同样污染了函数空间,于是乎想到通过柯里化的方式把这个集合移动到Square方法的内部:

    class Program
    {
        static void Main(string[] args)
        {
            Func<Func<int,int>> GetSquareFunc = () => {
                Dictionary<int, int> SquareResult = new Dictionary<int, int>();
                return x => {
                    if (!SquareResult.ContainsKey(x))
                    {
                        Console.WriteLine("缓存计算结果");
                        SquareResult[x] = x * x;
                    }
                    return SquareResult[x];
                };
            };
    
            Func<int,int> Square = GetSquareFunc();
    
            int num = 10;
            int result1 = Square(10);
            int result2 = Square(10);
    
            Console.WriteLine(result1);
            Console.WriteLine(result2);
            Console.ReadKey();
        }
    }

    首先我们定义了一个返回函数的函数起名叫GetSquareFunc,在其中定义了一个词典的局部变量,并在这个函数内部返回一个闭包,这个闭包的内部调用了我们的词典进行缓存和判断。在调用时,我们首先要通过GetSquareFunc来动态生成一个求平方函数,之后使用这个运行时产生的函数来进行求平方操作。

      在这里我们看到了如何利用闭包与柯里化的方式缓存数据,使用了函数式的手段进行代码重构之后我们的世界清静多了,不过有人可能会说这么做有点“反OO”,这不是把算法和调用耦合到一个调用方法里了吗?是的,重构总会有一些副作用,所以说任何重构与模式的使用都是要结合需求情境的。同时也有人会问,你这不是多此一举吗,我干嘛不直接把这个缓存逻辑内联在算法里呢?那么我想问,难道你希望用一堆#region/#endregion让代码成为很长的一坨吗?

       嗯,关于重构的话题已经脱离了本文的范围,而且牵扯到心理学、强迫症、洁癖症等……总之,这是函数式的一个应用,我们还是从需求出发!

    第二部分 令方法按需执行

      首先来看一段代码:

    static void Main(string[] args)
    {bool result = DoSth(2, GetList());
        Console.WriteLine("执行结果" + result);
                
        Console.ReadKey();
    }
    
    static bool DoSth(int x, List<object> list)
    {
        if (x < 10) return false;
        //...
        return true;
    }
    
    static List<object> GetList()
    {
        Console.WriteLine("获取数据源,耗时5秒");
        return new List<object>();
    }

    这段代码有一个容易被我们平常所忽略的诟病,在C#语言中,如果函数的参数是一个函数调用,那么C#一定会先调用这个参数当中的函数,也就是说,如果DoSth的第一个参数小于10,那么获取数据源的5秒钟就白白浪费掉了,而此时我们却又不得不传入一个list作为DoSth的参数!

      显然,这个DoSth的API设计是有问题的!那么我们如何来改造这个方法呢?

      相信大家一定都能想到在DoSth内部来获取list之类的方法,在这里,我们将读取数据的方法作为一个参数传进DoSth当中,并在其内部通过判断后,“惰性”执行读取数据源的方法:

    static void Main(string[] args)
    {
        bool result = DoSth(2, GetList);
        Console.WriteLine("执行结果" + result);
    
        Console.ReadKey();
    }
    
    static bool DoSth(int x, Func<List<object>> GetListFunc)
    {
        if (x < 10) return false;
        List<object> list = GetListFunc();
        //...
        return true;
    }

      这里我们将获取数据源的方法作为一个委托传给了DoSth函数,并在函数内部通过判断后执行这个委托来“延迟”获取数据源,这样一来就解决了获取数据的问题。

      有的人一定会问了,我完全可以在调用这个方法之前定义一个空的集合传进去,判断完成之后再读取数据呀,何苦写一堆Func什么的呢?

      答案是,难道你想为了一个方法定义一个私有的类级别成员吗?如果到处都飘满了这种零散的变量,那还有什么面向对象可言呢?还记得你曾经在aspx.cs后台文件开头写的一堆一堆的变量初始化声明吗?

      这个例子仅仅是演示一下Func作为参数实现延迟调用在重构当中的一个例子,其实这个API设计的真正症结在于它把判断逻辑和业务执行逻辑紧紧耦合在了一个方法里!也就是说要像让这个函数更“纯”一些,就应该把判断逻辑移除到方法之外!

    第三部分 提炼重复参数

      假设在某数据库访问层有这样一个装填参数的辅助类:

    static class SqlParamHelper 
    {
        public static void SetParam(SqlCmd obj, SqlType type, string fieldName, object param);
    }

    以及调用代码:

    SqlCmd sqlCmd = new SqlCmd();
    ParamHelper.SetParam(sqlCmd, SqlType.Guid, "PK", new Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")); ParamHelper.SetParam(sqlCmd, SqlType.String, "Name", "小明"); ParamHelper.SetParam(sqlCmd, SqlType.Int, "Age", 20);

      有没有觉得这个代码很蛋疼呢?我就在工作中见过一些类似这样的例子,以上只是仿造的一段代码。先抛开具体的数据库应用不说,首先这个方法调用有一个共性的参数sqlCmd,从功能实现角度来讲,我不得不传进这个参数才能让SetParam方法做一些有价值的事情,但是每次我都要传进它去,显得实在是太啰嗦了!我们有什么方法来重构这段代码呢?

      核心问题在于,既然SetParam不得不用这个sqlCmd,那么把它提出来了,秉承前面例子的思想,我不想搞一个单独的变量来污染方法空间,那么把它放在哪好呢?

      答案就是使用闭包!

      首先我们定义一个返回函数的方法:

    static Action<SqlType,string,object> GetSetParamFunc(SqlCmd sqlCmd)
    {
        SqlCmd cmd = sqlCmd;
        return (type, fieldName, param) => SqlParamHelper.SetParam(cmd, type, fieldName, param);
    }

    思路是,既然我们要干掉这个参数,那我们既要把它缓存起来,并且返回一个可以利用这个参数的闭包,于是乎,调用的代码就改变成了:

    Action<SqlType,string,object> SetParam = GetSetParamFunc(sqlCmd);
    
    SetParam(SqlType.Guid, "PK", new Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"));
    SetParam(SqlType.String, "Name", "小明");
    SetParam(SqlType.String, "Age", 20);

    首先通过GetSetParamFunc方法的调用,返回一个内部使用sqlCmd值的闭包函数,然后调用这个新获取的函数,使用三个参数来调用。

      另外,你有没有发现这么做之后,方法调用环境和SqlParamHelper静态类的耦合全都被推到了GetSetParamFunc方法之中呢?有没有体会出某些设计模式的味道呢?

    后记

      这篇文章主要介绍了函数式编程在重构当中的运用,实际上函数式编程在科学计算、大数据处理等领域还有更多的应用有待我们学习,希望我的两篇博客能给大家日后涉足这个领域起一个良好的铺垫作用。

  • 相关阅读:
    Java calendar类学习笔记
    Anaconda 学习笔记
    第八周作业
    第七次作业
    第六周作业
    第五次作业
    用户调研方法之焦点小组
    第三周作业
    软件工作量的评估方法有哪些
    软件质量保证与测试(--作业--)
  • 原文地址:https://www.cnblogs.com/Hlia/p/3033709.html
Copyright © 2020-2023  润新知