天天看点

编写高质量可维护的代码:程序范式

编写高质量可维护的代码:程序范式
这是第 92 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:编写高质量可维护的代码:程序范式

前言

什么是编程范式呢?

编程范式(Programming paradigm)是一类典型的编程风格,是指从事软件工程的一类典型的风格(可以对照方法学)。如:函数式编程、过程式编程、面向对象编程、指令式编程等等为不同的编程范式。

JS 是一种动态的基于原型和多范式的脚本语言,并且支持面向对象(OOP,Object-Oriented Programming)、命令式和声明式(如函数式(Functional Programming)编程)的编程风格。

编写高质量可维护的代码:程序范式

那么面向对象,命令式,声明式编程到底是什么呢?他们有什么区别呢?

命令式编程

命令式编程是一种描述计算机所需作出的行为的编程典范,即一步一步告诉计算机先做什么再做什么。举个简单的????: 找出所有人中年龄大于 35 岁的,你就需要这样告诉计算机:

  1. 创建一个新的数组 newArry 存储结果;
  2. 循环遍历所有人的集合 people;
  3. 如果当前人的年龄大于 35,就把这个人的名字存到新的数组中;
const people = [
    { name: 'Lily', age: 33 },
    { name: 'Abby', age: 36 },
    { name: 'Mary', age: 32 },
    { name: 'Joyce', age: 35 },
    { name: 'Bella', age: 38 },
    { name: 'Stella', age: 40 },
  ];
  const newArry = [];
  for (let i = 0; i < people.length; i++) {
    if (people[i].age > 35) {
      newArry.push(people[i].name);
    }
  }      

命令式编程的特点是非常易于理解,按照实际的步骤实现,优点就在于性能高,但是会依赖,修改较多外部变量,可读性低;

声明式编程

声明式编程与命令式编程是相对立的,只需要告诉计算机要做什么而不必告诉他怎么做。声明式语言包括数据库查询语(SQL),正则表达式,逻辑编程,函数式编程和组态管理系统。 上边的例子用声明式编程是这样的:

const peopleAgeFilter = (people) => {
return people.filter((item) => item.age > 35)
}      

函数式编程

什么是函数式编程呢?

函数式编程这里的函数并不是我们所知道的 Function,而是数学中的函数,即变量之间的映射,输入通过函数都会返回有且只有一个输出值。

// js 中的 function
function fun(data, value, type) {
  // 逻辑代码
}
// 函数
y=f(x)      

早在 1958 年,随着被创造出来的 LISP,函数式编程就已经问世。在近几年,在前端领域也逐渐出现了函数式编程的影子:箭头函数、map、reduce、filter,同时 Redux 的 Middleware 也已经应用了函数式编程...

函数式编程的特性
  • 函数是"第一等公民"

    所谓"第一等公民",指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。 例如:

  let fun = function(i){
    console.log(i);
  }
  [1,2,3].forEach(element => {
    fun(element);
  });      
  • 惰性计算

    在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。即函数只在需要的时候执行。

  • 没有"副作用"

    "副作用"指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。由于 JS 中对象传递的是引用地址,即使我们使用 const 关键词声明对象时,它依旧是可以变的。这样就会导致我们可能会随意修改对象。 例如:

 const user = {
  name: 'jingjing',
}
const changeName = (obj, name) => obj.name = name;
const changeUser = changeName(user, 'lili');
console.log(user); // {name: "lili"} user 对象已经被改变      

改成无副作用的纯函数的写法:

const user = {
  name: 'jingjing',
}
// const changeName = (obj, name) => obj.name = name;
const changeName = (obj, name) => ({...user, name }); 
const changeUser = changeName(user, 'lili');
console.log(user); // {name: "jingjing"}, 此时user对象并没有改变      
  • 引用透明性

    即如果提供同样的输入,那么函数总是返回同样的结果。就是说,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

在函数式编程中柯里化(Currying)和函数组合(Compose)是必不可少。

  • 柯里化

网上关于柯里化的文章很多,这里不再赘述,可以参考这里,函数柯里化Currying。

柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。简单来说,就是只传递给函数一个参数来调用它,让它返回一个函数去处理剩下的参数。即:

f(x, y, z) -> f(x)(y)(z)      

如下例,求两个数的平方和:

// 原始版本
const squares = function(x, y) {
  return x * x + y * y;
}
// 柯里化版本
const currySquares = function(x) {
    return function(y){
    return x * x + y * y;
    }
}
console.log(squares(1,2));
console.log(currySquares(1)(2));      

在柯里化版本中,实际的执行如下:

currySquares(1) = function(y){
  return 1 + y * y;
}
currySquares(1)(2) = 1 + 4 = 5;      
  • 函数组合(Compose)

    函数组合就是将两个或多个函数组合起来生成一个新的函数。

    在计算机科学中,函数组合是将简单函数组合成更复杂函数的一种行为或机制。就像数学中通常的函数组成一样,每个函数的结果作为下一个函数的参数传递,而最后一个函数的结果是整个函数的结果。所以说柯里化是函数组合的基础。

例如:

双函数情况:

const compose = (f, g) => x => f(g(x))
const f = x => x * x;
const g = x => x + 2;
const composefg = compose(f, g);
composefg(1) //9      

对于多函数情况,简单实现如下:

const compose = (...fns) => (...args) => fns.reduceRight((val, fn) => fn.apply(null, [].concat(val)), args);
const f = x => x * x;
const g = x => x + 2;
const h = x => x - 3;
const composefgh = compose(f, g, h);
composefgh(5); // 16      

声明式编程的特点是不产生“副作用”,不依赖也不会改变当前函数以外的数据,优点在于:

  1. 减少了可变变量,程序更加安全;
  2. 相比命令式编程,少了非常多的状态变量的声明与维护,天然适合高并发多现成并行计算等任务,这也是函数式编程近年又大热的重要原因;
  3. 代码更为简洁,接近自然语言,易于理解,可读性更强。 但是函数编程也有天然的缺陷:
  4. 函数式编程相对于命令式编程,往往会对方法过度包装,导致性能变差;
  5. 由于函数式编程强调不产生“副作用”,所以他不擅长处理可变状态;

面向对象编程

面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

面向对象的两个基本概念:

  1. 类:类是对象的类型模板;例如:政采云前端 ZooTeam 是一个类;
  2. 实例:实例是根据类创建的对象;例如:ZooTeam 可以创建出刘静这个实例; 面向对象的三个基本特征:封装、继承、多态: 注⚠️:以下例子均采用 ES6 写法
  • 封装:封装即隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别;将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体。根据我的理解,其实就是把子类的属性以及公共的方法抽离出来作为公共方法放在父类中;
class Zcy {
  constructor(name){
      this.name = name;
  }
  doSomething(){
      let {name} = this;
      console.log(`${name}9点半在开晨会`);
  }
  static soCute(){
      console.log("Zcy 是一个大家庭!");   
  }
}
let member = new Zcy("jingjing", 18);
member.soCute();   // member.soCute is not a function
member.doSomething();  // jingjing9点半在开晨会
Zcy.soCute();  // Zcy 是一个大家庭!      

Zcy 的成员都有名字和年龄,九点半时都在开晨会,所以把名字和年龄当作共有属性, 九点半开晨会当作公共方法抽离出来封装起来。static 表示静态方法,静态方法只属于 Zcy 这个类,所以当 member 调用 soCute 方法时,控制台报错。

  • 继承:继承)就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。 子类继承父类后,子类就会拥有父类的属性和方法,但是同时子类还可以声明自己的属性和方法,所以子类的功能会大于等于父类而不会小于父类。
class Zcy {
  constructor(name){
      this.name = name;
  }
  doSomething(){
      let {name} = this;
      console.log(`${name}9点半在开晨会`);
  }
  static soCute(){
      console.log("Zcy 是一个大家庭!");   
  }
}
class ZooTeam extends Zcy{
    constructor(name){
        super(name);
    }
    eat(){
      console.log("周五一起聚餐!");
    }
}
let zooTeam = new ZooTeam("jingjing");
zooTeam.doSomething(); // jingjing9点半在开晨会
zooTeam.eat(); // 周五一起聚餐!
zooTeam.soCute();    // zooTeam.soCute is not a function      

ZooTeam 继承了 Zcy 的属性和方法,但是不能继承他的静态方法;而且 ZooTeam 声明了自己的方法 eat;

  • 多态:多态)按字面的意思就是“多种状态”,允许将子类类型的指针赋值给父类类型的指针。即同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。 多态的表现方式有重写,重载和接口,原生 js 能够实现的多态只有重写。
  • 重写:重写是子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。方法重写又称方法覆盖。
class Zcy {
  constructor(name){
      this.name = name;
  }
  getName(){
    console.log(this.name);
  }
  doSomething(){
      let {name} = this;
      console.log(`${name}9点半在开晨会`);
  }
  static soCute(){
      console.log("Zcy 是一个大家庭!");   
  }
}
class ZooTeam extends Zcy{
    constructor(name){
        super(name);
    }
    doSomething(){
      console.log("zooTeam周五要开周会!");
    }
}
const zcy = new Zcy('jingjing');
const zooTeam = new ZooTeam('yuyu');
zcy.doSomething(); // jingjing9点半在开晨会
zcy.getName(); // jingjing
zooTeam.doSomething(); // zooTeam周五要开周会!
zooTeam.getName(); // yuyu      

ZooTeam 为了满足自己的需求,继承了父类的 doSomething 方法后重写了 doSomething 方法,所以调用 doSomething 方法之后得到了不同的结果,而 getName 方法只是继承并没有重写;

面向对象编程的特点是抽象描述对象的基本特征,优点在于对象易于理解和抽象,代码容易扩充和重用。但是也容易产生无用代码,容易导致数据修改。

总结

命令式、声明式、面向对象本质上并没有优劣之分,面向对象和命令式、声明式编程也不是完成独立、有严格的界限的,在抽象出各个独立的对象后,每个对象的具体行为实现还是有函数式和过程式完成。在实际应用中,由于需求往往是特殊的,所以还是要根据实际情况选择合适的范式。

参考文章

面向对象之三个基本特征

简明 JavaScript 函数式编程——入门篇

一文读懂JavaScript函数式编程重点-- 实践 总结

JavaScript 中的函数式编程:函数,组合和柯里化

招贤纳士