提高代码质量系列之三:我是怎么设计函数的?

  • 前言

  这篇其实是上两篇的两个主题思想的承接和发散:

    1. 我也想少写注释,想用2-4个很清晰的单词去描述函数,但是这个函数好复杂啊,我恨不得写近百字去描述它,要我用几个单词去描述?臣妾实在是做不到啊~  <如何做到少写注释>
    2. 我也不想写这么多if  else,然后看着那一堆一堆{}{{}{}{{}}}}}}}{{{}{{}头晕眼花,但逻辑就是有这么复杂,我能怎么办呢?  <如何简化代码逻辑>

    这篇博文,应该就是我对于以上问题结合设计原理的一些思考,不算多高深,但都是自己的总结,我也不会去谈xx设计模式,因为我觉得设计模式的本质就是让你写更好的代码,而不是反之,所以理解它背后的思想,才是真正有价值的东西.

 

  • 尽可能让你的函数符合"纯函数"标准

     先介绍下什么是"纯函数" 纯函数其实并没有一个很统一的定义,像Haskell的定义,就太苛刻,几乎是数学领域了,我比较认同下面这个定义:

       纯函数应该具有以下两个特性: 

    •  它没有任何副作用。 函数不会更改函数以外的任何变量或任何类型的数据。

    •  它具有一致性。 在提供同一组输入数据的情况下,它将始终返回相同的输出值。

      我自己总结下,意思是一个设计良好的函数,应该就像一个黑盒子一样,你完全不需要关注函数内部的实现,你只需要关注三点, 1.函数名 2.函数接受的参数类型 3.函数返回值的类型,只要我们确定了这三 点,我们即可完全"掌控"这个函数, 我们给定一个输出,必然会返回预设的结果,这个结果不受其他任何因素的干扰. 当然,这其实是最理想的情况,"纯函数"也并非就是非黑即白的定性修饰,它更多的是一个程度上的修饰,有些函数是无论如何也不可能写成成纯函数的,比如访问非托管资源的函数. 但我们可以这样说:FunA和FunB都不是纯函数,但FunA比FunB更"纯函数"(可以类比"声明式"这个概念).

  , 更具体的介绍,可以看msdn里面的一个小专题  纯函数转换简介 . 

    那么,我们为什么要写纯函数呢?因为省事省心, 直接来两段代码,

        public void DoSthWithTwoVariable1()
        {
            var p1 = Session["P1_key"];
            var p2 = _p2;
            //......DosthWith p1 and p2
        }

        public void DoSthWithTwoVariable2(Type1 p1 , Type2 p2)
        {
            //......DosthWith p1 and p2
        }

  第一个函数要考虑的东西很多,比如session里面是否有值,-p2这个全局变量会不会受到其他地方的干扰,而这些其实不该是doSth应该关心的,它的职责范围被扩大了.

  这两个函数,其他人或者过段时间我们自己调用的时候,谁更让人放心?

  所以我们要使函数显得純.第一步就是尽可能避免全局变量,我们分析一个函数,就只分析这个函数的全部代码(有效范围)就好,如果引入了全局变量,我们分析的时候,关注范围也难免会被强制扩大到全局,同理,能声明为静态函数的,就应该避免声明为成员函数,因为成员函数可以访问对象的实例,而该对象在调用成员函数的时候,是个什么状态,有无初始化,函数是否会修改实例(引用类型)的参数,如果我们要对这个函数做重构,就难免会束手束脚.

  宁愿多花一点功夫,将需要的变量在封装的纯函数中不断传递,也不要轻易将它设置为全局变量,因为在函数中传递,按照你调用的顺序,它的流程仍然是稳定的,而一旦使用全局变量,那么它就失去的约束,在哪里被人初始化了?怎么初始化的,顺序是不是按我要求的,有没有哪个地方在我做第二次初始化之前,就调用了第二次处理的功能逻辑?

再看一个例子:

 public void SetType3()
        {
            var p1 = this._p1;
            var p2 = this._p2;
             //......Deal p1 and p2
            this._p3 = xxx;
        }

        public static void SetType3(MyClass obj)  //静态函数,但修改了实例的成员 不是纯函数
        {
            var p1 = obj._p1;
            var p2 = obj._p2;
            //......Deal p1 and p2
            obj._p3 = xxx;
        }

        public static void SetType3(Type1 p1, Type2 p2, MyClass obj)  //静态函数,但修改了实例的成员 不是纯函数
        {
            //......Deal p1 and p2
            obj._p3 = xxx;
        }


        public static Type3 GetType3(Type1 p1, Type2 p2)
        {
            //......Deal p1 and p2
            Type3 p3 = xxx;
            return p3;
        }

以上四个函数的纯函数程度,是依次递增的,都是大家很常用的写法,那么这四个函数的区别是什么呢?

是我们调用者对函数内部实现逻辑的关注程度,依次递减,他们的功能也越来越纯粹(意味着更容易提炼和复用),调用起来也更省心,

当然,也难免会更琐碎,比如GetType3,还需要做一些具体的取值,传值,赋值操作.

其实他们也没有什么优劣之分,这之间的度,自己把握就好.

 Ps: 2015-01-29 11:06:30补充:

  今天看Qunit官网介绍,发现里面一个例子,也是我这种思想的一个印证:   Make Things Testable 

function prettyDate(time) VS function prettyDate(now, time)          //前者内部声明了now(当前时间),后者作为参数传递进去。

很明显,qunit官方也是推荐这种纯函数式的风格的。

 

 Ps: 2016-2-15 11:27:59补充:

  今天学习React的时候,看见他们对Component里面方法的分类,同样有这种思想的体现,

Methods defined within this block are static, meaning that you can run them before any component instances are created, and the methods do not have access to the props or state of your components. If you want to check the value of props in a static method, have the caller pass in the props as an argument to the static method.

      通过对函数的合理分工,让整个组件的逻辑更加清晰工整.

  我现在越来越觉得,我们最需要提高的,不是编程语言,功能库的熟练程度,而是我们对"美"的感知能力,当一个事物被精心雕琢,融入了作者的智慧与心血时,它自然而然就会散发一种美的光芒.

 

 

  • 函数内部的变量,尽可能少,声明尽可能晚,绝对禁止一值多用

  变量尽可能少: 函数内部的变量,有效范围是整个函数,如果我们在函数前面声明了10个变量,那么我们都必须时刻关注这些变量的使用情况,有些变量其实就在前面用了一次,但后来阅读的时候,你也不记得后面是不是还用到了它,所以减少变量数量,就意味着减少代码复杂度.举例:

          //取得操作实例,根据id取得对象,取出最终我们要的state,
            // appointmentManager,thisAppointment这两个变量我们都只用了一次,但以后看的时候,我们也不确定后面还用不用
            var appointmentManager = ManagerFactory.Create<AppointmentManager>();
            var thisAppointment = appointmentManager.GetById(appId);
            var state = thisAppointment.State;

            //其实可以这样,那么我们只需要关注一个state就好,阅读压力大大减少
            var state = ManagerFactory.Create<AppointmentManager>().GetById(appId).State;

 

  声明尽可能晚:可能我们写类的时候养成了习惯,将变量放在最上面,统一声明,易于整理和查阅. 其实类的声明和函数的声明是不一样的,类的所有成员(变量和函数)都是无所谓先后的,而函数里面的局部变量,则是有先后顺序的,我们在不必要的地方引入了不必要的约束,也就意味着不必要的麻烦.

  比如我们有一个200行代码的函数,我们在最前面声明了10个变量,这些变量是依次在函数不同部位使用的,但因为在最前面已经声明了,所以我们阅读这个函数的时候,也需要时刻注意这10个变量在函数中的使用情况, 这里我们简单的引入一个"关注度"的概念: G = 变量个数*变量的有效代码范围 ,那么这时候的总G数 = 10*200 = 2000. 

  而如果开始只声明2个变量,剩下的变量在使用的时候才声明,比如p3,p4是在101行代码里面声明的,那么你阅读1-100行代码的时候,就不需要关注p3,p4了(也没法关注,都还没声明呢),然后剩下6个变量在151行声明,那么现在的关注度,就只有G=2*200 +2*100 +6*50 = 900.

  禁止一值多用:前面不是说要尽可能少的声明变量么,有些人就这样做:比如我声明一个state,表示Appointment的状态,用完之后,后面需要用订单状态的时候,我仍然用state字段去接值,参与新的,属于Order的业务逻辑,这个我还真见过.不过相信这种大神应该还是极少数吧.

 

  • 重构还是不重构,这不是个问题

   几乎所有提到程序设计的书籍,都是推荐将函数中比较独立的业务抽取出来,放在一个新的函数中,好处很多:结构清晰,代码复用,业务解耦合.

但有时候我们的情况很尴尬,说功能独立吧,也不是特别独立,说要提公吧,其实在其他地方用的可能性也不大,但要就这样和主体业务放在一起,代码也确实显得比较乱,提公之后,又将业务逻辑分散了,这种情况应该怎么办呢?

  其实我们可以选一个折中的方案:委托.

  比如一个流程,需要在保存之前筛选初始数据,这个筛选的方法很大可能只在这里用(但也不排除以后再其他地方也会用,虽然可能性不大),和主体业务耦合也比较强,其实我们可以在函数中声明一个

   Func<IList<Product>, AttrItemDTO, bool> FilterProduct1= (lambda Express) 或Func<IList<Product>, AttrItemDTO,int, bool> FilterProduct2= (lambda Express)

  我们可以通过传递参数的形式,写成纯函数形式的FilterProduct2(第三个参数就是state),也可以写成FilterProduct1,在lambda里面直接使用前面函数中声明的"全局变量"state,

这两者都是将筛选这一流程进行了一次折中的"重构",而且花销很小, 首先它的业务逻辑还是线性顺序进行的,一条线下来,再次即使以后需要重构或者提公,也非常容易.

  Ps:其实委托和lambda等函数式思维的引入,真的可以给我们带来很多新的思维启发,  不过可能是我们以前都太习惯于过程式的编码, 还需要锻炼锻炼这种新的开发理念吧.

  Ps2: 关于这种函数式写法的一个非常炫酷的示例,可以参考下csdn .NET斑竹caozhy写的一个数独游戏

 

 

 

posted @ 2014-08-30 15:56  碎景  阅读(14198)  评论(37编辑  收藏  举报