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

本文探讨了如何通过减少全局变量依赖、精简内部变量及利用委托重构等手段提高函数的纯净度,进而提升代码的可读性和可维护性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 前言

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

    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,还需要做一些具体的取值,传值,赋值操作.

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

 

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

  变量尽可能少: 函数内部的变量,有效范围是整个函数,如果我们在函数前面声明了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写的一个数独游戏

 

 

 

分类:  c#笔记编程思维
标签:  重构设计模式函数设计
12
3
(请您对文章做出评价)
« 上一篇: ValueInjecter----最好用的OOM(以微信消息转对象举例)

posted on 2014-08-30 15:56 碎景 阅读(3643) 评论(29编辑 收藏

评论

#1楼   

mark
2014-08-30 16:27 |  小 莫   

#2楼   

我勒个去,又碰到个用这头像的……
2014-08-30 16:35 |  Canrz   

#3楼[楼主]   

@Canrz
引用 我勒个去,又碰到个用这头像的……

... 
好吧 用这个的确实很多.
2014-08-30 16:38 |  碎景   

#4楼   

@碎景
我在知乎上就碰到了两个,QQ空间、贴吧用的更多……
2014-08-30 16:43 |  Canrz   

#5楼   

mark
2014-08-30 16:46 |  cnblog_wz   

#6楼   

说的很好.
2014-08-30 20:16 |  Tinkerc   

#7楼   

博主,你开头给的两个链接给反了,简化逻辑的是减少注释的,减少注释的是简化逻辑的。
2014-08-31 13:00 |  码道程工   

#8楼   

当在类里面某个函数需要的成员变量比较多的时候,向这个函数传递参数那岂不是非常之蛋疼?
2014-09-01 09:48 |  星夜落尘   

#9楼   

楼上说的正确,关于怎么写函数,《代码大全》里面说得很详细。里面建议,参数太多的时候,放一个结构,或者类的。建议的阈值为7。说7是常人记忆的阈值。
2014-09-01 09:59 |  greatim   

#10楼   

说的很好,收藏了,给我的团队用
2014-09-01 11:13 |  Hawkon   

#11楼   

总结起来就是一句话: 一个好的程序员,如果你让他也开一个函数叫毁灭地球, 他会写一个函数叫毁灭星期,然后把地球作为一个参数传进去
2014-09-01 11:39 |  sld666666   

#12楼   

Clean code 与您的观点相反。uncle Bob 建议参数最好为零。
What you about high cohesion?
2014-09-01 13:50 |  IT老民工   

#13楼   

What about high cohesion?
2014-09-01 13:50 |  IT老民工   

#14楼   

。。。
2014-09-01 15:38 |  逍遥心   

#15楼[楼主]   

@码道程工
谢谢指出,确实是弄反了.
已改正.
2014-09-01 15:45 |  碎景   

#16楼[楼主]   

@星夜落尘
这是两回事啊,我只是说参数尽可能走函数入口传进来,你也可以封成一个poco再传进来.
如果业务逻辑就是需要这么多数据,那肯定是没法减少的,就算你不传进来,那还不是得从通过其他方式去拿?
2014-09-01 15:48 |  碎景   

#17楼[楼主]   

@IT老民工
<What about high cohesion>
这是一篇文章吗?
google了一下没找到,
不明白你的意思,
如果你在函数中还要时不时去访问下数据库,读下session或cache,这样也能做到高内聚?
2014-09-01 15:59 |  碎景   

#18楼   

可以马克?
2014-09-01 17:09 |  Badcode   

#19楼   

面向对象和纯函数概念上是冲突的。基于类的代码容易出问题也在这个地方,成员变量对成员函数来说相当于全局变量,相同的输入未必得到相同的输出。
2014-09-01 19:11 |  小时了了   

#20楼   

一看就是强迫症的程序员,而且是完美主义比较重的,但是逻辑非常清晰,系统性思维很强。
嘿嘿~~
2014-09-01 21:12 |  sfrost   

#21楼   

@碎景
见19楼的观点。
2014-09-01 22:43 |  IT老民工   

#22楼[楼主]   

@IT老民工
这个我也说了,纯函数,我并不定性的非黑即白去判定,
而是一个程度的问题,
我并不是说非要完全和它的定义一摸一样才行,
而是他的这种约束体现出的一种理念:函数只关注函数本身,关注度越集中,越纯粹越好, 调用这个函数的人,也就越不容易被误导,函数的维护也越容易.
这其实也是高内聚的一种体现.
2014-09-02 00:27 |  碎景   

#23楼[楼主]   

@小时了了
确实有冲突,但也能调和,比如我举的四个例子,最后一个不就是纯函数了么?当然,它的面向对象的特性也在这里也没了,在外部赋值的时候才会再次体现出来.

而且,也要看你是从什么维度去看,
比如我一个纯函数扔进来一个int和一个string 返回xxx,这个好理解.
但是你也可以放大到对象上来看啊,
我一个"纯函数"扔进来一个"People对象"和一个string 返回xxx,
如果你这里的关注点就是一整个People,那么仍然可以说这个函数很"纯".
因为它关注很集中,不涉及其他因素的影响,只根据people(至于people里面细微的不同,也许你这里不关注)和string来决定返回.

其实都只是一种思维方式,取决于你怎么去理解,去用.
2014-09-02 00:37 |  碎景   

#24楼[楼主]   

@sfrost
哈哈,其实真不算是强迫症,而是我发现我经常难以理解自己在2个月之前写的代码,我觉得我应该想个办法来解决这个问题,
正如我在<之一>里面说的:
(让代码更易读易懂,尤其是自己)最关键是要形成自己的编码规范,这个"规范"不仅仅指的是狭义的命名规则和代码格式,缩进,文件组织结构等.更关键的是,要形成一套有逻辑性,能自洽,有良性导向的一套思维模式,并时刻坚持遵守它,思考它,改进它.
2014-09-02 00:51 |  碎景   

#25楼   

@碎景
呵呵,懂的!
这个就跟做信息系统实施一样,当学会了之后,在做实施的过程中,就想着给自己抽象一套符合自己的实施方法,不仅仅限于一些演示PPT、培训手册的编写,而是一套保证项目能够达到预期效果的,可重复用在不同公司应用的很抽象的方法和理论。
然后在接触更多公司、更多人的时候,不断改进,使之更加丰富并具有一定的厚度。
2014-09-02 02:25 |  sfrost   

#26楼   

@碎景
引用 @小时了了
确实有冲突,但也能调和,比如我举的四个例子,最后一个不就是纯函数了么?......
我一个"纯函数"扔进来一个"People对象"和一个string 返回xxx,
如果你这里的关注点就是一整个People,那么仍然可以说这个函数很"纯".
因为它关注很集中,不涉及其他因素的影响,只根据people(至于people里面细微的不同,也许你这里不关注)和string来决定返回.
...

1.纯函数就是面向对象编程的实践,直接体现了:高内聚低耦合,
2.如果你把People对象做参数,那就不是纯函数了,
你自己给出了MSDN的连接,却连例子都没看仔细;

只要拥有面向对象的思维方式,会自然而然的写出纯函数
2014-09-02 06:28 |  缪军   

#27楼   

另外,对于纯函数的特性之一:
它具有一致性。 在提供同一组输入数据的情况下,它将始终返回相同的输出值
我要指出有些人对这一条的误区:
这不等同于:纯函数不能依赖参数以外的环境,
所以才有人误认为"纯函数和OOP冲突"

还有就是,千万不要用非此即彼的思维去认识纯函数,
它是一种编程手段而已,家常饭菜,爱吃不吃,不是空气和水
2014-09-02 06:39 |  缪军   

#28楼   

@缪军
OOP不恰恰如此吗?成员函数既容易改变外部变量(成员变量),也容易受外部变量(成员变量)的影响,既不容易做到无副作用,也不容易做到一致性。如果大而化之的认为成员变量也是输入数据的一部分,那么全局变量同样可以这么理解,如此一来还有什么东西不是纯函数呢?
纯函数的概念和OO本来既不在其范畴内,也不在同一个层次上,又何必对差异视而不见,为了一致而一致。
2014-09-02 09:37 |  小时了了   

#29楼   

@小时了了
引用 面向对象和纯函数概念上是冲突的。基于类的代码容易出问题也在这个地方,成员变量对成员函数来说相当于全局变量,相同的输入未必得到相同的输出。


是这样,所以很蛋疼,我之前在知乎上关于这个问题提了一个问题: 如何避免在编码过程中【成员变量】慢慢退化成【全局变量】
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值