美文网首页
以函数式编程思想优化我们的代码

以函数式编程思想优化我们的代码

作者: 当年的反应慢 | 来源:发表于2017-11-03 14:20 被阅读0次

    让我们从一段代码开始,引入函数式编程

        function buyCoffee(creditCard){
            charging(creditCard,1.00)
            let cup=new Coffee()
            return cup
        }
    

    这是平时我们常用的描述买一杯咖啡的过程,buyCoffee方法接收一个信用卡账号作为参数,我们在方法里直接调用另一个计费的方法,计费成功之后,再返回一个咖啡的实例,这样我们就完成了一个买咖啡的操作。这个方法看起来没有什么问题,但是,我们想多买几杯咖啡的是时候,应该怎么做呢?不考虑性能的话,我们会写循环调用这个买咖啡的方法,但是考虑到性能,或者万一中途调用失败的话,处理起来就比较尴尬了,所以我们一般会写一个批量处理的版本,代码大同小异。我这就不再用代码表示了。比较遗憾的是,批量版本没办法复用单一版本的处理结果。
    下面我再写出另一个处理这个过程的方法,请大家做下对比。

        function buyCoffee(creditCard){
            const coffee=new Coffee()
            const fee=1.00
            const charge={creditCard,fee}
            return {coffee,charge}
        } 
        
        function buyCoffees(creditCard,count){
            const turples=new Array(count).fill(buyCoffee(creditCard))
            const coffees=turples.map({coffee}=>coffee)
            const charges=turples.map({charge}=>charge)
            const charge=charges.reduce((a1,a2)=>{creditCard:a1.creditCard,fee:a1.fee+a2.fee})
            return {coffees,charge}
        }
        const {coffees,charge}=buyCoffees('1233445',12)
        const {creditCard,fee}=charge
        charging(creditCard,fee)
    

    由于ES6的语法不是我所想说的重点,大概给大家介绍下写法二的意思,buyCoffee方法实际上是返回了一个对象,对象里有两个属性,一个属性是coffee,另一个属性是账单。buyCoffees方法就比较有意思了,我复用了buyCoffee方法,在这个方法里,我主要的操作是合并账单,并返回一个咖啡列表和一个总账单。然后我在外面调用了付费方法。

    那么问题来了,第二种写法有什么好处?

    先不说第二种写法有什么好处,先说第一种写法有什么坏处。
    作为一个长期与业务逻辑打交道的一线码农,不知道大家有没有这种感觉,那就是特别不愿意遇到一个没有返回值的方法,没有返回值,基本上就意味着这个方法有输入输出,或者会改变你的参数,尤其你传入的是一个比较大的对象的时候,你并不知道这个方法如何处理你的对象,所以得颠颠的去读一下这个方法如何实现,看看你的对象有没有遭到意外的破坏。
    一般的,我们认为一个方法没有返回值,肯定会产生副作用。相对的,不会产生副作用的方法,我们称之为纯函数。这里解释下,是不是有返回值的方法,就一定是纯函数呢?大多数情况下,并不是这样的,检验一个函数是不是纯函数,有一个很简单的准则就是,如果用这个函数的返回值替代这个函数,产生的结果不变,这样一个函数才是纯函数,说起来绕嘴,实际上用下面的公式来表示的话

    y=f(x),g(y)=g(f(x))
    

    此时,我们就认为f(x)是一个纯函数。
    如果还不明白的话,参考第一段代码,我们的运行buyCoffee,实际上返回的是一个new Coffee(),我们在另一个地方引用到了buyCoffee,我们用new Coffee替换掉buyCoffee,显然这两个不能互相替代,因为buyCoffee发生了计费动作。

    纯函数的优点就是不会产生副作用。

    我们在业务处理中,尤其是需要对一个参数对象做一连串的处理过程中,可能会调用很多的方法,这些方法有可能是不同时期不同的人写的,如果有的方法产生了副作用,而其他人没有觉察到,这会导致很多问题。这里我再引入一个概念,叫不可变对象,一个对象是不可变对象,意味着,其是一个安全的对象,不论在哪里用到了,都不会被修改,我们可以安全的使用这个对象,即使在多线程环境下。当你在读代码的过程中,如果写这段代码的那个人把某个对象定义成了不可变对象,意味着你可以直接跳到你关心的那块代码上,而不用小心翼翼的去通过上下文推断这个对象到底经历了什么。

    所以,一个良好的编程习惯是,在处理过程中不要改变对象,如果你真的需要改变一个对象的某个值的话,把这个副作用推到最外层。

    之所以花这么大篇幅介绍什么叫做无副作用,因为无副作用是函数式编程的基石。

    const originArray=[1,2,3,4,5,6,7,8,9,0]
    const targetArray=originArray.filter(num=>num%2==0).map(num=>num*num)
    console.log(originArray) //[1,2,3,4,5,6,7,8,9,0]
    console.log(targetArray) //[2,16,36,64,0]
    // 无副作用意味着我们原始的数据不会遭到修改
    

    那么函数式编程除了让我们不用担心我们的参数被改变,还有什么好处呢?我还要以一段代码来演示:

    有一个场景,我需要知道一个容器中有没有包含我想要的对象,如果有的话,我就做一种处理,没有的话就做另一种处理,平时我们一般是这样写的

    List<Charge> charges=service.getCharges();
    boolean contains=false;
    for(Charge charge: charges){
        if(charge.getCreditCard().equals("123456789")){
            contains=true;
            break;
        }
    }
    if(contains){
        doSomething();
    }else{
        doAnothering();
    }
    

    作为这段代码的作者,很难察觉到for循环有没有什么问题。但是作为一个读者,你要非常关心这个for循环里面做了什么事,然后才能继续阅读下面的代码,如果你没有觉察到这个问题的话,请看下面这段代码:

    List<Charge> charges=service.getCharges();
    
    boolean contains= charges.stream().anyMatch(charge->{charge.getCreditCard().equals("123456789")})
    
    if(contains){
        doSomething();
    }else{
        doAnothering();
    }
    
    

    这段代码的意义不仅是帮我们省了一些代码,更是明确的指出了,我们的contains如何得出,最重要的是,它把contains定义的地方和使用的地方放到了一起,让我们保证了思维的连贯性。顺便吐槽一句,用Java就是麻烦,即使用函数式编程,语法也很啰嗦,scala里面这样表示constains

        val contains=charges contains { _.creditCard == "123456789" }
    

    scala用val表示一个不可变对象,能进一步保证了代码的安全性。java中我们可以用final关键字来约束这个contains,但是一般情况下,final这个关键字好像被我们忘了一样,很少被使用。

    直到这里,我还没有介绍函数式编程中的另一个重要的概念,那就是函数是一等公民,它可以像Int,String或者其他Object一样被传来传去,在前面的例子中

    boolean contains= charges.stream().anyMatch(charge->{charge.getCreditCard().equals("123456789")})
    

    anyMatch 方法接收的参数 charge->{charge.getCreditCard().equals("123456789")} 是一个lambda
    表达式,也就是我们通常说的匿名函数。如果你读Java的api,你会发现anyMatch接收的参数是一个Predicat接口的实例,那Predicat接口又是啥,跟进去发现Predicat是只有一个方法需要实现的接口,我们实际上是现实的是boolean test(T t)方法,也就是说实际上这段代码是这样的:

    Predicat<Charge> predicat=new Predicat<Charge>({
        boolean test(Charge charge){
           return charge.getCreditCard().equals("123456789")
        }
    })
    boolean contains= charges.stream().anyMatch(predicat)
    

    so,Java8的函数式编程只不过是有点甜的语法糖而已。很多三方的库比如guava,rxjava都能够帮助我们在Java7甚至Java6下写出这样的代码,所以,你还认为我们在老的Java项目上无法实现函数式编程吗?

    函数式编程思想不仅能够帮助我们编写更可读的代码,还能帮助我们优化架构

    想象下,如果你面对着一个超级复杂的业务系统,当一个数据发生改变的时候,可能涉及到多条业务线上的操作,编写传统的流水代码意味着我们需要对这个系统所承担的所有的业务线都要熟悉,否则的话,任何一点点修改可能引起很多麻烦。这样的系统,我们如何利用函数式编程思想优化我们的架构呢。

    答案就是,利用不可变对象。

    在前端上,现在最火的框架无非就是 vue/react,这两个框架思路非常的一致,自己维护一个virtual dom,当virtual dom上的值发生改变的时候,它们帮着我们去通知相应的组件,这些组件响应改变,但是这些组件不能反过来改变我们的值。如果某个组件在响应状态变化的过程中产生了新的值,那么它把这个值再放回virtual dom,框架再帮我们把新的值广播出去。

    这个听起来有点像观察者模式。没错,在复杂的系统中,需要一个消息总线来解耦各方的关系。引入消息总线之后,我们的代码虽然遍布在各个地方,但是代码在执行的时候,感觉起来就有点像这样

    listeners foreach {listern->listern(message)}
    

    这种形式有一个好处,我们的listener只需要在消息总线上注册一下就行,不需要耦合在一起,这样对系统的伸缩性非常有帮助。
    如果你的某个listener非常在意某个消息,可以将这个listener实现成同步的,一旦执行不成功,立马抛出异常,让整个消息处理都回滚。反之,如果你的listener只需要接受到这个消息,但是不需要一定等它执行成功,则可以把它实现成异步的,以让出宝贵的时间片执行下面的方法。

    “高内聚,低耦合”一直是我们作为一个码农的追求。在文章的最后,我只是简单的引入了一个代码解耦的方案,其实这种方案已经被很多大牛应用过了,只不过我们还没有意识到和听说到而已 。最近比较关注领域驱动设计(DDD),这种架构设计真正实现了“高内聚,低耦合”这六个字,消息总线是DDD在实现过程中引入的一种角色,然而它存在的意义是非凡的,等我真正领会了DDD,我希望我能再写出一篇文章介绍它。

    相关文章

      网友评论

          本文标题:以函数式编程思想优化我们的代码

          本文链接:https://www.haomeiwen.com/subject/otstmxtx.html