编程范式 | 青训营笔记

96 阅读15分钟

一、课程介绍

image.png

image.png

二、编程语言

机器语言

这种语言主要是利用二进制编码进行指令的发送,能够被计算机快速地识别,其灵活性相对较高,且执行速度较为可观,机器语言与汇编语言之间的相似性较高,但由于具有局限性,所以在使用上存在一定的约束性。

语言需要介质来承载早期使用纸带,近代使用线缆/开关来控制。

image.png

汇编语言

该语言主要是以缩写英文作为标符进行编写的,运用汇编语言进行编写的一般都是较为简练的小程序,其在执行方面较为便利,但汇编语言在程序方面较为冗长,所以具有较高的出错率。

image.png

中级语言

中级语言是介于机器语言和高级语言之间的一种语言,汇编语言是中级语言的一个例子。

  • 使用中级语言编写指令比使用低级语言编写指令更容易。
  • 与低级语言相比,中级语言更具可读性。
  • 易于理解,发现错误并进行修改。
  • 中级语言特定于特定的机器架构,这意味着它取决于机器。
  • 中级语言需要翻译成低级语言。
  • 与低级语言相比,中级语言执行速度较慢。

C:“中级语言”过程式语言代表

  • 可对位,字节,地址直接操作
  • 代码和数据分离倡导结构化编程
    • 能够把执行某个特殊任务的指令和数据从程序的其余部分分离出去、隐藏起来。获得隔离的一个方法是调用使用局部(临时)变量的子程序。
  • 功能齐全︰数据类型和控制逻辑多样化
    • C的数据类型有:整型、实型、字符型、数组类型、指针类型、结构体类型、共用体类型等,能用来实现各种复杂的数据类型的运算,并引入了指针概念,使程序效率更高。
  • 可移植能力强
    • C语言在不同机器上的C编译程序,86%的代码是公共的,所以C语言的编译程序便于移植。在一个环境上用C语言编写的程序,不改动或稍加改动,就可移植到另一个完全不同的环境中运行。

高级语言

高级语言(High-level programming language)是一种独立于机器,面向过程或对象的语言。高级语言是参照数学语言而设计的近似于日常会话的语言。包括很多编程语言,如流行的java,python,lisp等等。

C++:面向对象语言代表

  • C with Classes

  • 继承(为了代码的复用,保留基类的原始结构,并添加派生类的新成员)

  • 权限控制(来控制成员变量和成员变量的访问权限)

  • 虚函数(父类允许被其子类重新定义的成员函数,而子类重新定义父类函数的做法)

  • 多态(多态可以使我们以相同的方式处理不同类型的对象,其实用一句话来说,就是允许将子类类型的指针赋值给父类类型的指针。

Lisp:函数式语言代表

  • 与机器无关
  • 列表:代码即数据
  • 闭包

image.png

JavaScript: 基于原型和头等函数的多范式语言

  • 过程式
    • 过程式编程的核心在于模块化,在实现过程中使用了状态,依赖了外部变量,导致很容易影响附近的代码,可读性较少,后期的维护成本也较高。
  • 面向对象
    • 面向对象编程的核心在于抽象,提供清晰的对象边界。结合封装、集成、多态特性,降低了代码的耦合度,提升了系统的可维护性。
  • 函数式
    • 函数式编程的核心在于“避免副作用”,不改变也不依赖当前函数外的数据。结合不可变数据、函数是第一等公民等特性,使函数带有自描述性,可读性较高。
  • 响应式*
    • 响应式编程的核心在于Reactive,只是带有了部分的Functional的特性

总结

image.png

三、编程范式

程序语言特性

  1. 是否允许副作用
  2. 操作的执行顺序
  3. 代码组织
  4. 状态管理
  5. 语法和词法

编程范式

  • 命令式:命令式编程的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。主要分为以下两种形式:

    • 面向过程
      分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
    • 面向对象
      是把构成问题事物分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。
  • 声明式:声明式编程是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。主要分为以下两种形式:

    • 函数式
      将所有计算都当作纯函数,没有任何副作用,没有任何突变的编程泛型。

    • 响应式
      是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。

过程式编程

  • 自顶向下
  • 结构化编程

自顶向下

从整体分析一个比较复杂的大问题。为了解决这一问题,必须把它拆分为小问题,然后再逐步细分到更小的问题,直到每个问题对我们来说,都已经很简单。解决了所有的小问题,逐步向上汇总,就完成了最初的复杂问题。值得强调的是:从小问题汇总到最后的复杂问题,这是“自顶向下”编程的一个过程。

image.png

结构化编程

它采用子程序、程序码区块、for循环以及while循环等结构,来取代传统的 goto。希望借此来改善计算机程序的明晰性、品质以及开发时间,并且避免写出面条式代码。

image.png

结构化的程序是以一些简单、有层次的程序流程架构所组成,可分为顺序、选择及循环。

  • 顺序是指程序正常的执行方式,执行完一个指令后,执行后面的指令。

  • 选择结构顾名思义,当程序到了一定的处理过程时,遇到了很多分支,无法按直线走下去,它需要根据某一特定选择结构表示程序的处理步骤出现了分支,它需要根据某一特定的条件选择其中的一个分支执行,选择结构有单选择、双选择和多选择三种形式。

  • 不断的重复,被称作循环,所以这里循环结构通常就是用来表示反复执行一个程序或某些操作的过程,直到某条件为假(或为真)时才可终止循环。在循环结构中最主要的是:什么时候可以执行循环?出现哪些操作需要循环执行?循环结构的基本形式有两种:当型循环和直到型循环。

JS中的面向过程

export var car = {
  meter:100,
  speed:10,
};

export function advanceCar(meter){
  while(car < meter){
    car.meter += car.speed;
  }
}
  • 变量可以看作面向过程的数据
  • 函数可以看作面向过程中的算法
import { car , advanceCar } from './car'

function main(){
  console.log('before',car);
  
  advanceCar(1000);
  
  console.log('after',car);
}

面向过程问题

  • 数据与算法关联弱
  • 不利于修改和扩充
  • 不利于代码重用

面向对象的出现解决问题

  • 结构清晰,程序是模块化和结构化,更加符合人类的思维方式;
  • 易扩展,代码重用率高,可继承,可覆盖,可以设计出低耦合的系统;
  • 易维护,系统低耦合的特点有利于减少程序的后期维护工作量。

面向对象编程

  • 封装
  • 继承
  • 多态
  • 依赖注入

封装

  • 封装是把彼此相关数据和操作包围起来,抽象成为一个对象,变量和函数就有了归属,想要访问对象的数据只能通过已定义的接口。
  • 封装不仅仅实现了数据的保护,还把彼此相关联的变量和函数包围了起来。

java封装举例:

import java.util.*;
class Young {
	private String name;//私有属性name
	private int age;//私有属性age
	private String major;//私有属性major
	public void setName(String name) {//定义一个公共的方法来设置name属性
		this.name=name;
	}
        public void setAge(int age) {//定义一个公共的方法来设置age属性
		this.age=age;
	}
	public void setMajor(String major) {//定义一个公共的方法来设置major属性
		this.major=major;
	}
	public String getName() {//定义一个公共的方法来获取name属性
		return this.name;
	}
	public int getAge() {//定义一个公共的方法来获取age属性
		return this.age;
	}
	public String getMajor() {//定义一个公共的方法来获取major属性
		return this.major;
	}
}
public class Forever {
	public static void main(String[] args) {
		Young member=new Young();
		member.setName("张三");//对私有属性操作,要通过get来访问、set来更改的方法
		member.setAge(20);//同上
		member.setMajor("计算机科学与技术");//同上
		System.out.println("学生姓名:"+member.getName()+
				  ",年龄:"+member.getAge()+
				  ",专业:"+member.getMajor()+"!");
	}
}

继承

  • 子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
  • 作用:无需重写的情况下进行功能扩充。

在java中,类的继承格式:

class 父类 { 
}
class 子类 extends 父类 {
}

java继承举例:

class Person {
    public String name="xiaomin";
    public int age=20;

}

class Student extends Person {
    void study() {
        System.out.println("I can study!");
    }
}

public class JiCheng {
    public static void main(String args[]) {
        Student stu = new Student();
        stu.study();
        System.out.println("姓名:" + stu.name + "\n" + "年龄:" + stu.age);
    }
}

//运行结果
//I can study!  
//姓名:xiaomin  
//年龄:20

多态

  • 不同的结构可以进行接口共享,进而达到函数复用。
  • 多态存在的三个必要条件
    • 继承
    • 重写
    • 父类引用指向子类对象:Parent p = new Child();

java多态举例:

class Animal{
    public void eat(){
        System.out.println("动物要进食");
    }
}

class Cat extends Animal{
    public void eat(){
        System.out.println("猫咪要吃饭");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        //从右往左念,猫咪是动物
        //new Cat()已经创建了一个Cat类的对象,在堆内存之中
        //Animal类的引用变量指向了Cat类的子类对象
        Animal a = new Cat();
        //这里会优先调用子类的重写方法
        a.eat();
    }
}

依赖注入

依赖注入(DI)是实现控制反转的主要方式:在类 A 的实例创建过程中就创建了依赖的 B 对象,通过类型或名称来判断将不同的对象注入到不同的属性中。大概有 3 种具体的实现形式:

1)基于构造函数。实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
2)基于 set 方法。实现特定属性的 public set 方法,让外部容器调用传入所依赖类型的对象。
3)基于接口。实现特定接口以供外部容器注入所依赖类型的对象,这种做法比较构造函数和 set 方法更为复杂

image.png

面向对象编程_五大原则

单一职责原则SRP(Single Responsibility Principle)

是指一个类的功能要单一,不能包罗万象。如同一个人一样,分配的工作不能太多,否则一天到晚虽然忙忙碌碌的,但效率却高不起来。

开放封闭原则OCP(Open-Close Principle)

一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。比如:一个网络模块,原来只服务端功能,而现在要加入客户端功能,那么应当在不用修改服务端功能代码的前提下,就能够增加客户端功能的实现代码,这要求在设计之初,就应当将服务端和客户端分开,公共部分抽象出来。

里式替换原则LSP(the Liskov Substitution Principle LSP)

子类应当可以替换父类并出现在父类能够出现的任何地方。比如:公司搞年度晚会,所有员工可以参加抽奖,那么不管是老员工还是新员工,也不管是总部员工还是外派员工,都应当可以参加抽奖,否则这公司就不和谐了。

依赖倒置原则DIP(the Dependency Inversion Principle DIP)

具体依赖抽象,上层依赖下层。假设B是较A低的模块,但B需要使用到A的功能,这个时候,B不应当直接使用A中的具体类: 而应当由B定义一抽象接口,并由A来实现这个抽象接口,B只使用这个抽象接口:这样就达到了依赖倒置的目的,B也解除了对A的依赖,反过来是A依赖于B定义的抽象接口。通过上层模块难以避免依赖下层模块,假如B也直接依赖A的实现,那么就可能 造成循环依赖。一个常见的问题就是编译A模块时需要直接包含到B模块的cpp文件,而编译B时同样要直接包含到A的cpp文件。

接口分离原则ISP(the Interface Segregation Principle ISP)

模块间要通过抽象接口隔离开,而不是通过具体的类强耦合起来。

面向对象编程有什么缺点?为什么我们推荐函数式编程

image.png

函数式编程

  • 函数是"第一等公民"
  • 纯函数/无副作用
  • 高阶函数/闭包

First Class Function(第一等公民)

是指函数跟其它的数据类型一样处于平等地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值。


// 赋值
var func1 = function func1() {  }
// 函数作为参数
function func2(fn) {
    fn()
}   
// 函数作为返回值
function func3() {
    return function() {}
}

Pure Function(纯函数)

指相同的输入总会得到相同的输出,并且不会产生副作用的函数。

优势: 可缓存 可移植 可测试 可推理 可并行


// 是纯函数
function sum(x,y){
    return x + y
}
// 输出不确定,不是纯函数
function random(x){
    return Math.random() * x
}
// 有副作用,不是纯函数
function setFontSize(el,fontsize){
    el.style.fontsize = fontsize ;
}
// 输出不确定、有副作用,不是纯函数
let count = 0;
function addCount(x){
    count+=x;
    return count;
}

Currying— 柯理化

是一种处理多元函数的方法。它产生一系列连锁函数,其中每个函数固定部分参数,并返回一个新函数,用于传回其它剩余参数的功能。

实现一个通用的柯理化函数,需要以下几个条件:

  1. 首先需要一个函数fn作为参数;
  2. 其次,可以获取到fn声明时虚参的数量,通过fn.length属性可以实现;
  3. 最后,可以判断返回接受剩余参数的新函数,或者返回fn(...参数)执行结果,以及缓存已经固定的参数。通过fn.length、闭包和递归可以实现。

image.png

Composition- 函数组合

将多个简单的函数,组合成一个更复杂的函数的行为或机制。每个函数的执行结果,作为参数传递给下一个函数,最后一个函数的执行结果就是整个函数的结果。

用途

      1.避免洋葱嵌套代码,提高代码的可读性
      2.组合多个单一功能函数生成特定功能的新函数
      3.把功能复杂的函数拆解成功能相对单一的函数,便于维护和复用

image.png

Functor- 函子

  • Functor 是实现了map函数并遵守一些特定规则的容器类型。
  • 把值留在容器中,只能暴露出map接口处理它。

经查阅资料 解释什么函子:

const add = (a:number) => { return { value: a + 5 } }
const double = (x:number) => { return { value: x * 2 } }

// ...
const format = pipe(add, double, square)
const result = format(3) // NaN

假如add(3)的返回值是{value:8},但是double函数的参数要求number类型,另外double的执行结果{ value: 8 } * 2 => NaN。这显然不符合函数组合的应用场景。为了保证函数返回值和参数的衔接性,让它适用于函数组合的应用场景。我们可以做一些额外的工作,处理一下add函数的返回值,让它符合double函数的参数要求。

const format = pipe(add, (data)=>double(data.value), (data)=>square(data.value))
const result = format(3) // 256

这种处理参数的方式,实际是一种映射:{value: number} => number。为了方便使用,我们把它整理成函数(map)。

响应式编程

  • 异步/离散的函数式编程

    • 数据流

    • 操作符

      • 过滤
      • 合并
      • 转化
      • 高阶

观察者模式 指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。

观察者模式是一种对象行为型模式,其主要优点如下:

  1. 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。

  2. 目标与观察者之间建立了一套触发机制。

它的主要缺点如下:

  1. 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
  2. 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。

image.png

迭代器模式 提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。迭代器模式是一种对象行为型模式,其主要优点如下:

  1. 访问一个聚合对象的内容而无须暴露它的内部表示。
  2. 遍历任务交由迭代器完成,这简化了聚合类。
  3. 它支持以不同方式遍历一个聚合,甚至可以自定义迭代器的子类以支持新的遍历。
  4. 增加新的聚合类和迭代器类都很方便,无须修改原有代码。
  5. 封装性良好,为遍历不同的聚合结构提供一个统一的接口。
    其主要缺点是:增加了类的个数,这在一定程度上增加了系统的复杂性。

响应式编程_操作符

  • 合并
  • 过滤
  • 转化
  • 异常处理
  • 多播

image.png

响应式编程_Monad

  • 去除嵌套的Observable

image.png

总结

image.png

四、领域特定语言

定义:领域特定语言指的就是专注于某个应用程序领域的计算机语言 。其目的是让用户通过阅读代码就可以明白建模里的业务规则,而非一堆架构图上的框框和箭头。

  • HTML 是我们熟知的用于描述 Web 页面的标记语言;
  • CSS 是用于描述页面样式的语言;SQL 是用于创建和检索关系型数据库的语言;
  • SQL 是用于创建和检索关系型数据库的语言;

与 DSL 处在对立面的就是 GPL(General Purpose Language),即通用编程语言,没有特定的使用场景,用来实现任意领域的计算机程序,这种能力为具有通用的表达力。

  • C/C++
  • JavaScript
  • ....

语言运行

image.png

lexer

SQL Token分类

  • 注释
  • 关键字
  • 操作符
  • 空格
  • 字符串
  • 变量

image.png

Parser

语法规则

上下文无关语法规则

image.png

  • 推导式:表示非终结符到(非终结符或终结符)的关系。
  • 终结符:构成句子的实际内容。可以简单理解为词法分析中的token.
  • 非终结符:符号或变量的有限集合。它们表示在句子中不同类型的短语或子句。

Parser_LL

LL:从左到右检查,从左到右构建语法树

image.png

Parser_LR

LR:从左到右检查,从右到左构建语法树

LL(K) > LR(1) > LL(1),其中k,1,1表示构建语法树需要向下看的数量

image.png

tools

用于设计域特定语言,然后生成用户创建基于该语言的模型必需的所有内容。

image.png

visitor

对生成的语法树进行遍历 image.png

课程总结

image.png

参考资料: 【1】面向对象三大特性五大原则 + 低耦合高内聚 - 凝果屋的韩亦乐 - 博客园 (cnblogs.com)