模式系統與最簡單的Node.js MVC Web Server設計

counterxing發表於2019-02-16

學了這麼久的設計模式,最近一直在看Node.js的設計模式,一直納悶為何會有模式這一類東西的存在,那麼模式究竟是什麼東西?後面在看了《面向模式的軟體架構》之後才慢慢知道有了一些系統的概念。

模式是什麼?

面對特定問題時,專家很少去尋找與既有解決方案截然不同的新方案,而通常會想起一個以前解決過的類似問題,並將其解決方案的精髓用於解決這個新問題。

從特定問題—解決方案中提煉出通用的因素便可得到模式:這些問題—解決方案通常是一系列熟悉的問題和解決方案,其中每對問題—解決方案都呈現出相同的模式。

Model-View-Controller模式

MVC模式大量用在現代軟體開發流程中,為何會有MVC模式的存在,來看這一個例子:開發帶人機介面的軟體。

使用者介面需求容易變化。例如,新增應用程式功能時,必須修改選單以便能夠訪問新功能,還可能需要針對特定客戶調整使用者介面。系統可能需要移植到另一個平臺,而該平臺採用的“外觀”標準完全不同。即便是升級到新的視窗系統版本,也可能需要修改程式碼。總之,如果系統的使用壽命很長,可能經常需要修改使用者介面。設計靈活的系統時,讓使用者介面與功能核心緊密地交織在一起將付出高昂的代價,且容易出錯。這樣做的後果是,可能需要開發和維護多個大不相同的軟體系統——每種使用者介面實現一個,且修改將涉及眾多不同的模組。總之,開發這種互動式軟體系統時,必須考慮如下兩個方面:

  • 應該能夠輕鬆地修改使用者介面,在執行階段就能完成;
  • 調整或移植使用者介面時,不應影響到應用程式功能核心的程式碼。

為解決這種問題,應將互動式應用程式劃分成三部分:處理、輸出和輸入。

  • 模型(model)元件封裝核心資料和功能,獨立於輸出表示方式和輸入行為。
  • 檢視(view)元件向使用者顯示資訊。檢視從模型那裡獲取它顯示的資訊,一個模型可以 有多個檢視。
  • 每個檢視都有相關聯的控制器(controller)元件。控制器接受輸入,通常是表示滑鼠移動、滑鼠按鈕啟用或鍵盤輸入的事件。事件被轉換為服務請求,而服務請求要麼被髮送給模型,要麼被髮送給檢視。使用者只通過控制器與系統互動。

通過將模型與檢視和控制器元件分開,讓同一個模型可以有多個檢視。如果使用者通過一個檢視的控制器修改了模型,這種變更應在依賴相關資料的其他所有檢視中反映出來。為此,每當模型的資料發生變化時,它都會通知所有檢視,而檢視將從模型那裡檢索新資料,並更新顯示的資訊。這種解決方案確保了修改應用程式的一個子系統時不會嚴重影響其他子系統。例如,可將非圖形使用者介面改成圖形使用者介面而無需修改模型子系統,還可支援新的輸入裝置而不影響資訊的顯示和功能核心。所有軟體版本都可依賴同一個模型子系統,該子系統獨立於“外觀”。

模式系統與最簡單的Node.js MVC Web Server設計

用Model-View-Controller模式實現一個鑑權服務

我們從下圖所示的結構開始分析:

模式系統與最簡單的Node.js MVC Web Server設計

上圖顯示了Model-View-Controller模式的典型示例;它描述了一個簡單的鑑權服務的結構。AuthController接受來自客戶端的輸入,從請求中提取登入資訊,並執行一些初步驗證。之後AuthService檢查客戶端提供的憑證是否與儲存在資料庫中的資訊匹配;最後使用db模組執行一些特定的查詢來完成的,作為與資料庫通訊的一種手段。這三個元件連線在一起的方式將決定應用程式的可重用性,可測試性和可維護性。

在這裡:模型(Model)指的就是db模組,控制器(Controller)指的就是AuthControllerAuthService,而檢視則是前端的使用者介面,也就是HTML文件。

將這些元件連線在一起的最自然的方法是通過AuthService請求db模組,然後從AuthController請求AuthService

讓我們通過實際實現剛剛描述的系統來演示這一點。那麼我們來設計一個簡單的鑑權伺服器,它將有以下兩個HTTP API

  • POST '/ login':接收包含使用者名稱和密碼對進行身份驗證的JSON物件。 成功時,它會返回一個JSON Web Token(JWT),隨後的請求中使用它來驗證使用者的身份。

  • GET'/ checkToken':檢視使用者是否具有許可權。

對於這個例子,我們將使用幾種技術;這對我們來說並不陌生。我們使用express來實現Web APIlevelup來儲存使用者的資料。

db模組

我們先從底層開始構建應用程式;首先實現levelUp資料庫例項的模組。我們來建立一個名為lib/db.js的新檔案,其中包含以下內容:

const level = require('level');
const sublevel = require('level-sublevel');
module.exports = sublevel(
  level('example-db', {
    valueEncoding: 'json'
  })
);
複製程式碼

前面的模組是儲存在./example-db目錄中的LevelDB資料庫的連線,然後使用sublevel來修飾例項,通過這一模組實現了增刪查改資料庫。模組匯出的物件是資料庫物件本身。

authService模組

現在我們有了db單例,我們可以使用它來實現lib/authService.js模組,它負責查詢資料庫,根據使用者身份憑證檢視使用者是否具有許可權。 程式碼如下(只顯示相關部分):

"use strict";

const jwt = require('jwt-simple');
const bcrypt = require('bcrypt');

const db = require('./db');
const users = db.sublevel('users');

const tokenSecret = 'SHHH!';

exports.login = (username, password, callback) => {
  users.get(username, (err, user) => {
    if(err) return callback(err);
    
    bcrypt.compare(password, user.hash, (err, res) => {
      if(err) return callback(err);
      if(!res) return callback(new Error('Invalid password'));
      
      let token = jwt.encode({
        username: username,
        expire: Date.now() + (1000 * 60 * 60) //1 hour
      }, tokenSecret);
      
      callback(null, token);
    });
  });
};

exports.checkToken = (token, callback) => {
  let userData;
  try {
    //jwt.decode will throw if the token is invalid
    userData = jwt.decode(token, tokenSecret);
    if (userData.expire <= Date.now()) {
      throw new Error('Token expired');
    }
  } catch(err) {
    return process.nextTick(callback.bind(null, err));
  }
    
  users.get(userData.username, (err, user) => {
    if (err) return callback(err);
    callback(null, {username: userData.username});
  });
};

複製程式碼

authService模組實現login()服務,該服務負責查詢資料庫,檢查使用者名稱和密碼資訊,checkToken()服務接受token作為引數並驗證其有效性。

authController模組

繼續在應用程式的層次上,我們現在要看看lib/authController.js模組。這個模組負責處理HTTP請求,它本質上是Express路由的集合;該模組的程式碼如下:

"use strict";

const authService = require('./authService');

exports.login = (req, res, next) => {
  authService.login(req.body.username, req.body.password,
    (err, result) => {
      if (err) {
        return res.status(401).send({
          ok: false,
          error: 'Invalid username/password'
        });
      }
      res.status(200).send({ok: true, token: result});
    }
  );
};

exports.checkToken = (req, res, next) => {
  authService.checkToken(req.query.token,
    (err, result) => {
      if (err) {
        return res.status(401).send({
          ok: false,
          error: 'Token is invalid or expired'  
        });
      }
      res.status(200).send({ok: 'true', user: result});
    }
  );
};
複製程式碼

authController模組實現兩個Express路由:login()用於執行登入操作並返回相應的tokencheckToken()用於檢查token的有效性。這兩個路由委託他們的大部分邏輯到authService,所以他們唯一的工作是處理HTTP請求和響應。

app模組

最後,在應用程式的入口點,我們呼叫我們的controller。遵循約定,我們將把這個邏輯放在名為app.js的模組中,放在我們專案的根目錄下,如下所示:

"use strict";

const Express = require('express');
const bodyParser = require('body-parser');
const errorHandler = require('errorhandler');
const http = require('http');

const authController = require('./lib/authController');

let app = module.exports = new Express();
app.use(bodyParser.json());

app.post('/login', authController.login);
app.get('/checkToken', authController.checkToken);

app.use(errorHandler());
http.createServer(app).listen(3000, () => {
  console.log('Express server started');
});
複製程式碼

我們可以看到,我們的應用程式模組是非常基礎的。 它包含一個簡單的Express伺服器,它註冊了一些中介軟體和authController匯出的兩條路由。這也就是一個簡單的包含controllermodelWeb服務,新增好前端HTML頁面,也就實現了MVC架構的分離

模式的特徵

  • 模式闡述了在特定設計情形下反覆出現的問題,並提供瞭解決方案。
  • 模式記錄了已得到充分證明的既有設計經驗。
  • 模式描述了超越類、例項和元件的抽象。
  • 模式提供了一種通用語言,並讓大家對設計原則有一致的認識。
  • 模式是一種記錄軟體架構的手段。
  • 模式有助於建立具有指定特徵的軟體。
  • 模式有助於打造複雜而異質的軟體架構。
  • 模式有助於控制軟體的複雜度。

為什麼叫模式

每個模式都包含三部分:

  • 背景(Context) 問題出現的背景;
  • 問題(Problem) 該背景下反覆出現的問題;
  • 解決方案(Solution) 經過實踐檢驗的解決之道。

背景

背景描繪了問題發生的情形,讓原本平淡無奇的問題—解決方案更為豐滿。模式的背景可能非常籠統,如“開發帶人機介面的軟體”,也可能將具體的模式聯絡在一起,如“在模型、檢視和控制器之間實現變更傳播機制”。

問題

模式描述綱要的這部分闡述了給定背景下反覆出現的問題。它以籠統的問題陳述開始,闡述了問題的本質:必須解決的具體設計問題是什麼?例如,Model-View-Controller模式解決的是使用者介面頻繁變更的問題。模式表示解決問題時需要考慮的方方面面:

  • 解決方案必須滿足的需求,如程式之間的對等通訊必須高效;
  • 必須考慮的約束條件,如程式間通訊必須遵守特定協議;
  • 解決方案必須具備的特徵,如應該能夠輕鬆地修改軟體。

Model-View-Controller模式說明了兩種作用力:修改使用者介面應輕而易舉,且這種修改不應影響軟體的核心功能。

解決方案

模式的解決方案部分指出瞭如何解決反覆出現的問題,更準確地說是如何平衡相關的作用 力。在軟體架構中,這樣的解決方案包括兩個方面:

  • 每個模式都指定了特定的結構,即元素的空間配置。例如,Model-View-Controller模式的描述中有這樣一句話:“將互動式應用程式分成三部分——處理、輸出和輸入。”
  • 每個模式都說明了執行階段的行為。例如,在Model-View-Controller模式的“解決方案”部分有這樣一句話:“控制器接受輸入,這通常是表示滑鼠移動、滑鼠按鈕啟用或鍵盤輸入的事件。事件被轉換為服務請求,而服務請求要麼被髮送給模型,要麼被髮送給檢視。”

模式的型別

模式一般分為三類:

  • 架構模式:具體軟體架構的模板,描繪了應用程式的系統級結構特徵,並將影響子系統的架構。例如Model-View-Controller模式
  • 設計模式:是一種中型模式,規模比架構模式小,但通常獨立於程式語言和程式設計正規化。應用設計模式不會影響軟體系統的基本架構,但可能嚴重影響子系統的架構。例如:觀察者模式。
  • 成例:如何解決特定的設計問題。針對於特定的語言的模式。例如C++語言的Counted Body模式。

總結

模式提供了一種前途無量的方法,可用於開發具有指定特徵的軟體。它們記錄了既有的設計知識,有助於找到設計問題的妥善解決方案。模式的規模和抽象程度各異,涵蓋了眾多重要的軟體開發領域。模式彼此交織在一起,我們可以使用一個模式來改善另一個更大的模式,還可結合使用多個模式來解決複雜的問題。模式論述了軟體架構的一些重要方面,並給既有技術和方法提供了補充。模式可以和任何程式設計正規化結合使用,且幾乎可使用任何程式語言實現。

相關文章