天天看點

編寫高品質可維護的代碼:程式範式

編寫高品質可維護的代碼:程式範式
這是第 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 中的函數式程式設計:函數,組合和柯裡化

招賢納士