天天看點

前端的設計模式系列-模版模式場景模版模式js 的模版模式代碼實作更多場景總

代碼也寫了幾年了,設計模式處于看了忘,忘了看的狀态,最近對設計模式有了點感覺,索性就再學習總結下吧。

大部分講設計模式的文章都是使用的

Java

C++

這樣的以類為基礎的靜态類型語言,作為前端開發者,

js

這門基于原型的動态語言,函數成為了一等公民,在實作一些設計模式上稍顯不同,甚至簡單到不像使用了設計模式,有時候也會産生些困惑。

下面按照「場景」-「設計模式定義」- 「代碼實作」- 「更多場景」-「總」的順序來總結一下,如有不當之處,歡迎交流讨論。

場景

(示例代碼來源于極客時間課程,React Hooks 核心原理與實戰)

平常開發中一定遇到過這樣的場景:發起異步請求,

loading

狀态顯示,擷取資料并顯示在界面上,如果遇到錯誤還會顯示錯誤狀态的相關展示。

為了友善運作,先寫一個

mock

資料的方法:

const list = {
  page: 1,
  per_page: 6,
  total: 12,
  total_pages: 2,
  data: [
    {
      id: 1,
      email: "[email protected]",
      first_name: "windliang",
      last_name: "windliang",
      avatar: "https://reqres.in/img/faces/1-image.jpg"
    },
    {
      id: 2,
      email: "[email protected]",
      first_name: "Janet",
      last_name: "Weaver",
      avatar: "https://reqres.in/img/faces/2-image.jpg"
    },
    {
      id: 3,
      email: "[email protected]",
      first_name: "Emma",
      last_name: "Wong",
      avatar: "https://reqres.in/img/faces/3-image.jpg"
    }
  ]
};
export const getDataMock = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(list);
    }, 2000);
  });
           

複制

然後是清單元件:

import React from "react";
import { getDataMock } from "./mock";

export default function UserList() {
  // 使用三個 state 分别儲存使用者清單,loading 狀态和錯誤狀态
  const [users, setUsers] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);

  // 定義擷取使用者的回調函數
  const fetchUsers = async () => {
    setLoading(true);
    try {
      const res = await getDataMock();
      // 請求成功後将使用者資料放入 state
      setUsers(res.data);
    } catch (err) {
      // 請求失敗将錯誤狀态放入 state
      setError(err);
    }
    setLoading(false);
  };

  return (
    <div className="user-list">
      <button onClick={fetchUsers} disabled={loading}>
        {loading ? "Loading..." : "Show Users"}
      </button>
      {error && <div style={{ color: "red" }}>Failed: {String(error)}</div>}
      <br />
      <ul>
        {users &&
          users.length > 0 &&
          users.map((user) => {
            return <li key={user.id}>{user.first_name}</li>;
          })}
      </ul>
    </div>
  );
}
           

複制

效果就是下邊的樣子:

前端的設計模式系列-模版模式場景模版模式js 的模版模式代碼實作更多場景總

事實上,可能會有很多元件都需要這個過程,

loading

-> 展示資料 ->

loading

消失、錯誤展示,每一個元件單獨維護這一套邏輯就太麻煩了,此時就可以用到模版模式了。

模版模式

看下 維基百科 給到的定義:

★The template method is a method in a superclass, usually an abstract superclass, and defines the skeleton of an operation in terms of a number of high-level steps. These steps are themselves implemented by additional helper methods in the same class as the template method.

★The helper methods may be either abstract methods, in which case subclasses are required to provide concrete implementations, or hook methods, which have empty bodies in the superclass. Subclasses can (but are not required to) customize the operation by overriding the hook methods. The intent of the template method is to define the overall structure of the operation, while allowing subclasses to refine, or redefine, certain steps.[2]

簡單來說,模版模式就是抽象父類提供一個骨架方法,裡邊會調用一些抽象方法或者空方法,抽象方法/空方法由子類自行去實作,可以看一下

UML

類圖。

前端的設計模式系列-模版模式場景模版模式js 的模版模式代碼實作更多場景總

image-20220210212704745

舉一個做飯的簡單例子,看一下代碼示例:

abstract class Cook {
    public abstract void prepareIngredients();
    public abstract void cooking();
    public void prepare() {
        System.out.println("準備幹淨鍋");
    }
    /* A template method : */
    public final void startCook() {
        prepare();
        prepareIngredients();
        cooking();
    }
}

class TomatoEgg extends Cook {
    @Override 
    public void prepareIngredients() { 
        System.out.println("拌雞蛋、切蕃茄");
    } 
    @Override 
    public void cooking() { 
        System.out.println("熱油,炒雞蛋,出鍋");
        System.out.println("少油,炒蕃茄,加鹽、加糖,加雞蛋炒");
        System.out.println("出鍋");
    }

}

class Potato extends Cook {
    @Override 
    public void prepareIngredients() { 
        System.out.println("切洋芋片、腌肉");
    } 
    @Override 
    public void cooking() { 
        System.out.println("熱油,炒洋芋片,出鍋");
        System.out.println("加油,蒜姜辣椒爆香,炒肉、加洋芋炒");
        System.out.println("加醬油、加鹽、加醬油上色");
        System.out.println("出鍋");
    }

}

public class Main {
    public static void main(String[] args) {
        Cook tomatoEgg = new TomatoEgg();
        tomatoEgg.startCook();
        Cook potato = new Potato();
        potato.startCook();

        System.out.println("開吃!");
    }
}
/*
準備幹淨鍋
拌雞蛋、切蕃茄
熱油,炒雞蛋,出鍋
少油,炒蕃茄,加鹽、加糖,加雞蛋炒
出鍋
準備幹淨鍋
切洋芋片、腌肉
熱油,炒洋芋片,出鍋
加油,蒜姜辣椒爆香,炒肉、加洋芋炒
加醬油、加鹽、加醬油上色
出鍋
開吃!
*/
           

複制

Cook

類提供骨架方法

startCook

,編寫了做飯的主要流程,其他抽象方法

prepareIngredients

cooking

下放給子類去實作自己獨有的邏輯。

讓我們用

js

來改寫一下:

const Cook = function () {};
Cook.prototype.prepare = function () {
    console.log("準備幹淨鍋");
};
Cook.prototype.prepareIngredients = function () {
    throw new Error("子類必須重寫 prepareIngredients 方法");
};
Cook.prototype.cooking = function () {
    throw new Error("子類必須重寫 cooking 方法");
};
Cook.prototype.startCook = function () {
    this.prepare();
    this.prepareIngredients();
    this.cooking();
};
const TomatoEgg = function () {};
TomatoEgg.prototype = new Cook();
TomatoEgg.prototype.prepareIngredients = function () {
    console.log("拌雞蛋、切蕃茄");
};
TomatoEgg.prototype.cooking = function () {
    console.log("熱油,炒雞蛋,出鍋");
    console.log("少油,炒蕃茄,加鹽、加糖,加雞蛋炒");
    console.log("出鍋");
};

const Potato = function () {};
Potato.prototype = new Cook();
Potato.prototype.prepareIngredients = function () {
    console.log("切洋芋片、腌肉");
};
Potato.prototype.cooking = function () {
    console.log("熱油,炒洋芋片,出鍋");
    console.log("加油,蒜姜辣椒爆香,炒肉、加洋芋炒");
    console.log("加醬油、加鹽、加醬油上色");
    console.log("出鍋");
};

const tomatoEgg = new TomatoEgg();
tomatoEgg.startCook();

const potato = new Potato();
potato.startCook();

console.log("開吃!");
           

複制

上邊是

js

照貓畫虎的去按照

java

的形式去實作模版方法,作為函數是一等公民的

js

,也許我們可以換一種方式。

js 的模版模式

模闆模式是一個方法中定義一個算法骨架,可以讓子類在不改變算法整體結構的情況下,重新定義算法中的某些步驟。

原始定義中通過抽象類繼承實作,但由于

js

并沒有抽象類,實作起來也有些繁瑣,也許我們可以通過組合的方式,将需要的方法以參數的形式傳給算法骨架。

const Cook = function ({ prepareIngredients, cooking }) {
    const prepare = function () {
        console.log("準備幹淨鍋");
    };
    const startCook = function () {
        prepare();
        prepareIngredients();
        cooking();
    };
    return {
        startCook,
    };
};

const tomatoEgg = Cook({
    prepareIngredients() {
        console.log("拌雞蛋、切蕃茄");
    },
    cooking() {
        console.log("熱油,炒雞蛋,出鍋");
        console.log("少油,炒蕃茄,加鹽、加糖,加雞蛋炒");
        console.log("出鍋");
    },
});
tomatoEgg.startCook();

const potato = Cook({
    prepareIngredients() {
        console.log("切洋芋片、腌肉");
    },
    cooking() {
        console.log("熱油,炒洋芋片,出鍋");
        console.log("加油,蒜姜辣椒爆香,炒肉、加洋芋炒");
        console.log("加醬油、加鹽、加醬油上色");
        console.log("出鍋");
    },
});
potato.startCook();

console.log("開吃!");
           

複制

通過組合的方式,代碼會變得更加清爽簡單,不需要再定義

TomatoEgg

類和

Potato

類,隻需要簡單的傳參。

js

實作的隻能是帶引号的模版方法了,一方面我們并沒有通過繼承去實作,另一方面

js

并沒有抽象類、抽象方法的功能,如果某些方法沒有實作,并不能在代碼編寫階段發現,到了運作階段才會收到

Error

代碼實作

回到開頭異步請求的例子,我們可以定義一個請求

Hook

,将

loaing

處理、資料傳回處理這些步驟封裝起來,外界隻需要傳遞請求的方法即可。

import { useState, useCallback } from "react";
export default (asyncFunction) => {
  // 設定三個異步邏輯相關的 state
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  // 定義一個 callback 用于執行異步邏輯
  const execute = useCallback(() => {
    // 請求開始時,設定 loading 為 true,清除已有資料和 error 狀态
    setLoading(true);
    setData(null);
    setError(null);
    return asyncFunction()
      .then((response) => {
        // 請求成功時,将資料寫進 state,設定 loading 為 false
        setData(response);
        setLoading(false);
      })
      .catch((error) => {
        // 請求失敗時,設定 loading 為 false,并設定錯誤狀态
        setError(error);
        setLoading(false);
      });
  }, [asyncFunction]);

  return { execute, loading, data, error };
};
           

複制

業務調用的地方使用上邊的

Hook

即可。

import React from "react";
import useAsync from "./useAsync";
import { getDataMock } from "./mock";

export default function UserList() {
  // 通過 useAsync 這個函數,隻需要提供異步邏輯的實作
  const { execute: fetchUsers, data: users, loading, error } = useAsync(
    async () => {
      const res = await getDataMock();
      return res.data;
    }
  );

  return (
    <div className="user-list">
      <button onClick={fetchUsers} disabled={loading}>
        {loading ? "Loading..." : "Show Users"}
      </button>
      {error && <div style={{ color: "red" }}>Failed: {String(error)}</div>}
      <br />
      <ul>
        {users &&
          users.length > 0 &&
          users.map((user) => {
            return <li key={user.id}>{user.first_name}</li>;
          })}
      </ul>
    </div>
  );
}
           

複制

完整代碼放到 Sandxox 上了,感興趣的同學也可以去運作下。

https://codesandbox.io/s/great-flower-o83v0?file=/src/list.js:0-786

更多場景

「模版方法」在架構中會更常見,比如我們平常寫的

vue

,它的内部定義了各個生命周期的執行順序,然後對我們開放了生命周期的鈎子,可以執行我們自己的操作。

<script>
  var vm = new Vue({
    el: '#app',
    data: {
      message: 'Vue的生命周期'
    },
    beforeCreate: function() {
    },
    created: function() {
    },
    beforeMount: function() {
    },
    mounted: function() {
    },
    beforeUpdate: function () {
    },
    updated: function () {
    },
    beforeDestroy: function () {
    },
    destroyed: function () {
    }
  })
</script>
           

複制

「模版方法」如果再說的寬泛一點,

ElementUI

dialog

也可以當作模版方法。

<el-dialog
  title="提示"
  :visible.sync="dialogVisible"
  width="30%"
  :before-close="handleClose">
  <span>這是一段資訊</span>
  <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="dialogVisible = false">确 定</el-button>
  </span>
</el-dialog>
           

複制

el-dialog

實作了

Dialog

的基本樣式和行為,并且通過

slot

以供擴充,讓我們實作自己個性的東西。

雖然在

js

中我們并不能真正實作模版模式,但模版模式的作用我們還是實作了,踐行了「開放關閉原則」:

  • 對擴充開放: 可以通過傳入不同的參數,實作不同的應用需求。
  • 對修改關閉: 模版方法通過閉包的形式,内部的屬性、方法外界并不能修改。

模版方法同樣提升了複用能力,我們可以把公共的部分提取到模版方法中,業務方就不需要自己再實作一次了。