如何在前端代碼中,應用面向對象的編程範式?

爲何要面向對象?

你須要知道的面向對象

面向對象並非針對一種特定的語言,而是一種編程範式。可是每種語言在設計之初,都會強烈地支持某種編程範式,好比面向對象的Java,而Javascript並非強烈地支持面向對象。html

何時須要面向對象?

任何一名開發人員,在編寫具體的代碼的時候,不該該爲了套用某種編程範式,而去編寫代碼和改造代碼。任何編寫方式的目的是:前端

  • 讓代碼邏輯清晰
  • 可讀性良好
  • 沒有冗餘代碼

前端編寫過程當中何時須要面向對象?

在個人平常工做中,最不想作的的就是兩點:es6

  • 複製粘貼代碼
  • 不一樣的代碼中具有相同的邏輯或者變量

由於這兩種方式,會讓代碼冗餘,並且不易維護。爲何?編程

由於相同的代碼,具有相同的邏輯,也就是具有相同的業務邏輯場景,若是場景一旦改變,你將會改變兩處代碼。

ok,到這裏,咱們來說一個具體的業務場景。redux

場景1: 前端須要顯示工人的工做完成狀態,若是已經完成了,前端提供一個查看詳情的入口,若是沒有完成,提供工人去完成任務的入口。後端傳遞過來顯示工人完成狀態的字段:user_done_status:0,表明未完成,1表明已完成。前端須要實現這樣一個表格:
工人名字 完成狀態 操做
小王 已完成 查看詳情
老王 未完成 去完成

階段一:實現最基本的功能

// status.js
// 1:須要一個狀態映射表,來實現第二列的功能
export const statusMap = new Map([
  [0, '未完成'],
  [1, '已完成']
]);
// 2: 須要一個動做映射表,來實現第三列的功能
export const actionMap = new Map([
  [0, '查看詳情'],
  [1, '去完成']  
]);
// 3: 須要一個狀態判讀函數,來實現第三列的功能
function isUserDone(status) {
  return +status === 1;
}

const actionMap = new Map([
  [status => isUserDone(status), userCanCheckResult],
  [status => !isUserDone(status), needUserToCompoleteWork]
]);

function handleClick() {
  for (let [done, action] of actionMap) {
    if (done()) {
      actionMap();
      return;
    }
  }
}

至於第三個爲何這麼寫,能夠看一下這篇文章後端

階段二:壞代碼的味道

上面的三段代碼單獨寫出來沒啥問題,看看下面的可能問題就出來,這至關於實現了三個函數,那麼須要在顯示在表格中就須要這樣寫:異步

import {
  statusMap,
  actionMap,
  getUserAction
} from './status.js'

.... ....
// 第二列
return (
  <span>
    {
      statusMap.get(status)
    }
  </span>
);
// 第三列
return (
  <span onClick={() => getUserAction(status)}>
    actionMap.get(status)
  </span>
);

這樣的寫法,看起來沒啥問題,可是可讀性是不好的,主要體如今兩點:函數

  • 三個函數都和status相關,可是展示形式上是割裂的
  • 每一個函數都須要傳遞一個status

可能有的人會說,這樣把上面的代碼單獨抽離出一個文件,也沒什麼問題,狀態也是比較集中的,嗯,這種說法也沒什麼問題,單獨提取一個文件,用做處理用的狀態,是一種常見的抽象方法。可是可能會遇到下面集中狀況,就會讓你很難受:post

  • 後端改了下字段,那麼你就須要在階段二中的第二列和第三列中傳入參數的地方修改對應的字段名字(估計想宰了rd吧)
  • 業務場景變化,工人的任務狀態,添加了其餘限制,好比任務的時間限制,任務有未開始、進行中、已過時三種狀態,只有當在任務進行中的時候,才能夠展現用戶的狀態,不然就展現未開始或者已過時,總結起來,須要下面的幾種狀態:this

    • 未開始
    • 已完成/未完成
    • 已過時

那麼顯然,你就須要修改代碼的邏輯,僅僅依靠一個statusMap就不能行了。固然這裏有人說了,那我把map編程一個函數:

const getUserStatus = (status, startTime, endTime) => {
  // ...do something
}

這樣是否是就能夠了,嗯,說的也沒什麼問題,那你須要去修改以前寫的全部代碼,傳入不一樣的參數,就算一開始你用的不是map而是函數,那麼你的代碼也須要再傳入兩個多餘的參數,start_time和end_time。

須要解決的痛點:

  • 展示形式的分離,須要一種集中的狀態處理
  • 須要傳入多個參數進行判斷,業務場景的變化或者字段的變化,都須要多處修改代碼

最開始遇到這來那個問題的時候,我想的是怎麼樣可以把全部的處理集中到一塊兒,天然而然就想到了面向對象,將用戶的狀態做爲一個對象,對象具有特定的屬性和對應的操做行爲。

Javascript中如何編寫面向對象的代碼?

先睹爲快,咱們看一下,上面的代碼在面向對象的寫法,直接使用es6的class

上面業務場景的面向對象的寫法

import moment from 'moment';

class UserStatus {
  constructor(props) {
    const keys = [
      user_done_status,
      start_time,
      end_time
    ] ;
    for (let key of keys) {
      this.[`_${key}] = (props || {})[key];
    }
  }

  static StatusMap = new Map([
    [0, '未完成'],
    [1, '已完成']
  ]);

  static TimeMap = newMap([
    [0, '未開始'],
    [1, '已過時']
  ]);

  get userDoneStatus () {
    return this._user_done_status;
  }

  get isInWorkingTime() {
    const now = new Date();
    return moment(now).isBetween(moment(this._start_time), moment(this._end_time));
  }

  get isWorkStart() {
    const now = new Date();
    return moment(now).isAfter(moment(now));
  }

  get userStatus () {
    if (this.isInWorkingTime) {
      return UserStatus.StatusMap.get(this.userDoneStatus);
    } else {
      return UserStatus.TimeMap.get(+this.isWorkStart);
    }
  }
  ... ...
  // 省略其餘的了
}

那麼寫好了上面的類,咱們應該在其餘地方怎麼引用呢?

// 第一步:直接講後端傳過來的信息,構造一個新的對象
const userInfo = new UserStatus(info);

// 第二步:直接調用對應的方法或者參數

return (
  <span>
    {
      userInfo.userStatus
    }
  </span>
);

之後不管業務場景如何改變這部分代碼都不須要從新改寫,只須要改寫對應的類的操做就能夠了。
這樣看了比較乾淨的是具體的view層代碼,就是簡單的html和對應的數據,沒有其餘操做。其實這就是如何消除代碼反作用的問題:將反作用隔離。當你把全部的反作用隔離以後,代碼看起來乾淨許多,你像redux-saga就是將對應的異步操做隔離出來。

ok,看了上面的類的寫法,咱們來看一下面向對象的寫法應該要怎麼寫:

面向對象

面向對象的三大特性

  • 封裝
  • 繼承
  • 多態
特性 特色 舉例
封裝 封裝就是對具體的屬性和實現細節進行隱藏,造成統一的的總體對外部提供對應的接口 上面的例子就是很好的解釋
繼承 繼承就是子類能夠繼承父類的屬性和行爲,也能夠重寫父類的行爲 好比工人有用戶狀態,老闆也有用戶狀態,他們均可以繼承UserStatus這一個基類
多態 同一個行爲在在不一樣的調用方式下,具有不一樣的行爲,依賴於抽象和重寫 好比工人和老闆都具有一個行爲那就是吃飯,工人吃的是饅頭,老闆吃的是海鮮,一樣是吃這個行爲,產生了不一樣的表現形式

封裝對象的幾個原則

在基本的面向對象中有幾個原則SOLID原則,可是這裏我不想詳細寫了,想說一下,我在封裝對象的時候會注重的幾個方面

  • 基類與具體數據無關,只封裝了特定的行爲和屬性,基類只注重抽象公共的部分
  • 類的行爲對擴展是開放的,可是對於修改是不開放的(開放封閉原則),像上面的寫法是存在風險的,由於生成的對象實例中的屬性能夠被隨意的修改,我加了_,就是防止這種行爲,可是最好的方式應該是使用get/set方法來對屬性限制操做;對於對象的屬性,必定要明確,由於js中一個是沒有類型的限制不要出現下面的寫法:
class Base {
  constructor(props) {
    for (let key of props) {
      this[key] = props[key];
    }
  }
}
  • 一個類只應該依賴於他繼承的類,不能依賴於其餘類,這樣能最大限度地減小耦合

注意的問題

注意⚠️在js中必定小當心this的使用,假設有一個初始類:
初始類:

class Base {
    constructor(props) {
        this._a = props.a;
    }

    status() {
        return this._a;
    }
}

避免下面的行爲:

// 方式1:
let { status } = new Base({a: 678});
status() // 會報錯

而應該使用下面的寫法:

//方式2:
let info = new Base({a: 678});
info.status(); //輸出正確

根本緣由就是this在做怪,第一種this指向了全局做用域。

最後也是最重要的

上面的面向對象主要解決了前文提到的兩個痛點,可是也不是全部的業務場景都適合面向對象,當你的代碼出現了一些壞味道(代碼容易、代碼分散不易處理),能夠考慮下面向對象,畢竟適合的纔是最好的

參考資料

面向對象封裝的五個原則)
五個原則比較形象的解釋

相關文章
相關標籤/搜索