讀書心得#重構JavaScript 3.1.2. 高階與低階(Hight-level and low-level)


3.1.2. 高階與低階(Hight-level and low-level)

高階與低階一詞在程式的世界中很常見,越高階的程式碼越抽象,越低階的程式碼越多實作。
第一次看到這句話的人可能已經開始神遊,不過現在讓哥帶你釐清如何辨別高階與低階。
註:「高階」也常以 「Client」 一詞做取代

低階程式

首先來看低階程式碼,越低階的程式碼越接近底層。一般來說我們都懂,越底層的程式碼越複雜,這是因為物件導向開發傾封裝複雜的過程,以便重複利用。
這個概念其實在開發過程中也常碰到,以 jQuery 的 ajax (非同步連線)作舉例,你知道 jQuery 在封裝 ajax 之前有多複雜嗎??
// If url is an object, simulate pre-1.5 signature
if (typeof url === "object") {
    options = url;
    url = undefined;
}

// Force options to be an object
options = options || {};

var // Create the final options object
s = jQuery.ajaxSetup({}, options),
// Callbacks context
callbackContext = s.context || s,
var // Create the final options object
s = jQuery.ajaxSetup({}, options),
// Callbacks context
callbackContext = s.context || s,
// Context for global events
// It's the callbackContext if one was provided in the options
// and if it's a DOM node or a jQuery collection
globalEventContext = callbackContext !== s &&
( callbackContext.nodeType || callbackContext instanceof jQuery ) ?
    jQuery(callbackContext) : jQuery.event,
// Deferreds
deferred = jQuery.Deferred(),
completeDeferred = jQuery.Callbacks("once memory"),
// Status-dependent callbacks
statusCode = s.statusCode || {},
// ifModified key
ifModifiedKey,
// Headers (they are sent all at once)
requestHeaders = {},

/**
 *  ajax 全長有 380 行程式碼,故省略。
 **/
這些程式碼,全部都是「實作」非同步連線功能的程式碼。如果每次使用非同步連線功能都要寫 380 行程式碼,那真的會瘋掉。
為了避免這種情況,
物件導向開發會將這些複雜的「實作」封裝起來,變成一個「抽象」的概念。
註:這裡的抽象化並不是指物件導向裡面的「介面」或是「抽象類別」,而是指電腦科學的「抽象化」概念。

抽象化是為了人可以使用「概念知識」來理解「程序知識」。

例如以下動作可被抽象化為「電話原理」:
以薄鐵片做成的振動板與固定板間充滿了碳粒子,將聲波變成電流。
當我們說話時,聲波振動振動板,振動板與固定板問的碳粒子因為受到擠壓,間隙縮小,接觸面積增大而使電阻變小,電流增強;反之,則電流減弱。
隨著我們說話聲音的大小,發話器便送出強弱不同的電流到通話對象的收話器。
> 抽象化的意義是,可以忽略非求解過程中必需的解。

有了抽象化,開發人員就直接使用「低階模組」抽象化後的方法,不必了解「低階模組」提供的功能或服務是如何實作的。

如 jQuery 把非同步連線功能的「實作程式碼」封裝成 jQuery.ajax 方法:

// 封裝實作後,只需要呼叫「抽象」的 ajax 方法即可使用非同步連線功能,舒服!
$.ajax({
    method: "POST",
    url: "register.php",
    data: {name: "John", location: "Boston"}
}).done(function (msg) {
    alert("Data Saved: " + msg);
});

高階程式

了解「抽象化」的概念後,接著可以來認識一下「高階」程式碼。
假設有需求要將商品資訊列印成 QRCode,並且要發送 QRCode 線上服務 API 才能產生並取回 QRCode 圖檔。
這時候就可以使用 jQuery.ajax 來達成。
首先我們可以建立一個 QRCode 物件,並且使用 jQuery.ajax 替我們發送 API 給 QRCode 線上服務:
QRCode = function() {
    this.qrcodeString = null;
};

QRCode.prototype.setQrcodeString = function(qrcodeString) {
    this.qrcodeString = qrcodeString;
};

// 封裝發送 QRCode 線上服務 API 的實作
QRCode.prototype.getQrcodePNG = function(callback) {

    // 依賴 jQuery.ajax
    $.ajax({
        method: "GET",
        url: "https:www.qr-code-generator.com/a/generator.php",
        data: {
            text: this.qrcodeString;
        }
    }).done(function (data) {
        callback(data);
    });
};
如範例可見,QRCode 類別依賴於 jQuery.ajax,這就已經產生一個高階與低階的關係:

現在 QRCode 只要透過 getQRCodePNG 這個抽象方法,就可以取得 QRCode 圖檔了!

更高階的程式

目前只有 QRCode 類別,還沒有辦法列印商品資料,所以我們要寫一些來讓程式可以印出商品資訊:

var qrcode = new QRCode();

var products = [ /* 省略 */];

products.forEach(function(product) {

    let qrcodeString = '';
    let productInfo = [];
    productInfo.push(product['id']);
    productInfo.push(product['name']);
    productInfo.push(product['price']);
    productInfo.push(product['createTime']);

    qrcodeString = productInfo.join(';');

    // 依賴於 QRCode 類別
    qrcode.setQrcodeString(qrcodeString);

    qrcode.getQrcodePNG(function(png) {
        let image = document.create('img', {
            src:png
        });
        document.body.append(image);
    });
});
如範例,這一段列印 QRCode 的程式碼依賴於 QRCode 物件,那麼這段程式碼也就成為高階程式,而 QRCode 物件也變成低階程式(被依賴)。
這種高階與低階的關係,會在物件導向的程式碼中環環相扣,最後造成了「越高階就越抽象,越低階就越多實作」的現象。
雖然現在已經可以列印圖片的 QRCode,但我們仍可將這段程式碼重構一下,讓程式碼之間的關係更清楚:
Product = function(data) {
    this.data = data;
    this.qrcodePrinter = null;
};

/**
 * 透過依賴注入的方式,把 QRCode 物件注入到 Product 裡面。
 * 
 * 供取得 QRCode 方法使用。
 * 
 * @param QRCode qrcodePrinter
 */
Product.prototype.setQrcodePrinter = function(qrcodePrinter) {
    this.qrcodePrinter = qrcodePrinter;
};

// 封裝取得產品 QRCode 的實作
Product.prototype.getQrcode = function(callback) {

    qrcodeString = '';
    productInfo = [];
    productInfo.push(this.data['id']);
    productInfo.push(this.data['name']);
    productInfo.push(this.data['price']);
    productInfo.push(this.data['createTime']);

    qrcodeString = productInfo.join(';');

    this.qrcodePrinter.setQrcodeString(qrcodeString);
    this.qrcodePrinter.getQrcodePNG(callback);
};

/*******************************
* 重構後的程式碼
*******************************/

var qrcode = new Qrcode();

var products = [ /* 省略 */];

products.forEach(function(product) {

    let productObj = new Product(product);

    // 依賴注入
    productObj.setQrcodePrinter(qrcode);

    productObj.getQrcode(function(png) {
       image = document.create('img', {
           src:png
       });
       document.body.append(image);
   });
});
比對一下重構前跟重構後的關係圖:

總結:

  • 測試案例也可以是摡括性的(高階)或更加深入細節的(低階)。
  • 在測試領域裡面,高階一詞大致對應到「整合測試」、「端對端測試」,而低階則對應到「單元測試」(Unit tests)。

參考資料:


留言

這個網誌中的熱門文章

Git Commit Message 這樣寫會更好,替專案引入規範與範例

Gitlab 合併請求 Merge Request 是什麼?

PHP OO 物件導向基礎教學