設計模式 – 以 JavaScript 為例
設計法則
良好的模組化是提高程式品質的好方法,在設計函數模組時,記得遵循一些原則。像是:
- KISS 原則: 能簡單就不要複雜的
- KISS: Keep It Simple and Stupid
 
 - SRP 原則: 單一的函數只做一個功能。
- SRP : Single Responsibility Principle
 
 - 提高內聚力,降低附著力。
 - 變數命名應有意義,不要只是為了求短。
 - 盡量少用全域變數。
 - 程式風格要盡可能統一。
 - 恰當的註解,並保持註解與程式的一致性。
 - 避免使用奇技淫巧。
 
另外、物件導向也有一些設計準則可以依循
- 慎用繼承,可改用組合 (Composition) 代替
 - LSP 原則: 子型態必須可以被父型態取代
- LSP : Liskove Substitution Principle
 - 正例: Shape <= Circle, Rectangle
 - 反例: Rectangle <= Square
 
 - PLK 最小知識原則: 人命令狗,但不要命令狗的腿
- PLK: Principle of Least Knowledge
 - 狗的腿由狗自己去命令操控
 
 - 好萊塢原則: 低階元件不要呼叫高階元件
- Hollywood Principle
 - 而是像驅動程式一樣,先註冊後由高階元件呼叫低階元件。
 
 - OCP 開放封閉原則: 模組必須容易被改變,但卻不需要大改
- OCP: Open Closed Principle
 - 使用依賴性注射、多型之類的技巧達成
 
 - DIP 依賴反向原則:高階模組不應依賴低階模組,兩者必須依賴抽象層運作。
- DIP : Dependency Inversion Principle
 
 - IoC 控制反轉原則: 使用依賴注射 DI 完成。
- IoC: Inversion of Control
 - DI : Dependence Injection (以下列出三種 DI)
 - 建構子注入 (Constructor Injection)
 
- 設定屬性注入 (Setter Injection)
 
- 介面參數注入 (Interface Injection)
 
 - SOI 介面分離原則: 採用 interface 避免多重繼承
- SOI: Separation of Interface
 
 - 迪米特法則: 盡量不要與無關的物件產生關係。
- 對被呼叫函式傳回的物件,不該再呼叫該物件內的方法
 - 像是 ctxt.getOptions().getScratchDir().getAbsolutePath() 就違反迪米特法則。
 - 但是 jQuery 那類傳回自身的鏈式語法,並沒有違反迪米特法則。
 
 
設計模式列表
關於經典書籍 Design Patterns - Elements of Reusable Object-Oriented Software 中所描述的那些設計模式,筆者就不多寫了,您可以閱讀下列書籍了解那些經典的物件導向模式。
進階閱讀:
JavaScript 的設計模式
在本文中,我們將就 JavaScript (JS) 常見的一些《設計模式》進行解說,這些模式都是對 JS 特別實用的模式。
我們所選出的 JS 專屬模式如下:
- Callback《回呼》 : o.call(…, f)
- Promise : 一種特殊的 callback 方式
 - async / await : 將 callback 轉為類似傳統的循序式寫法。
 
 - Chaining《鏈式語法》 : o.f().g().h()
- MyQuery : 類似 jQuery
 - BDD : 創造 expect().not.to.contain(….) 這類的語法
 
 - Pub/Sub《發布/訂閱》: pub(channel), sub(channel).on(event)
- Message Queue
 
 - Middleware《中間件》 : Framework.use(middleware)
- Koa 的中間件設計
 
 - MVC《三層式結構》
- MVC: Model-View-Controller
 
 
Callback《回呼》
o.call(…, f)
範例: Callback 檔案讀取
const fs = require('fs')
fs.readFile(process.argv[2], 'utf8', function (err, text) {
  if (err) { 
    console.log('Error:', err)
    return
  }
  console.log('text=', text)
})
執行結果
$ node io abc.txt
Error: { [Error: ENOENT: no such file or directory, open 'D:\course\se107a\example\designPattern\00-callback\abc.txt']
  errno: -4058,
  code: 'ENOENT',
  syscall: 'open',
  path:
   'D:\\course\\se107a\\example\\designPattern\\00-callback\\abc.txt' }
$ node io io.js
text= const fs = require('fs')
fs.readFile(process.argv[2], 'utf8', function (err, text) {
  if (err) {
    console.log('Error:', err)
    return
  }
  console.log('text=', text)
})
範例: Promise 網頁抓取
const W = (module.exports = {})
const http = require('http')
W.get = function (url) {
  return new Promise(function (resolve, reject) {
    http
      .get(url, function (res) {
        let chunks = []
        console.log('Got response: ' + res.statusCode)
        res.on('data', function (chunk) {
          chunks.push(chunk)
        })
        res.on('end', function () {
          resolve(chunks.join())
        })
      })
      .on('error', function (e) {
        reject(e)
      })
  })
}
主程式 1 : 使用 promise.then 的方式回呼
const web = require('./web')
web
  .get(process.argv[2])
  .then(function (text) {
    console.log(text)
  })
  .catch(function (error) {
    console.log('error:' + error)
  })
執行結果:
$ node mainThen http://railway.hinet.net/Foreign/TW/index.html
Got response: 200
<!DOCTYPE html>
...
    <title>交通部臺灣鐵路管理局-網路訂票系統
...
</body>
</html>
主程式 2 : 使用 async/await 的漂亮語法
const web = require('./web')
async function main (url) {
  try {
    let text = await web.get(url)
    console.log(text)
  } catch (error) {
    console.log('error:' + error)
  }
}
main(process.argv[2])
執行結果:
$ node mainAwait http://railway.hinet.net/Foreign/TW/index.html
Got response: 200
<!DOCTYPE html>
...
    <title>交通部臺灣鐵路管理局-網路訂票系統
...
</body>
</html>
Chaining《鏈式語法》
- o.f().g().h()
 
MyQuery – 簡化版 jQuery 的鏈式語法
myquery.js 模組
const $ = function (filter) {
  let q = new Q()
  return q.get(filter)
}
class Q {
  constructor () {
    this.filter = ''
    this.nodes = window.document
  }
  get (filter) {
    this.filter += ' ' + filter
    this.nodes = document.querySelectorAll(this.filter)
    return this
  }
  each (f) {
    for (let node of this.nodes) {
      f(node)
    }
    return this
  }
}
主網頁: myquery.html
<html>
  <body>
<ol>
  <li>item1.1</li>
  <li>item1.2</li>
</ol>
<ol>
  <li>item2.1</li>
  <li>item2.2</li>
</ol>
<script src="myquery.js"></script>
<script>
    $('ol').get('li').each((x) => {
      x.innerHTML = 'hi'
      x.style.color = 'red'
      console.log('x=', x)
    })
</script>
</body>
</html>
開啟 myquery.html 會看到下列情況:

這是 myquery.html 中下列程式的作用:
$('ol').get('li').each((x) => {
  x.innerHTML = 'hi'
  x.style.color = 'red'
  console.log('x=', x)
})
案例: BDD 測試框架的鏈式語法
const O = {}
const expect = function (obj) {
  O.obj = obj
  return O
}
O.to = O
O.be = O
O.a = O
O.include = function (child) { return O.obj.indexOf(child) >= 0 }
O.html = function () { return O.obj.indexOf('<html>') >= 0 }
console.log(expect('<html><body><body></html>').to.be.a.html())
console.log(expect('<html><body>hello!<body></html>').to.include('hello'))
console.log(expect('<html><body>hello!<body></html>').to.include('world'))
執行結果
$ node bdd.js
true
true
false
Pub/Sub《發布/訂閱》
- pub(channel), sub(channel).on(event)
 
案例: 以事件驅動創建 Pub/Sub Message Queue
var events = require('events')
class MQ {
  constructor () {
    this.emitter = new events.EventEmitter()
  }
  pub () {
    this.emitter.emit(...arguments)
  }
  sub (event, f) {
    this.emitter.on(event, f)
  }
}
var mq = new MQ()
mq.sub('mbox1', (msg) => { console.log('mbox1:', msg) })
console.log('20181008')
mq.pub('mbox1', 'You have a meeting today')
console.log('20181009')
mq.pub('mbox1', 'Nothing important today')
執行結果:
$ node pubsub.js
20181008
mbox1: You have a meeting today
20181009
mbox1: Nothing important today
Middleware《中間件》
- Framework.use(middleware)
 
var app = {
  ctx: { port: 3000, name: 'myapp' },
  jobList: []
}
app.use = function (job) {
  app.jobList.push(job)
}
app.run = function () {
  for (let job of app.jobList) {
    job(app.ctx) // 1. showCtx(app.ctx)  2. hello(app.ctx)
  }
}
function showCtx(ctx) {
  console.log('%j', ctx)
}
function hello(ctx) {
  console.log('hello!')
}
app.use(showCtx)
app.use(hello)
app.run()
執行結果:
$ node myKoa
{"port":3000,"name":"myapp"}
hello!
範例: Koa 原始碼的中間件設計
  // ...
  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }
  // ...
  callback() {
    const fn = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
其中的關鍵用到 koa-compose
原始碼: https://github.com/koajs/compose/blob/master/index.js
function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }
  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */
  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
使用範例
MVC《三層式結構》
- MVC: Model-View-Controller
 
範例: 經典網誌系統 BlogMVC
範例請參考 BlogMvc