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

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

模式是什麼?

面對特定問題時,專家不多去尋找與既有解決方案大相徑庭的新方案,而一般會想起一個之前解決過的相似問題,並將其解決方案的精髓用於解決這個新問題。前端

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

Model-View-Controller模式

MVC模式大量用在現代軟件開發流程中,爲什麼會有MVC模式的存在,來看這一個例子:開發帶人機界面的軟件。數據庫

用戶界面需求容易變化。例如,添加應用程序功能時,必須修改菜單以便可以訪問新功能,還可能須要針對特定客戶調整用戶界面。系統可能須要移植到另外一個平臺,而該平臺採用的「外觀」標準徹底不一樣。即使是升級到新的窗口系統版本,也可能須要修改代碼。總之,若是系統的使用壽命很長,可能常常須要修改用戶界面。設計靈活的系統時,讓用戶界面與功能核心緊密地交織在一塊兒將付出高昂的代價,且容易出錯。這樣作的後果是,可能須要開發和維護多個大不相同的軟件系統——每種用戶界面實現一個,且修改將涉及衆多不一樣的模塊。總之,開發這種交互式軟件系統時,必須考慮以下兩個方面:express

  • 應該可以輕鬆地修改用戶界面,在運行階段就能完成;
  • 調整或移植用戶界面時,不該影響到應用程序功能核心的代碼。

爲解決這種問題,應將交互式應用程序劃分紅三部分:處理、輸出和輸入。npm

  • 模型(model)組件封裝核心數據和功能,獨立於輸出表示方式和輸入行爲。
  • 視圖(view)組件向用戶顯示信息。視圖從模型那裏獲取它顯示的信息,一個模型能夠 有多個視圖。
  • 每一個視圖都有相關聯的控制器(controller)組件。控制器接受輸入,一般是表示鼠標移動、鼠標按鈕激活或鍵盤輸入的事件。事件被轉換爲服務請求,而服務請求要麼被髮送給模型,要麼被髮送給視圖。用戶只經過控制器與系統交互。

經過將模型與視圖和控制器組件分開,讓同一個模型能夠有多個視圖。若是用戶經過一個視圖的控制器修改了模型,這種變動應在依賴相關數據的其餘全部視圖中反映出來。爲此,每當模型的數據發生變化時,它都會通知全部視圖,而視圖將從模型那裏檢索新數據,並更新顯示的信息。這種解決方案確保了修改應用程序的一個子系統時不會嚴重影響其餘子系統。例如,可將非圖形用戶界面改爲圖形用戶界面而無需修改模型子系統,還可支持新的輸入設備而不影響信息的顯示和功能核心。全部軟件版本均可依賴同一個模型子系統,該子系統獨立於「外觀」。編程

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

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

上圖顯示了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模式。

總結

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

相關文章
相關標籤/搜索