编程范式学习 | 青训营笔记

134 阅读15分钟

编程范式

编程范式是指在计算机编程和软件开发中使用的一种风格或方法,它规范了代码的结构、组织和解决问题的方法。

编程范式是一种计算思维的方法,它帮助开发人员更好地理解和设计计算机程序,从而提高代码的可读性、可维护性和可重用性。不同的编程范式有不同的语法、结构和风格,例如面向对象编程、函数式编程、过程式编程等。

一些流行的编程范式包括:

  1. 面向对象编程: 将应用程序的功能视为对象,将对象组织成类,然后使用继承和多态性来实现代码重用和组合。
  2. 函数式编程: 重视函数的返回值而不是执行时间,通过函数的纯粹性来避免副作用,使程序更加简洁和安全。
  3. 原型设计: 通过小规模的原型来验证新想法,以便更容易地发现和修复错误。
  4. 数据流编程: 将程序看作一个不断流动的数据流,通过对数据流的操作来实现算法和数据处理。
  5. 状态编程: 将程序看作一系列状态的转换,通过保存状态来实现代码重用和缓存。
  6. 模型编程: 将程序看作一个模型,通过定义模型结构和属性来实现代码重用和扩展。
  7. 设备编程: 将程序看作一个设备,通过访问硬件资源来实现特定的功能。
  8. 用户空间编程: 将程序看作用户空间中的进程或线程,通过运行时代码调用硬件函数来实现高效的并行计算。

过程式

1.结构化编程

(1)定义

结构化编程是一种编程范例,旨在通过广泛使用选择(if / then / else)和重复(while and for),块结构的结构化控制流构造来提高计算机程序的清晰度、质量和开发时间。它在1950年代后期出现,出现了ALGOL 58和ALGOL 60编程语言,后者包括对块结构的支持。结构化编程最常与偏差一起使用,以便在某些特定情况下(例如,当必须执行异常处理时)可以使程序更清晰。

(2)特点

结构化编程的主要特点包括:

  1. 将程序制作为单个结构的编程方法,代码将一条接一条地执行一条指令。
  2. 不支持在诸如 GOTO 等任何语句的帮助下从一条指令跳转到另一条指令的可能性。 因此,这种方法中的指令将以串行和结构化的方式执行。
  3. 程序必须维护良好、整洁,且逻辑清晰。

(3)优缺点

结构化编程的优点包括:

  1. 更容易阅读和理解。
  2. 方便使用。
  3. 更易于维护。
  4. 主要基于问题而不是基于机器。
  5. 更容易调试。
  6. 机器无关,主要是针对人类的编程,而不是针对机器的编程。

结构化编程的缺点:由于它是 Machine-Independent,所以转换成机器码需要时间。 转换后的机器码与汇编语言不同。 该程序取决于可变因素,如数据类型。 因此,它需要根据旅途中的需要进行更新。 通常这种方法的开发需要更长的时间,因为它依赖于语言。 而在汇编语言的情况下,开发时间较短,因为它是为机器固定的。

总之,结构化编程是一种非常重要的编程范式,它可以提高代码的可读性、可维护性和可重用性,使得程序更加清晰、简洁和安全。虽然结构化编程有一些缺点,但是它在许多情况下都比较实用和有效。

2.JS中的面向过程

在JavaScript中,面向过程的编程方式与面向对象的编程方式有很大的不同。下面是一些面向过程编程的基本概念:

  1. 声明变量:在面向过程编程中,我们通常需要在程序中声明变量。变量是存储数据的容器,它们可以被用来存储数据或执行计算。
  2. 定义函数:在面向过程编程中,我们通常需要定义函数来执行特定的任务。函数是一段代码,用于执行特定的操作。
  3. 执行代码:在面向过程编程中,我们通常需要按照程序中定义的顺序执行代码。每一行代码通常对应程序中的一条指令。
  4. 控制流程:在面向过程编程中,我们通常需要使用控制流程来控制程序的执行流程。控制流程包括条件语句、循环语句和其他控制程序执行流程的语句。
  5. 异常处理:在面向过程编程中,我们通常需要处理程序中出现的异常情况。异常是指程序运行过程中出现的错误情况,如除以0、无法访问某个变量等。

下面是一些面向过程编程的示例代码:

// 声明变量  
var num1 = 10;  
var num2 = 20;  
  
// 定义函数  
function add(num1, num2) {  
  return num1 + num2;  
}  
  
// 执行代码  
console.log(add(num1, num2));  
  
// 控制流程  
if (num1 < num2) {  
  console.log("num1 is less than num2");  
} else {  
  console.log("num1 is greater than or equal to num2");  
}  
  
// 异常处理  
throw new Error("An error occurred");

在上面的示例代码中,我们定义了两个变量 num1num2,然后定义了一个函数 add,用于将两个数相加并返回结果。接着,我们执行了一些代码,包括输出结果和控制程序执行流程。在控制流程中,我们使用了 if 语句来判断 num1 是否小于 num2,如果是,则输出一条消息。如果不是,则输出另一条消息。在异常处理中,我们使用了 throw 关键字来抛出一个异常。

需要注意的是,在JavaScript中,函数是一等公民,也就是说,函数可以像变量一样被声明和使用。此外,JavaScript中没有像C++或Java那样的命名空间或作用域,因此函数的名称是全局唯一的。

面向对象

1.封装

封装是面向对象编程的重要特性,它将对象的状态和行为包装在一起,并通过接口来控制对内部信息的访问。以下是一个JS封装的示例:

function Car(model, year, miles) {
  this.model = model;
  this.year = year;
  this.miles = miles;

  this.getDetails = function() {
    return `${this.model} made in ${this.year} with ${this.miles} miles`;
  };
}

const myCar = new Car('BMW', 2020, 10000);
console.log(myCar.getDetails()); // BMW made in 2020 with 10000 miles

在上述代码中,我们定义了一个Car构造函数,并通过this关键字将modelyear以及miles属性赋值对应的参数值。getDetails方法用来获取汽车的详细信息,这个方法被封装在对象内部,通过外部调用来访问。

2.继承

继承是指从已有的类中派生出新的类,并且新的类可以获取到已有类的属性和方法。以下是一个JS继承的示例:

function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(`${this.name} is eating food`);
}

function Dog(name) {
  Animal.call(this, name);
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
  console.log(`Woof!`);
};

const myDog = new Dog('Buddy');
myDog.eat(); // Buddy is eating food
myDog.bark(); // Woof!

在这个示例中,我们定义了一个Animal构造函数,并将其原型对象上挂载了一个eat方法。然后我们定义了Dog类,通过Object.create方法来继承了Animal类的原型对象并添加了自己的bark方法。最后通过Animal.call(this, name);来继承Animal类的name属性,从而实现了继承。

3.多态

多态是基于继承的一种特性,它允许具有不同类型的对象对同一消息作出响应。以下是一个JS多态的示例:

class Shape {
  constructor() {
    this.area = 0;
  }

  calculateArea() {
    console.log(`Area of shape: ${this.area}`);
  }
}

class Square extends Shape {
  constructor(length) {
    super();
    this.length = length;
    this.area = this.length * this.length;
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
    this.area = this.width * this.height;
  }
}

const shapes = [new Square(5), new Rectangle(10, 20)];

shapes.forEach(shape => shape.calculateArea());

在这个示例中,我们定义了一个Shape抽象类,它的属性area以及方法calculateArea都是由子类去具体实现的。在SquareRectangle两个类中,我们通过重写calculateArea方法,实现了不同的计算面积方式。在主程序入口中,我们使用多态来输出不同的图形面积。

函数式编程

1.定义和优点

函数式编程是一种编程范式,它将程序看做是一组函数的组合。在函数式编程中,函数被视为对输入数据进行转换的映射,其核心思想在于避免状态和变量的使用,避免副作用,并鼓励使用纯函数。

以下是函数式编程的一些优点:

1.更容易理解

由于函数式编程中的函数是纯函数,只依赖于函数的输入而不会影响外部状态,因此这些函数更容易理解和推理。

2.更容易测试

由于函数式编程中的函数是纯函数,它们只依赖于参数的值来返回输出,因此很容易测试这些函数。

3.可以节省时间和资源

函数式编程中的函数是不可变的,因此它们可以在多个线程和进程之间共享使用,从而提高效率。

4.更简洁和可读性更强的代码

函数式编程中的函数通常是短小精悍的,因此代码更简洁易读。

5.鲁棒性更高

由于函数式编程中的函数是纯函数,它们对于输入数据的非法值不会有意外影响,从而使程序更加健壮。

2.柯里化函数

(1)定义

柯里化函数是一种特殊的函数转化技巧,它将一个接受多个参数的函数转化为一系列只接受单一参数的函数,并返回一个新的函数。这种技术可以减少代码的复杂性,使代码更加简洁易懂。

(2)结构

柯里化函数的结构与普通函数类似,但它只接受一个参数并返回一个新的函数。新函数的返回值类型必须与原函数的返回值类型相同或是其子类型。

(3)作用

柯里化函数的作用主要有以下几点:

  1. 简化代码:通过柯里化函数,可以将一个复杂的函数转化为多个简单的函数,从而简化代码。
  2. 提高可读性:柯里化函数可以将一个复杂的函数转化为多个简单的函数,使代码更加易懂。
  3. 提高效率:柯里化函数可以将一个复杂的函数转化为多个简单的函数,减少了函数调用的次数,提高了代码的效率。
  4. 增强代码的灵活性:柯里化函数可以将一个复杂的函数转化为多个简单的函数,使得代码更加灵活,可以适应更多的情况。

(4)代码举例

function curry(func, arity) {  
  const curriedFunc = (...args) =>  
    func(...args)  
      .then(result => curriedFunc(...args, result))  
      .catch(error => {  
        throw new Error(error)  
      })  
  return curriedFunc  
}  
  
const add = (a, b) => a + b  
  
const curriedAdd = curry(add)  
  
console.log(curriedAdd(1, 2)(3)) // 输出 5  
console.log(curriedAdd(1, 2))        // 抛出异常  
console.log(curriedAdd(3)(2))        // 输出 5

这个柯里化函数接受两个参数,一个是要应用柯里化的函数,另一个是函数的参数数量。它返回一个新的函数,这个新函数接受相同数量的参数,并返回一个新的结果。

这个示例使用curriedAdd函数作为一个示例。首先定义一个原始的add函数,然后使用curriedAdd函数将它应用于原始函数,返回一个新的函数。

在这个示例中,我们首先调用curriedAdd函数,传入1和2作为参数,得到一个新的函数。然后我们调用这个新函数,传入3作为参数,得到一个新的结果。

接下来,我们调用原始函数,传入3作为参数,得到一个新的结果。然后我们尝试调用新函数,但这时柯里化函数已经将函数异步执行了,导致curriedAdd函数抛出了一个异常。

最后,我们调用原始函数,传入2作为参数,得到一个新的结果。然后我们尝试调用新函数,这时柯里化函数已经将函数异步执行了,导致curriedAdd函数抛出了一个异常。

这个示例展示了如何使用柯里化函数来简化代码并提高可读性。

3.组合函数

组合函数是指可以从给定的一组元素中选择若干个元素,并按照指定的规则对这些元素进行运算,最终得到一个或多个结果的函数。组合函数具有以下特点:

  1. 通用性:组合函数可以用于解决各种不同的问题,而不仅仅局限于某一特定的领域。
  2. 递归性:组合函数通常会涉及到递归调用,这是因为它们需要处理多个元素的情况。
  3. 非线性:组合函数通常需要处理非线性的问题,例如在一个函数中需要对多个元素进行加减乘除等运算。
  4. 状态转移:组合函数中可能存在一些被调用的参数或边界情况,因此可以采用状态转移方法,通过遍历集合中的所有元素,找到正确的解决方案。
function combination(n, k) {  
  if (k === 0) {  
    return [[]];  
  } else if (n === 0) {  
    return [];  
  } else if (k < n) {  
    return [[]];  
  } else {  
    return [[]];  
  }  
}  
  
const combinations = combination(5, 2);  
  
for (let i = 0; i < combinations.length; i++) {  
  for (let j = 0; j < combinations[i].length; j++) {  
    console.log(combinations[i][j]);  
  }  
}

这个组合函数接受两个参数n和k,其中n表示集合中的元素个数,k表示选择的元素个数。它返回一个由n个列表组成的列表,每个列表中包含了k个不同的组合。这个函数的基本思路是从左到右遍历集合中的所有元素,按照递归的方式生成组合,最终返回所有的组合。

在这个示例中,我们定义了一个名为combination的函数,它接受两个参数n和k,并返回一个由n个列表组成的列表,每个列表中包含了k个不同的组合。我们调用combination函数,传入5和2作为参数,得到一个由5个列表组成的列表。然后,我们使用两个嵌套的for循环遍历这个列表,并打印出每个元素的值。

响应式编程

响应式编程是一种编程范式,它可以在运行时根据数据的变化形态,自动更新计算结果。它的目标是在运行时根据数据的变化形态,自动更新计算结果,从而避免编写大量重复代码。

在传统的编程中,程序通常在编写时就已经确定了其计算结果,而不会根据数据的变化形态进行更新。因此,如果需要处理数据变化较快的应用程序,这种编程范式可能会导致性能问题。响应式编程的目标是在运行时根据数据的变化形态,自动更新计算结果,以提高应用程序的性能。

响应式编程通常包括以下几个步骤:

  1. 数据流分析:分析应用程序中的数据流,确定数据的变化形态和更新规则。
  2. 数据格式转换:对于不同的数据格式,需要进行相应的数据格式转换。
  3. 计算结果更新:根据数据的变化形态,自动更新计算结果。
  4. 模型选择和实现:选择合适的计算模型来实现响应式编程。
  5. 编译和测试:将代码编译成可执行文件,并进行测试以确保其在不同数据格式和计算模型下都能正常运行。

响应式编程有许多不同的实现方式,包括 Reactive Programming、Lambda表达式、函数式编程、Stream API 等等。不同的实现方式适用于不同的应用场景,开发者需要根据具体情况选择最适合的实现方式。

1.观察者模式

观察者模式(Observer Pattern)是一种对象行为模式,它定义了对象间的一种一对多的依赖关系。在这种模式中,一个对象通过观察另一个对象的状态来更新自己的状态。

观察者模式的优点包括:

  1. 状态共享:多个依赖对象可以观察同一个状态,从而实现状态共享。
  2. 简化状态更新:通过让依赖对象观察另一个对象的状态,可以简化状态的更新操作。
  3. 降低耦合性:观察者模式可以降低多个对象之间的耦合性,使它们更容易进行修改和管理。
  4. 代码简洁:观察者模式可以使代码更加简洁,因为不需要频繁地创建和销毁观察者对象。
  5. 易于测试:观察者模式可以方便地测试多个依赖对象之间的状态依赖关系。

需要注意的是,观察者模式并不是万能的,有一些情况下可能会出现问题。例如,当观察者对象和被观察对象之间存在复杂的依赖关系时,可能会导致状态不一致或死锁等问题。因此,在使用观察者模式时,需要仔细考虑其适用场景和限制。

2.迭代器模式

迭代器模式(Iterator Pattern)是一种对象行为型设计模式,它允许通过一个迭代器对象来遍历和操作一个集合(如数组、列表、映射等)中的元素,而无需暴露该集合的内部表示。

迭代器模式的优点包括:

  1. 封装性:迭代器模式将集合的内部表示和操作封装在一个迭代器对象中,使得外部代码只需要关注如何使用迭代器对象,而无需关心集合的具体实现细节。
  2. 易用性:使用迭代器对象可以避免与具体集合实现的直接接触,降低了开发者与底层集合的耦合度,提高了代码的可维护性和可测试性。
  3. 可扩展性:迭代器模式支持在不改变集合本身结构的情况下,为其定制迭代器,从而满足不同应用场景的需求。
  4. 线程安全性:迭代器模式保证了在多线程环境下对集合的访问不会发生冲突,提高了程序的并发性和可靠性。
  5. 支持异步编程:使用迭代器对象可以实现异步操作,避免了程序在等待某些操作完成时阻塞整个线程,提高了程序的响应性和效率。

需要注意的是,迭代器模式并不是适用于所有情况的最佳设计模式,有些情况下可能需要使用其他设计模式,例如生成器模式、装饰器模式等。此外,迭代器模式的实现可能会涉及到对象池、懒加载等技术,需要开发者根据实际情况进行选择和实现。

学习编程范式比较枯燥,但是意义也是很大的。编程是一种思维方式,而编程范式是指用于描述和定义编程思维的规则和约束。不同的编程范式有着不同的语言特性、设计风格和编程习惯,但都旨在提高代码的可读性、可维护性和可扩展性。学习编程范式可以让开发人员更加专注于解决问题本身,而不是被迫处理复杂的代码结构和细节。同时,学习编程范式也有助于提高代码的可读性、可维护性和可重用性,从而提高代码的质量和生产力。