编程范式

本篇作为笔记形式存在,对左耳听风中的编程范式做笔记,便于以后去查看。
原文中主要用到了语言有C、C++、Python、Java、Javascript、Go、Prolog。由于我对Go语言了解不深,委托范式像是一种组合方式的应用,这里就不对这种范式进行记录了,以后有机会,再回来补充;另外Prolog的方式我觉得很有趣,以后想去研究一下再写,这里也不包括在内。

我需要找到一种合适的记录顺序:原文以一种语言发展的顺序展开,是一种不错的方式; 根据我学习语言的前后顺序,也是一种不错的方式; 再有一种是总分式展开方式。这里选用第二种吧,以我接触这些范式的先后关系来展开。

过程式编程

作为学习的起点,我也是从C语言起步的,但作为使用的起点却不能算是。真正使用的起点应该是汇编语言了,那时候在学校里做课程设计,用汇编控制循环。后来第二次做的时候就用上了C语言,感觉轻松了好多,循环用for就可以了,不用来回的jump。

对C语言的特性,这里直接引用了

  • C 语言是一个静态弱类型语言,在使用变量时需要声明变量类型,但是类型间可以有隐式转换;
  • 不同的变量类型可以用结构体(struct)组合在一起,以此来声明新的数据类型;
  • C 语言可以用 typedef 关键字来定义类型的别名,以此来达到变量类型的抽象;
  • C 语言是一个有结构化程序设计、具有变量作用域以及递归功能的过程式语言;
  • C 语言传递参数一般是以值传递,也可以传递指针;
  • 通过指针,C 语言可以容易地对内存进行低级控制,然而这引入了非常大的编程复杂度;
  • 编译预处理让 C 语言的编译更具有弹性,比如跨平台。

我这里尝试来回忆一下:

  • 从数据上看,数据本身都是二进制,类型是对这段二进制的解释,类型本身并不是数据。
  • 从内存山看,可以分成静态区、堆区、栈区、常量区,分别用来存储存储全局、new、局部、常量字符串等数据
  • 数据对齐方式要注意大小端的差异,尤其在传输过程中,一般都需要大小端的转换,实现方式就是按中轴进行互换。
  • 还有一些比较有趣的结构:比如union,还记得老师写的那段代码,用union来包含两个struct,它们的长度相同,但在传输与解析时做不同的使用。
  • 还有很多宏定义,像是去年看的,内核中使用链表时候那段宏定义就很有意义。
  • 中断的处理,单片机上的中断口有限,需要对中断手动进行配置,包括端口以及回调函数。中断其实一种异步方式,中断方式比起轮询方式节省很多资源。

C语言回忆就到这里了,说是过程式的,不如说是指令式的,你像机器发送着一些指令,这其实比汇编语言好多了,起码不需要记住每一个寄存器。另外,以前只听老师说过C语言其实可以写OO,一直无缘得见,后来看vfs时候才明白,原来面向对象是一种思想,并不一定被语言限制,在C中一样可以用这种思想。

面向对象式编程

最早接触C其实是在实验室里,那时候用MFC做桌面端的应用程序,当然那时候我们称桌面为上位机。对C的认识也比较模糊,因为MFC做了好多封装,也体会不出C的特点。工作之后,写了5年的C,目前为止,在所有语言里使用时间也是最长的。

面向对象的3大特性:封装、继承、多态。
对面向对象的评价继续引用:

  • 优点

    • 能和真实的世界交相辉映,符合人的直觉。
    • 面向对象和数据库模型设计类型,更多地关注对象间的模型设计。
    • 强调于“名词”而不是“动词”,更多地关注对象和对象间的接口。
    • 根据业务的特征形成一个个高内聚的对象,有效地分离了抽象和具体实现,增强了可重用性和可扩展性。
    • 拥有大量非常优秀的设计原则和设计模式。S.O.L.I.D(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转,是面向对象设计的五个基本原则)、IoC/DIP……
  • 缺点

    • 代码都需要附着在一个类上,从一侧面上说,其鼓励了类型。
    • 代码需要通过对象来达到抽象的效果,导致了相当厚重的“代码粘合层”。
    • 因为太多的封装以及对状态的鼓励,导致了大量不透明并在并发下出现很多问题。

缺点上这两点原文中,原文中的举例都是针对Java,像Spring的那些注入,导致了大量的封装,这些封装还屏蔽了细节。

这里只对自己在使用C++的时候的一些回忆吧,主要针对面向对象的3大特性来展开

  • 封装,封装其实沿用着C语言模块化编程的思维,讲究低耦合高内聚的方式,只不过面向对象是将数据与操作封装成一个类,这里的数据也就是后边说的状态。
  • 继承,继承是代码复用的一种方式,起先比较喜欢继承,后来发现继承也是有缺点的,会出现类爆炸的情况。于是就提倡使用组合的方式来复用代码。尤其加之与后边将的多态,使对组合元素的依赖于接口,而不是具体实现,耦合程度进一步降低。
  • 多态,就是刚才说的,依赖于接口而不是具体实现。其实后来发现那些设计模式,其实就是对继承、组合、多态的一些常见用法,直接点就是套路。

还有一部分是开发过程的控制

  • 多线程及线程池的使用,重构预处理服务的时候,将不同的处理策略抽象各自的实现中,然后将整个流程又封装成了各自的过程对象,最后获取线程池中的线程运行过程对象。后来发现这种方式在Java中与runable的方式类似,而且当时各个流程是硬编码的,如果是面AOP的方式,或者装饰器的方式会更好一些。
  • 由于各个处理步骤的只完成对数据处理的一部分,可能会new出一段数据区来,在不同的步骤之间传递引用,但当时的的情况不同,最后的结果并不是线性依赖的,而是根据各个步骤处理结果的逻辑运算。在实现的过程中将数据放到了一块contex的数据区,并将每个步骤的结果也保存在集中,最后将取结果的逻辑与结果本身解耦出来。
  • 在多线程中处理全局数据,对锁的要求很高。

感慨一下,没想到3年过去了,还能顺利记起当时的做法。

其实这隐含着这种编码方式很大的问题,在分布式系统中,状态如此之多,处理起来会加倍的困难。虽然当时从内存监控上看,并没有出现泄露的情况,但也是战战兢兢,如履薄冰的。

Java其实在面向对象方面其实比C做的好一些,从我的基础上看,MVC、IoC、动态代理这些是接触到Java之后才真正了解,而且Java的那本Head First设计模式比C那本更容易理解。由于Java使用并没有C++使用的深,就此作罢吧。

泛型编程

对于泛型编程,除去比用的STL,其实自己写的时候还真的比较少,记忆中,刚开始写Qt界面的时候,3个表格数据不同,表达基本相同,我用泛型做了一个实现。这里做个引用:

理想情况下,算法应是和数据结构以及类型无关的,各种特殊的数据类型理应做好自己分内的工作。算法只关心一个标准的实现。而对于泛型的抽象,我们需要回答的问题是,如果我们的数据类型符合通用算法,那么对数据类型的最小需求又是什么呢?
原文中给出的方式包括3重:

  1. 它通过类的方式来解决
    构造函数与析构函数
    拷贝构造函数,表示对内存的复制
    重载操作符

  2. 通过模板达到类型和算法的妥协。

  3. 通过虚函数和运行时类型识别。

这里想说一下STL,没有接触STL之前,培训老师出了一题,题目现在已经忘记了,记得最主要的数据结构就是动态数据了,我们自己写链表来实现,人家直接用vector,效率高出许多。如果没用vector、set、map,那C++的路还会坎坷一些。

函数式编程

函数式编程最早也是从coolshell中看到的,而且缺少实践,需要引用的东西更多一些了。

  • 定义

    定义输入数据和输出数据相关的关系,数学表达式里面其实是在做一种映射(mapping),输入的数据和输出的数据关系是什么样的,是用函数来定义的。

    从通篇来看,函数式编程都是在借鉴函数,y = f(x) ,在这里只定义了一种关系,而且这种关系可以替换,如 z = g(y),这时候可以将y带入变成z = g(f(x)),由此,函数与变量本身是等价的,自然也就是懒惰的,并且,是可以嵌套的。

  • 特点

    stateless: 无状态,就像是电路,本身不能存储电荷一样,有出有入,入多少出多少
    immutable: 输入数据是不能动的,动了输入数据就有危险,所以要返回新的数据集。这条其实是从stateless中演化而来,对与函数f(x),进了x就要出y,且x不能变

  • 一些技术

    • first class function(头等函数),函数像变量一样使用,参考 z = g(f(x))
    • recursing(递归), z = f(f(x)),是对上一条的加深
    • pipeline(管道), 原文解释:将函数实例成一个一个的 action,然后将一组 action 放到一个数组或是列表中,再把数据传给这个 action list,数据就像一个 pipeline 一样顺序地被各个函数所操作,最终得到我们想要的结果。 pipleline最早也是shell的管道操作了。在代码方面有点。这些用法其实都是基于stateless的。
    • tail recursion optimization(尾递归)
    • map & reduce & filter,这个不论在python还是js中都有使用
      ...
  • 一些总结

    • 函数式编程的核心就stateless,仿照y = f(x),仿照节点不存电荷
    • 函数可以替换,也就是函数与变量等价,因为这种等价,在多线程调用时候,也就更安全
    • 函数可以嵌套,也就可以currying、可以recursing、可以pipeline
  • 一些问题

    • 关于无状态
      联想到了http协议,本来设计是一个无状态的协议,最后却不得不加上cookie、session等的加上状态。那函数式变成处理陈老师提到的频繁复制的问题,还会有什么问题。

      另一个是体系架构,都是尽量在做到无状态,但一个请求需要在不同的服务中流转来完成,这种感觉有些像pipeline,需要流转的次序可配置,并且需要记录在每个流程中的一些信息,比如日志聚合,这种方式跟http协议上加cookie是何等的相似。

      需要状态才是函数编程存在的问题,无状态是一种存在,优点也是缺点。

    • 函数编程与面向对象编程
      面向对象基础其实是封装了,封装是将数据与操作封装在一起的思想,这些数据也就是成员变量,就是状态。但完全可以封装一个类,没有成员变量,里边的函数全是函数式的无状态。这种范式即是面向对象,也是函数编程。那其实也就没有继承的必要性了,完全组合就可以,但多态还是需要的,面向接口编程会保存下来。

基于原型的编程

基于原型的编程已经单独总结过了,这里只放个链接:
js中的原型

基于原型的编程,其实就是用组合的方式在完成着继承。继承跟组合的界限在这里有些模糊了。
node.js中的异步其实是一种软中断,这一部分可以整体梳理成一个博文了,包括单片机的中断处理、linux上下部的中断处理、以及node.js异步的处理,这里就不多写了。

编程的本质

陈老师给出的编程本质也是函数式的:
Programs = Algorithms + Data Structures
Algorithm = Logic + Control
So:
Promgrams = Logic + Control + DataStructures
其中Logic是业务逻辑,逻辑过程的抽象,加上由术语表示的数据结构的定义
Control与业务逻辑无关,你控制它的执行,控制一个程序执行的方式,串行or并行,以及调度不同的执行模块,数据之间的存储关系,这些和业务逻辑无关。

尤其其中给出的注册验证的代码例子,看的我有些汗颜,重构那个模块时,没有想起来,然后这块代码其实在pg测试里见过,没有联想到可以这样玩。

这里对logic与control的分离做一点简单的思考:

在代码级别上,将logic以配置的方式(json、xml)来表达出来,然后将control针对格式来做,而不是针对内容。
在服务级别上,跟上边的无状态服务有些类似,每个无状态的服务可以看作是logic,通过编排来control执行的服务。

注释

本文为了自己记忆方便,做一些记录与思考的东西,所有引用都是从陈皓老师,极客时间《左耳听风》处引来。若有不妥,请联系我,联系方式,详见站内“关于”部分。

# 语言 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×