面向對象與函數式編程的簡單案例

2021-02-20 前端先鋒
// 每日前端夜話 第396篇
// 正文共:2600 字
// 預計閱讀時間:8 分鐘

介紹

先簡要介紹一下面向對象和函數式編程。

兩者都是編程範式,在允許和禁止的技術上有所不同。

有僅支持一種範式的程式語言,例如 Haskell(純函數式)。

還有支持多種範式的語言,例如 JavaScript,你可以用 JavaScript 編寫面向對象的代碼或函數式代碼,甚至可以將兩者混合。

創建項目

在深入探究這兩種編程範式之間的差異之前,先創建一個階乘計算器項目。

首先創建所需的所有文件和文件夾,如下所示:

$ mkdir func-vs-oop
$ cd ./func-vs-oop
$ cat index.html
$ cat functional.js
$ cat oop.js 

接下來在 index.html 內創建一個簡單的表單。

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
  <script src="functional.js" defer></script>
</head>
<body>
  <div class="container mt-5">
    <div class="container mt-3 mb-5 text-center">
      <h2>Functional vs OOP</h2>
    </div>
    <form id="factorial-form">
      <div class="form-group">
        <label for="factorial">Factorial</label>
        <input class="form-control" type="number" name="factorial" id="factorial" />
      </div>
      <button type="submit" class="btn btn-primary">Calculate</button>
    </form>
    <div class="container mt-3">
      <div class="row mt-4 text-center">
        <h3>Result:</h3>
        <h3 class="ml-5" id="factorial-result"></h3>
      </div>
    </div>
  </div>
</body>
</html>

為了使界面看上去不那麼醜陋,我們把 bootstrap 作為 CSS 框架。

如果在瀏覽器中顯示這個 HTML,應該是這樣的:

現在這個表單還沒有任何操作。

我們的目標是實現一種邏輯,在該邏輯中你可以輸入一個最大為 100 的數字。單擊「Calculate」按鈕後,結果應顯示在 result-div 中。

下面分別以面向對象和函數式的方式來實現。

函數式實現

首先為函數式編程方法創建一個文件。

$ cat functional.js

首先,需要一個在將此文件加載到瀏覽器時要調用的函數。

該函數先獲取表單,然後把我們需要的函數添加到表單的提交事件中。

function addSubmitHandler(tag, handler) {
  const form = getElement(tag);
  form.addEventListener('submit', handler);
}

addSubmitHandler("#factorial-form", factorialHandler);

首先聲明一個名為 addSubmitHandler 的函數。

這個函數有兩個參數,第一個是要在 HTML 中查找的標籤,第二個是要綁定到 Element 的 commit-event 的函數。

接下來,通過傳入#factorial-form 和函數名 factorialHandler 來調用此函數。

標籤前面的 # 表明我們正在尋找 HTML 中的 id 屬性。

如果現在嘗試運行該代碼,則會拋出錯誤,因為在任何地方都還沒有定義函數 getElement 和 factorialHandler。

因此,首先在 addSubmitHandler 函數前面定義 getElement ,如下所示:

function getElement(tag) {
  return document.querySelector(tag);
}

這個函數非常簡單,只返回通過傳入的標記找到的 HTML元素。但是稍後我們將重用此功能。

現在添加 factorialHandler 函數來創建核心邏輯。

function factorialHandler(event) {
  event.preventDefault();

  const inputNumber = getValueFromElement('#factorial');

  try {
    const result = calculateFactorial(inputNumber);
    displayResult(result);
  } catch (error) {
    alert(error.message);
  } 
}

把事件傳回後立即調用 preventDefault 。

這將阻止 Submit 事件的默認行為,你可以試試不調用 preventDefault 時單擊按鈕後會發生什麼。

之後,通過調用 getValueFromElement 函數從輸入欄位中獲取用戶輸入的值。在得到數字後,用函數 calculateFactorial 計算階乘,然後通過將結果傳遞給函數 displayResult 將結果展示到頁面。

如果該值的格式不正確或者數字大於 100,將會拋出錯誤並彈出 alert。

下一步,創建另外兩個輔助函數:getValueFromElement 和 displayResult,並將它們添加到 getElement 函數後面。

function getValueFromElement(tag) {
  return getElement(tag).value;
}

function displayResult(result) {
  getElement('#factorial-result').innerHTML = result
}

這兩個函數都使用我們的 getElement 函數。這種可重用性是為什麼函數式編程如此有效的一個原因。

為了使它更加可重用,可以在 displayResult 上添加名為 tag 第二個參數。

這樣就可以動態設置應該顯示結果的元素。

但是在本例中,我用了硬編碼的方式。

接下來,在 factoryHandler 前面創建 calculateFactorial 函數。

function calculateFactorial(number) {
  if (validate(number, REQUIRED) && validate(number, MAX_LENGTH, 100) && validate(number, IS_TYPE, 'number')) {
    return factorial(number);
  } else {
    throw new Error(
      'Invalid input - either the number is to big or it is not a number'
    );
  }
}

接著創建一個名為 validate 的函數來驗證參數 number 是否為空且不大於 100,且類型為 number。如果檢查通過,就調用 factorial 函數並返回其結果。如果沒有通過,則拋出在 factorialHandler 函數中捕獲的錯誤。

const MAX_LENGTH = 'MAX_LENGTH';
const IS_TYPE = 'IS_TYPE';
const REQUIRED = 'REQUIRED';

function validate(value, flag, compareValue) {
  switch (flag) {
    case REQUIRED:
      return value.trim().length > 0;
    case MAX_LENGTH:
      return value <= compareValue;
    case IS_TYPE:
      if (compareValue === 'number') {
        return !isNaN(value);
      } else if (compareValue === 'string') {
        return isNaN(value);
      }
    default:
      break;
  }
}

在這個函數中,用 switch 來確定要執行的驗證範式類型。這只是一個簡單的值驗證。

然後在 calculateFactorial 聲明的前面添加實際的 factor 函數。這是最後一個函數。

function factorial(number) {
  let returnValue = 1;
  for (let i = 2; i <= number; i++) {
    returnValue = returnValue * i;
  }
  return returnValue;
}

最終的 functional.js 文件下所示:

const MAX_LENGTH = 'MAX_LENGTH';
const IS_TYPE = 'IS_TYPE';
const REQUIRED = 'REQUIRED';

function getElement(tag) {
  return document.querySelector(tag);
}

function getValueFromElement(tag) {
  return getElement(tag).value;
}

function displayResult(result) {
  getElement('#factorial-result').innerHTML = result
}

function validate(value, flag, compareValue) {
  switch (flag) {
    case REQUIRED:
      return value.trim().length > 0;
    case MAX_LENGTH:
      return value <= compareValue;
    case IS_TYPE:
      if (compareValue === 'number') {
        return !isNaN(value);
      } else if (compareValue === 'string') {
        return isNaN(value);
      }
    default:
      break;
  }
}

function factorial(number) {
  let returnValue = 1;
  for (let i = 2; i <= number; i++) {
    returnValue = returnValue * i;
  }
  return returnValue;
}

function calculateFactorial(number) {
  if (validate(number, REQUIRED) && validate(number, MAX_LENGTH, 100) && validate(number, IS_TYPE, 'number')) {
    return factorial(number);
  } else {
    throw new Error(
      'Invalid input - either the number is to big or it is not a number'
    );
  }
}

function factorialHandler(event) {
  event.preventDefault();

  const inputNumber = getValueFromElement('#factorial');

  try {
    const result = calculateFactorial(inputNumber);
    displayResult(result);
  } catch (error) {
    alert(error.message);
  } 
}

function addSubmitHandler(tag, handler) {
  const form = getElement(tag);
  form.addEventListener('submit', handler);
}

addSubmitHandler("#factorial-form", factorialHandler);

在這種方法中,我們專門處理函數。每個函數都只有一個目的,大多數函數可以在程序的其他部分中重用。

對於這個簡單的 Web 程序,使用函數式的方法有些過分了。接著將編寫相同的功能,只不過這次是面向對象的。

面向對象的實現

首先,需要將 index.html 文件的腳本標籤中的 src 更改為以下內容。

<script src="oop.js" defer></script>

然後創建 oop.js 文件。

$ cat oop.js

對於面向對象方法,我們要創建三種不同的類,一種用於驗證,一種用於階乘計算,另一種用於處理表單。

先是創建處理表單的類。

class InputForm {
  constructor() {
    this.form = document.getElementById('factorial-form');
    this.numberInput = document.getElementById('factorial');

    this.form.addEventListener('submit', this.factorialHandler.bind(this));
  }

  factorialHandler(event) {
    event.preventDefault();

    const number = this.numberInput.value;

    if (!Validator.validate(number, Validator.REQUIRED) 
      || !Validator.validate(number, Validator.MAX_LENGTH, 100)
      || !Validator.validate(number, Validator.IS_TYPE, 'number'))
      {
        alert('Invalid input - either the number is to big or it is not a number');
        return;
      }

      const factorial = new Factorial(number);
      factorial.display();
  }
}

new InputForm();

在構造函數中獲取 form-element 和 input-element 並將其存儲在類變量(也稱為屬性)中。之後將方法 factorialHandler 添加到 Submit-event 中。在這種情況下需要把類的 this 綁定到方法。如果不這樣做,將會得到一個引用錯誤,例如調用 this.numberInput.value 將會是 undefined。之後以事件為參數創建類方法 factorialHandler。

該方法的代碼看起來應該有點熟悉,例如 if 語句檢查輸入值是否有效,就像在 calculateFactorial 函數中所做的那樣。Validator.validate 是對我們仍然需要創建的 Validator 類中的靜態方法的調用。如果使用靜態方法,則無需初始化對象的新實例。驗證通過後創建 Factorial 類的新實例,傳遞輸入值,然後將計算的結果顯示給用戶。

接下來在 InputForm 類 前面創建 Validator 類。

class Validator {
  static MAX_LENGTH = 'MAX_LENGTH';
  static IS_TYPE = 'IS_TYPE';
  static REQUIRED = 'REQUIRED';

  static validate(value, flag, compareValue) {
    switch (flag) {
      case this.REQUIRED:
        return value.trim().length > 0;
      case this.MAX_LENGTH:
        return value <= compareValue;
      case this.IS_TYPE:
        if (compareValue === 'number') {
          return !isNaN(value);
        } else if (compareValue === 'string') {
          return isNaN(value);
        }
      default:
        break;
    }
  }
}

這個類內部的所有內容都是靜態的,所以我們不需要任何構造函數。

這樣做的好處是不需要在每次使用它時都初始化該類。

validate 與 validate 函數與我們的 functional.js 幾乎完全相同。

接下來在 Validator 類的後面創建 Factorial 類。

class Factorial {
  constructor(number) {
    this.resultElement = document.getElementById('factorial-result');
    this.number = number;
    this.factorial = this.calculate();
  }

  calculate() {
    let returnValue = 1;
    for (let i = 2; i <= this.number; i++) {
      returnValue = returnValue * i;
    }
    return returnValue;
  }

  display() {
    this.resultElement.innerHTML = this.factorial;
  }
}

在初始化這個類的實例後,我們獲得 resultElement 並將其存儲為屬性以及我們傳入的數字。

之後調用方法 calculate 並將其返回值存儲在屬性中。calculate 方法包含與 functional.js 中的 factor 函數相同的代碼。最後是 display 方法,該方法將結果元素的  innerHTML 設置為現實計算出的階乘數。

完整的 oop.js 文件如下所示。

class Validator {
  static MAX_LENGTH = 'MAX_LENGTH';
  static IS_TYPE = 'IS_TYPE';
  static REQUIRED = 'REQUIRED';

  static validate(value, flag, compareValue) {
    switch (flag) {
      case this.REQUIRED:
        return value.trim().length > 0;
      case this.MAX_LENGTH:
        return value <= compareValue;
      case this.IS_TYPE:
        if (compareValue === 'number') {
          return !isNaN(value);
        } else if (compareValue === 'string') {
          return isNaN(value);
        }
      default:
        break;
    }
  }
}

class Factorial {
  constructor(number) {
    this.resultElement = document.getElementById('factorial-result');
    this.number = number;
    this.factorial = this.calculate();
  }

  calculate() {
    let returnValue = 1;
    for (let i = 2; i <= this.number; i++) {
      returnValue = returnValue * i;
    }
    return returnValue;
  }

  display() {
    this.resultElement.innerHTML = this.factorial;
  }
}

class InputForm {
  constructor() {
    this.form = document.getElementById('factorial-form');
    this.numberInput = document.getElementById('factorial');

    this.form.addEventListener('submit', this.factorialHandler.bind(this));
  }

  factorialHandler(event) {
    event.preventDefault();

    const number = this.numberInput.value;

    if (!Validator.validate(number, Validator.REQUIRED) 
      || !Validator.validate(number, Validator.MAX_LENGTH, 100)
      || !Validator.validate(number, Validator.IS_TYPE, 'number'))
      {
        alert('Invalid input - either the number is to big or it is not a number');
        return;
      }

      const factorial = new Factorial(number);
      factorial.display();
  }
}

new InputForm();

我們創建了三個類來處理程序的三個不同的功能:

總結

兩種方法都是編寫代碼的有效方法。我喜歡在自己不同項目中嘗試最有效的方法。在很多情況下,甚至不可能如此清晰地分離這兩種範式。

希望這篇文章可以使你對不同的編程方法有一個基本的了解。


相關焦點

  • Facebook 開源 Skip,面向對象+函數式程式語言
    而通過靜態類型系統追蹤可變性,Skip 完成了這個目標,同時它也支持現代程式語言特徵,例如 trait、泛型與子類型。Skip 是一種通用程式語言,它跟蹤副作用,提供反應失效的緩存、ergonomics 和安全的並行化以及高效的 GC。Skip 是靜態類型的,它使用 LLVM 提前編譯,生成高度優化的可執行文件。
  • Python中的函數式編程
    (英語:functional programming)或稱函數程序設計,又稱泛函編程,是一種編程範型,它將電腦運算視為數學上的函數計算,並且避免使用程序狀態以及易變對象。(維基百科:函數式編程)所謂編程範式(Programming paradigm)是指編程風格、方法或模式,比如面向過程編程(C語言)、面向對象編程(C++)、面向函數式編程(Haskell),並不是說某種程式語言一定屬於某種範式,例如 Python 就是多範式程式語言。
  • 大數據入門:Scala函數式編程
    提到Scala,首先會提到的一個概念,就是函數式編程,這也是Scala語言區別與其他程式語言的典型特徵。Scala是一門多範式(multi-paradigm)的程式語言,設計初衷是要集成面向對象編程和函數式編程的各種特性。
  • Java如何支持函數式編程?
    在很長的一段時間裡,Java一直是面向對象的語言,一切皆對象,如果想要調用一個函數,函數必須屬於一個類或對象,然後在使用類或對象進行調用。但是在其它的程式語言中,如JS、C++,我們可以直接寫一個函數,然後在需要的時候進行調用,既可以說是面向對象編程,也可以說是函數式編程。從功能上來看,面向對象編程沒什麼不好的地方,但是從開發的角度來看,面向對象編程會多寫很多可能是重複的代碼行。
  • 函數式編程聖經
    上帝看到約翰·麥卡錫發明了表處理語言 Lisp,卻只用來學術研究,很是傷心,就把 Lisp 解釋器的秘密告訴了他的學生史蒂芬·羅素,史蒂芬·羅素將eval函數在IBM 704機器上實現後,函數式編程的大門第一次向人類打開了。
  • 面向對象編程會被拋棄嗎?這五大問題不容忽視
    面向對象編程的主要思想非常簡單:嘗試將一個功能強大的程序整體分解為功能同樣強大的多個部分。直到 1976 年,即面向對象的程序設計的概念問世十年之後,繼承性才被引入。 又過了十年,多態性才進入面向對象的編程。簡單來講,這意味著某種方法或對象可以用做其他方法或對象的模板。從某種意義上說,多態性是繼承性的泛化,因為並不是原始方法或對象的所有屬性都需要傳輸到新實體。相反,你還可以選擇重寫一些屬性。
  • 如何使用JavaScript -面向對象編程
    在實際開發中,對象是一個抽象的概念,可以將其簡單理解為:數據集或功能集。ECMAScript-262 把對象定義為:無序屬性的集合,其屬性可以包含基本值、對象或者函數。嚴格來講,這就相當於說對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都 映射到一個值。
  • 再談JavaScript面向對象編程
    言歸正傳,我們切入主題——Javascript的面向對象編程。要談Javascript的面向對象編程,我們第一步要做的事情就是忘記我們所學的面向對象編程。傳統C++或Java的面向對象思維來學習Javascript的面向對象會給你帶來不少困惑,讓我們先忘記我們所學的,從新開始學習這門特殊的面向對象編程。
  • 函數式編程,真香
    最開始接觸函數式編程的時候是在小米工作的時候,那個時候看老大以前寫的代碼各種 compose,然後一些 ramda 的一些工具函數,看著很吃力,然後極力吐槽函數式編程,現在回想起來,那個時候的自己真的是見識短淺,只想說,'真香'。
  • 函數式編程
    這個有點像Javascript的Prototype(參看Javascript的面向對象編程)尾遞歸優化:我們知道遞歸的害處,那就是如果遞歸很深的話,stack受不了,並會導致性能大幅度下降。所以,我們使用尾遞歸優化技術——每次遞歸時都會重用stack,這樣一來能夠提升性能,當然,這需要語言或編譯器的支持。Python就不支持。
  • 函數式編程,我心中的 C 位!
    最常見的三種範式分別是面向對象程序設計、命令式程序設計和函數式程序設計。這三種思想體系並無優劣之分,通常我們都需要選擇正確的工具來完成工作。大多數軟體工程師對於函數式編程的概念並不太熟悉。實際上,歷史上的第二個程式語言Lisp就屬於函數式範式。
  • 函數式編程看React Hooks(一)簡單React Hooks實現
    函數式編程看React Hooks(一)簡單React Hooks實現函數式編程看React Hooks(二)事件綁定副作用深度剖析
  • 使用JavaScript對象數組進行函數式編程
    我們著眼於使用map,filter和reduce來操縱對象數組,使用從函數式編程中借用的技術。數據操作是任何JavaScript應用程式中的常見任務。通過這些例子,我們將學習這些方法的強大程度,同時了解它們與函數式編程的關係。功能編程:好的部分函數式編程有許多概念超出了我們要實現的範圍。在本文中,我們將討論一些絕對的基礎知識。在整個示例中,我們將傾向於使用多個語句的單個表達式。
  • C風格的面向對象編程
    面向對象編程(OOP),最早是C++、java等面向對象語言的一個核心特點,之後發展成了一種編程思想。面向對象編程的主要特點是,把屬性(數據)與方法(函數)按照類型綁定,並按照類型之間的關係分層分模塊設計,通過基類指針和虛函數實現多態。
  • Swift 不是多範式函數式程式語言
    它通常的結構是帶有實例(對象)的類的層次結構,這些實例繼承了屬性和方法。當面對一個問題時,面向對象編程的問題是「需要什麼樣的對象一起工作才能解決此問題?」這兩種思維方式在流行的程式語言中極其普遍,並且可以很好地協同工作。自從第一批機器語言問世以來,過程式編程就一直伴隨著我們。即使是早期的自動織布機也是在過程式範式下工作。
  • 史上最全Python面向對象編程
    類和和類的實例(也稱對象)是面向對象的核心概念,是和面向過程編程、函數式編程的根本區別。面向對象編程和函數式編程(面向過程編程)都是程序設計的方法,不過稍有區別。面向過程編程:1. 導入各種外部庫2. 設計各種全局變量3. 寫一個函數完成某個功能4.
  • 一篇非常全的Python 面向對象編程
    .html面向對象編程和函數式編程(面向過程編程)都是程序設計的方法,不過稍有區別。寫一個main函數作為程序入口在多函數程序中,許多重要的數據被放置在全局數據區,這樣它們可以被所有的函數訪問。每個函數都可以具有它們自己的局部數據,將某些功能代碼封裝到函數中,日後便無需重複編寫,僅調用函數即可。從代碼的組織形式來看就是根據業務邏輯從上到下壘代碼 。面向對象編程:1.
  • java8的函數式編程解析
    其實在java8就已經有java的函數式編程寫法,只是難度較大,大家都習慣了對象式用法,但在其它語言中都有函數式的用法,如js,scala,函數式其實是抽象到極致的思想。什麼是函數式編程 函數式編程並不是Java新提出的概念,其與指令編程相比,強調函數的計算比指令的計算更重要;與過程化編程相比,其中函數的計算可以隨時調用。
  • 史上最全 Python 面向對象編程
    作者:浪子燕青    來自:http://www.langzi.fun/Python面向對象編程.html面向對象編程和函數式編程
  • R 的面向對象編程系統(S3、S4系統介紹)
    R 的面向對象編程R 語言中有四套面向對象編程系統:我們所熟悉的