美文网首页
编程范式知多少——厘清主流编程范式

编程范式知多少——厘清主流编程范式

作者: 架构悟道 | 来源:发表于2020-03-07 18:23 被阅读0次

    本文主要是对业界主流的编程范式(编程思想)做一个汇总阐述,厘清各个编程范式之间的差异点、优缺点等,希望能对大家系统的了解编程范式提供点帮助。

    编程范式哪家强

    3种主流编程范式

    命令式编程

    看个例子:

    周末,中午我想吃个烤鸡翅,然后我:

    去菜场买几个鸡翅;鸡翅洗净、腌制;放入烤箱,设定烘烤温度、时间,开始烤;烤箱中取出鸡翅,放入盘中。

    开始吃鸡翅...

    例子中的诉求很明显,要吃鸡翅,为了实现这个目的,然后具体的一步一步的去做对应的事情,最终实现了目的,吃上了鸡翅。——这就是命令式编程

    命令式编程的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。命令式编程是最常规的一种编程方式,各种主流编程语言如C、C++、JAVA等都可以遵循这种方式去写代码。

    举个例子,从给定的一个数字列表collection里面,找到所有大于5的元素,用JAVA语言可以如下方式来写:

    List<Integer> results = new ArrayList<>();
    for (int num : collection) {
        if (num > 5) {
            results.add(num);
        }
    }
    

    这就是一个典型的命令式编程的写法,上面的执行过程如下:

    1. 先定义一个result列表,用于存放执行的结果;
    2. 对原始列表的元素进行逐个的遍历操作;
    3. 对每个元素,都判断下是否大于5,如果大于则放到result列表里面去;
    4. 继续for循环,直到遍历结束,执行完成。

    这个代码明确的告诉了计算机具体的每一步的执行过程,然后计算机按照代码执行的流程逐个命令去执行就可以了。

    通过上面的例子可以看出:

    命令式编程是通过赋值(由表达式和变量组成)以及控制结构(如,条件分支、循环语句等)来修改可修改的变量。

    说白了,就是计算机的一个个指令序列,按照写定的顺序或者跳转逻辑(for/while/goto等)进行逐个执行。程序执行的效率,取决于指令序列的多少、变量占用的空间大小等因素决定。因此,便有了有衡量算法优劣的时间复杂度空间复杂度的概念。

    声明式编程

    继续前面吃鸡翅的例子。

    周末,晚上我还想吃个烤鸡翅...
    我:妈,我要吃烤鸡翅...
    (一段时间后)
    妈:烤鸡翅来咯...
    我开始吃鸡翅...

    这个例子里,想吃烤鸡翅,但是我没有像前面一样自己去动手一步步的做,而是将我的这个诉求传递给,然后将鸡翅给准备好,实现了的诉求。对例中的而言,只需要声明要吃鸡翅就行了,至于这个鸡翅是怎么做出来的,完全不用关心。——这就是声明式编程

    声明式编程的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。典型的声明式编程语言,比如:SQL语言、正则表达式等。

    SELECT * FROM collection WHERE num > 5
    

    再同样用JAVA举个声明式编程的例子:

    List<Integer> results = getAllGreaterThan5Numbers(collection);
    

    上面就是声明式编程的一个典型示例,对于当前调用方而言,只需要告诉计算机我现在需要从collection里面获取所有大于5的数字就行了,至于getAllGreaterThan5Numbers()这个方法具体是如何一步步执行将大于5的数字过滤出来的,调用方完全不用关心。

    函数式编程

    先要搞清楚,此函数非彼函数

    对于程序员而言,所谓的函数,一般都是指的各个编程语言中的函数/方法,但是函数式编程中的函数,指的是数学意义上的函数,也即映射关系

    举个例子:

    y = f(x)
    f(x) = x + 1
    

    这个数学公式里,f(x)是一个函数,其实表名的也就是xy之间的某种关系,也即x加上1之后就是y了。在这个例子里:

    若x=1,则y=f(x)计算后的值为2。
    若x=2,则y=f(x)计算后的值为3。
    若x=3,则y=f(x)计算后的值为4。
    ...
    

    所以说,函数的特点很明显了,接收一个或多个输入,生成一个或多个对应的结果,且输入和输出之间的关系很明确且固定、不会受之前其它输入的影响。也即:

    当一个函数无论何时、无论谁调用、无论传入什么,只要保持相同的输入,不管调用多少次,都能够得到同样的结果(无副作用、引用透明性),这样,就可以说这个函数具备了函数式的特征。

    了解了函数式的概念,再回头看下函数式编程。其核心思想与声明式编程是一致的,即:只关注做什么而不是怎么做。所以这种层面上来说,函数式编程是声明式编程的一部分

    List<Integer> results = 
        collection.stream()
                  .filter(n -> n > 5)
                  .collect(Collectors.toList());
    

    再回头看下前面关于函数的描述,在数学中,函数f(x)的输入变量x,其实就是代表一个需要带入到函数公式中计算的具体的数值。同理、类比到函数式编程场景中,函数中传入的变量也是不可赋值的(final类型的),即:变量的值是不可变的。具体而言:

    • 函数传入的变量值,一旦绑定后就不能再发生变化了。
    • 与其说传入的变量是值,不如说传入的变量是个表达式(惰性求值)。

    当然咯,函数式编程的概念是个很庞大的理论体系,除了上面提及的几个特征,还有一些别的特性(比如柯里化),此处不做展开细述。

    巅峰对决

    命令式编程 VS 声明式编程

    老规矩,先说个生活中的例子:

    一天下午,在家。
    老婆:“老公,我要吃千层蛋糕”。
    ...(一段时间后)...
    老公:“老婆,给你千层蛋糕”

    上面就是个典型的声明式编程的例子,即只说要什么,不关注怎么做。老婆只告诉老公她要吃千层蛋糕,然后老公负责将千层蛋糕给到老婆,至于老公是怎么弄来这个千层蛋糕的,到底是出门买的、还是点的外卖、还是自己亲手做的,老婆完全不关心。

    演变下上面的例子:

    一天下午,在家。
    老婆:“老公,我要吃千层蛋糕,你先去手机上下载个XX美食APP,然后搜索下需要哪些材料,然后步行去小区斜对面的菜场里面买一下鸡腿,再去对面超市里面买其余的辅料,最后回来做千层蛋糕给我吃”。
    ...(老公遵照老婆的指示,先下载APP,再搜索需要的材料,再去菜场买菜、去超市买辅料,回家开始做蛋糕...一段时间后完成...)...
    老公:“老婆,给你千层蛋糕”

    这个例子中的老婆指令详细的告知了一步步要怎么做,这就是命令式编程的雏形。显然,与前面的声明式编程相比,本例子中的老婆显然要事无巨细的操心、麻烦了很多,而前面的例子中,则可以当一个甩手掌柜,发出诉求后,等着吃就行了~~

    对上面例子再展开描述下:

    一天下午,在家。
    Step1
    老婆:“老公,我要吃千层蛋糕”。
    Step2
    老公开始搜索食谱教程,准备食材,跟着食谱一步步的做蛋糕(累的满头大汗)...
    Step3
    老公:“老婆,给你千层蛋糕”

    这个例子中,对于老婆而言,上面已经说过了,属于声明式编程。对于Step2中的老公而言,就是完全的命令式编程了,他需要知道具体的每一步需要做什么事情,最后将蛋糕做出来。

    从上面的3个例子中可以看出来,如果要最终实现一个诉求,肯定是需要有人发出诉求、有人要具体操心去最终实现,而老婆能否当甩手掌柜,取决于老公是否足够全能(当然,程序猿老公肯定都是全能型的了,不然怎么会有老婆呢...哈哈)。

    对应到开发场景中,例子中的老婆便是业务模块,而老公便是各种封装的工具类、开源库函数等,如果有够厉害的库函数支撑把底层复杂的逻辑都实现了,那业务逻辑层就是负责发号施令就可以了。

    我们回到代码层面,继续看下前面声明式编程中的提到的那个例子的完整代码:

    // 声明式编程、只告诉应该要做什么,不关心具体怎么做
    List<Integer> results = getAllGreaterThan5Numbers(collection);
    ...
    
    // 命令式编程、告诉计算机具体的每一步操作逻辑
    private List<Integer> getAllGreaterThan5Numbers(List<Integer> collection) {
        List<Integer> results = new ArrayList<>();
        for (int num : collection) {
            if (num > 5) {
                results.add(num);
            }
        }
        return results;
    }
    

    声明式编程是一种最为简单的方式,但声明式编程通常都是需要依赖于命令式编程的基础上才能实现的。之所以能实现声明式编程,是因为相关的库函数、或者自定义的工具函数(比如这里例子中的getAllGreaterThan5Numbers()方法)将底层具体一步步执行的逻辑给封装了,而这些工具方法,最终还是需要基于命令式编程的方式来实现的(是不是像极了前面“老公”和“老婆”的例子?)。

    当然,命令式编程声明式编程并无优劣之分,对于程序员而言,声明式编程的风格自然是更好的,这是人的惰性决定的。而且,从代码调试的角度来看,声明式编程不好调试,甚至无从调试(比如调用其他库或者依赖包的API、且没有源码的时候)。此外,对于大型项目系统而言,命令式编程声明式编程两种形式肯定是相互依存的。

    所以,这也给我们一个很重要的启示:

    • 项目中,对于公共的工具方法的封装,一定要保证绝对准确、绝对可靠
    • 技术选型的时候,选择的三方或者开源的框架或者工具库的时候,一定要尽可能选择迭代成熟且稳定的。
    • 站在巨人的肩膀上,有现成的工具时,避免闭门造车,重复造轮子。

    命令式编程 VS 函数式编程

    当前硬件发展速度日新月异,几乎所有的CPU都是支持多核运行,而囿于CPU单核执行的效率瓶颈,多核编程的概念越来越受到重视。

    对于命令式编程而言,其执行的时候只能占据着单核CPU进行处理(注意:这里与传统意义上的多线程并发不是一个概念),如果需要实现多核编程,则需要对代码进行改动、然后依托多线程并发编程等方式去实现,复杂度与实现难度都会高很多。

    依旧是前面命令式编程所使用的例子,从给定的数字列表中,找出所有大于5的元素,前面也已经看过其命令式编程的代码如下:

    List<Integer> results = new ArrayList<>();
    for (int num : collection) {
        if (num > 5) {
            results.add(num);
        }
    }
    

    很明显,我们都知道,这段代码去执行的时候,一定是从上到下在一个单核单线程中执行。那假定给定的collection数字列表特别大,for循环中的逻辑又比较耗时,则这段代码执行下来肯定是要耗费很长的时间,应该如何优化呢?——很容易想到,多线程!

    继续,针对上面的例子,将其改造为多线程并行处理逻辑,可以这样(有多种方式,此处只是举个简单的实现~):

    1. 搞个线程池,里面维护若干个执行线程,比如10个。
    2. 封装个执行Thread线程,run()方法里面封装具体的逻辑。
    3. 将collection内容10等分,分别分配给线程池里面的10个线程一起处理。
    4. 建立一个线程安全的结果集容器,用于存放各个线程处理筛选出来的结果。
    5. 等待所有线程处理完成,得到最终的结果集。

    具体的代码就不贴了,总之,可以看出来,在命令式编程中,要实现多核编程,需要额外的很多复杂处理逻辑才能实现、还要关注各种信号量的概念,稍有不慎,还容易出现严重问题——这也是命令式编程的一个硬伤,关于并发编程的内容足够撑起一本厚厚的教科书。

    再来看下函数式编程函数式编程似乎天生就是为了多核编程而生的,它打破了由于循环体的存在而导致的只能单核运行的束缚。函数式编程的出现给各种库函数开拓了一片新天地,可以提供各种更方便的API接口。比如结合JAVA中的Stream流,并行操作甚至可以一行代码就搞定了:

    List<Integer> results = 
        collection.parallelStream()
            .filter(n -> n > 5)
            .collect(Collectors.toList());
    

    最后

    一个大型系统中,各种编程范式一般都是相互集成、互为补充的。只能说在恰当的地方使用恰当的方式完成恰当的事情,具体这个度,需要根据实际情况进行把控。当然咯,函数式编程作为近年来各种编程语言的新宠,还是值得学习下的,可以有效的简化我们的代码逻辑、增强可读性,提升并行处理效率等。

    参考内容

    本文中部分观点或者内容参考自互联网,相关引用地址如下:



    欢迎关注我的公众号“架构笔录”,原创技术文章第一时间推送,也可互动一起探讨交流技术。

    相关文章

      网友评论

          本文标题:编程范式知多少——厘清主流编程范式

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