###### tags: `javascript` `design patterns`
# JavaScript Design Patterns
## 前言: Concepts behind "javascript is a prototype-based language"
- 原型程式設計 - [wiki](https://zh.wikipedia.org/wiki/%E5%8E%9F%E5%9E%8B%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88)
- 深入了解物件模型 - [MDN](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Guide/Details_of_the_Object_Model)
- 該來理解 JavaScript 的原型鍊了 - [techbridge](https://blog.techbridge.cc/2017/04/22/javascript-prototype/)
- JavaScript 中的「繼承」 - [MDN](https://developer.mozilla.org/zh-TW/docs/Learn/JavaScript/Objects/Inheritance)
- What does it mean that Javascript is a prototype-based language? - [stackoverflow](https://stackoverflow.com/questions/186244/what-does-it-mean-that-javascript-is-a-prototype-based-language)
- 世界上有哪些 [prototype-based languages](https://en.wikipedia.org/wiki/List_of_programming_languages_by_type#Object-oriented_prototype-based_languages)?
- 世界上有哪些 [class-based languages](https://en.wikipedia.org/wiki/List_of_programming_languages_by_type#Object-oriented_class-based_languages)?
## Quick review "inheritance" in JavaScript
**實例**(instance)、**類別**(class),其實是 **class-based 的物件導向語言** (e.g. Java 和 C++) 所使用且有明確定義的術語。
**但 javascript (JS) 是 prototype-based 的語言,並沒有這些區別。** 在 JS 的世界裡,**所有物件都是實例**。
又**建構子**(constructor) 也是 class-based 的物件導向語言中,指用來**創建該 class 實例的特殊方法**;**繼承** (inheritance) 則是**物件導向程式設計**裡最重要的三大特色之一。
在 JS 的世界,**函式** (function) 本身也是一種物件,且我們**可以直接將函式充當建構子**、定義物件的屬性與初始值;而 JS 要達成繼承的目的,得靠所謂的**原型鍊** (prototype chain) 機制來實現
:::info
:information_source: 為何 programmer 想要「繼承」的機制?
繼承能夠幫助我們達到更好的「代碼重用」和「可擴展性」的需求,為物件導向程式設計 (OOP) 的特徵之一
而物件導向程式設計的總目標就是:提高軟體的重用性、靈活性和擴充性
:::
### Inheritance by prototype chain
要討論繼承的概念,首先要有個 **bass class**。
下例中的 `Person` 函式物件就扮演著 bass class 的角色。而此 function 被直接當作 `Person` 的建構子來用。函式內的 `this` 指實例化後的物件本身 (i.e. 12 行的 `p`):
```javascript=
function Person (first, last, age, gender, interests) {
this.name = {
first,
last
}
this.age = age
this.gender = gender
this.interests = interests
}
const p = new Person(
'Max', 'Cian', 30, 'male', ['Javascript', 'Python', 'Golang'])
console.log(p)
// Person {
// name: { first: 'Max', last: 'Cian' },
// age: 30,
// gender: 'male',
// interests: [ 'Javascript', 'Python', 'Golang' ] }
```
> 這就是後述的第一個 pattern:**Constructor Pattern**
另外,我們在 class-based language 裡還可能替 class 添加 method。但如何在 `Person` 中添加**方法**呢?
此時,需要使用 Function Object 中的 `prototype`,對它做擴充:
```javascript=21
Person.prototype.greeting = function () {
console.log(`Hi! I'm ${this.name.first}.`)
}
p.greeting()
// Hi! I'm Max.
```
:::info
:information_source: 為何不在建構子裡寫 method?
若直接寫 function 在建構子裡,未來每一個 person,都會有自己一份獨一無二的 function。
但寫 function 有一個目的叫 code reuse,不只是 programmer 少寫 code,也希望此 function 定義完後在記憶體內也能共享。
利用此法擴充的 methods 才不會因創建多個 Person 而重複佔用記憶體。
:::
接下來,**我們有個 `Teacher`,想要繼承自 `Person`**:
:::success
:green_apple: 所以**繼承**的目的是?
我們希望 Teacher 也跟 Person 一樣擁有 name, age, gender 等屬性(但我不想重複寫 code)、且我還想基於 Person,新增 subject 的屬性給 Teacher。
:::
```javascript=27
function Teacher (first, last, age, gender, interests, subject) {
Person.call(this, first, last, age, gender, interests) // Key 1
this.subject = subject
}
Teacher.prototype = Object.create(Person.prototype) // Key 2
const teacher = new Teacher(
'Max', 'Cian', 30, 'male', ['Javascript', 'Python', 'Golang'],
'Object-Oriented Programming of Javascript')
console.log(`teacher's name: ${teacher.name.first} ${teacher.name.last}`)
console.log(`teacher's age: ${teacher.age}`)
console.log(`teacher's gender: ${teacher.gender}`)
console.log(`teacher's interests: ${teacher.interests}`)
console.log(`teacher's subject: ${teacher.subject}`)
// teacher's name: Max Cian
// teacher's age: 30
// teacher's gender: male
// teacher's interests: Javascript,Python,Golang
// teacher's subject: Object-Oriented Programming of Javascript
teacher.greeting() // Key 3
// Hi! I'm Max.
```
**Key 1**
- 透過 Function Object 的 `call()`,重新呼叫該函數
- 然後將 Teacher 的 `this` 放在第一個參數傳給 Person.call(),這樣呼叫 Person 建構子時內部的 this 就是 teacher 的 this
- 接著依序將 Person 建構子所需的參數 first, last, age, gender, interests 傳進去做初始化
- 此時 Teacher 就擁有 Person 所有屬性以及自己才有的 subject
**Key 2**
- 此步就是利用**原型鍊**機制,使用 Object.create(),創建一個 Person.prototype 的物件,並只派給 Teacher.prototype,就完成繼承了
**Key 3**
- 完成繼承之後我們呼叫 teacher.greeting()
- 此時會因為在 Teacher 中找不到,而去找 Teacher.prototype 裡有沒有 greeting,然後就在 Person.prototype 中找到 greeting 了
最後,如果我們想要更改 greeting(),Teacher 有自己的打招呼方式,一樣透過擴充 Teacher.prototype 來達成:
```javascript=51
Teacher.prototype.greeting = function () {
let prefix
if (this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
prefix = 'Mr.'
} else if (this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
prefix = 'Ms.'
} else {
prefix = 'Mx.'
}
console.log(`Hello, My name is ${prefix} ${this.name.last}, and I teach ${this.subject}.`)
}
teacher.greeting()
// Hello, My name is Mr. Cian, and I teach Object-Oriented Programming of Javascript.
```
## Gentle introduction of JavaScript Design Patterns
> 本週分配的文章是 [Top 10 JavaScript Patterns Every Developer Likes](https://dev.to/shijiezhou/top-10-javascript-patterns-every-developers-like-168p),但這篇其實是不老實的中國人求職前製作的履歷門面文章,文中談論的 10 個 patterns 皆**斷章取義**自著作-[Learning JavaScript Design Patterns](https://addyosmani.com/resources/essentialjsdesignpatterns/book/), a O'Reilly book by Addy Osmani
> 應直接看原作,看不懂的話拿關鍵字餵 google
### 1. The Constructor Pattern
- 見前節所述,JS 的世界裡直接使用函式來建構物件實體
### 2. The Module Pattern
- [Day06 - 常見的 JS 模組模式 (Pattern)](https://ithelp.ithome.com.tw/m/articles/10215129)
- JS 並不像其他程式語言有內建命名空間 (namespace) 的語法,但你還是可以透過結構化的方式組織你的程式碼達到類似的效果
- 亦可參考 [namespacing patterns](https://addyosmani.com/resources/essentialjsdesignpatterns/book/#detailnamespacing)
- 利用 ***closure*** 的特性,將**函式**和**變數**限制在一個範疇內存取與使用,且也製作出一個 namespace 的概念
Example
```javascript=
const CarController = (() => {
let carSpeed = 0
return {
speedUp: (speed) => { carSpeed += speed },
slowDown: (speed) => { carSpeed -= speed },
checkSpeed: () => carSpeed
}
})()
console.log(CarController.checkSpeed())
CarController.speedUp(100)
console.log(CarController.checkSpeed())
CarController.slowDown(33)
console.log(CarController.checkSpeed())
```
### 3. The Revealing Module Pattern
- [Day06 - 常見的 JS 模組模式 (Pattern)](https://ithelp.ithome.com.tw/m/articles/10215129)
- Module Pattern 的進化改良版
- 基於 module pattern,內部可能有些複雜了,為了提高可讀性,選擇在最後的時候才決定哪些函式是應該被揭露出來
- 其實有點遵循 [Interface segregation principle](https://en.wikipedia.org/wiki/Interface_segregation_principle) 的感覺了
Example
```javascript=
const CarController = (() => {
let carSpeed = 0
function NitrogenAcceleration (speed) {
carSpeed += speed
}
function LockFrontWheel (speed) {
carSpeed -= speed
}
function LockReerWheel (speed) {
carSpeed -= speed
}
function LockWheels (speed) {
LockFrontWheel(speed * 0.6)
LockReerWheel(speed * 0.4)
}
return {
speedUp: NitrogenAcceleration,
slowDown: LockWheels,
checkSpeed: () => carSpeed
}
})()
console.log(CarController.checkSpeed())
CarController.speedUp(100)
console.log(CarController.checkSpeed())
CarController.slowDown(33)
console.log(CarController.checkSpeed())
```
### 4. The Singleton Pattern
- [GoF Design Patterns](https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA)) 中的一種
- 我們總是有需要延遲實例化全域物件的需求,原因可能是某些參數得在 runtime 時才能得知
- singleton pattern 指的就是利用程式語言的特性製作出一個經過實例化之後、就永遠是唯一存在的實例
Conventional construct an "instance"
```javascript=
function Car (model, year, miles) {
this.model = model
this.year = year
this.miles = miles
}
let car = new Car('M4', '2019', '1000')
console.log(car) // Car { model: 'M4', year: '2019', miles: '1000' }
car = new Car('M4', '2030', '12345')
console.log(car) // Car { model: 'M4', year: '2030', miles: '12345' }
```
Singleton "instance"
```javascript=
const SingletonCar = (() => {
let car
const init = (model, year, miles) => new Car(model, year, miles)
return {
getCar: (model, year, miles) => {
if (car) {
console.log('Car was created, return the old')
return car
}
console.log('Lazy create a new car')
car = init(model, year, miles)
return car
}
}
})()
let car = SingletonCar.getCar('M4', '2019', '1000')
console.log(car) // Car { model: 'M4', year: '2019', miles: '1000' }
car = SingletonCar.getCar('M4', '2030', '12345')
console.log(car) // Car { model: 'M4', year: '2019', miles: '1000' }
```
### 5. The Observer Pattern
- [GoF Design Patterns](https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA)) 中的一種
- aka. **Pub**lication/**Sub**scription **Pattern**
- 實作一個 Subject 物件,它提供其他物件「訂閱」收到特定事件時,要「通知」它們
- 可參考 [JavaScript Observer Pattern](https://www.dofactory.com/javascript/design-patterns/observer) 的解釋與實作
- 此模式能夠**降低程式碼之間的耦合性**、同時也讓程式碼更加物件導向
```javascript=
function CelloNews () {
this.handlers = [] // observers
}
CelloNews.prototype = {
subscribe: function (fn) {
this.handlers.push(fn)
},
unsubscribe: function (fn) {
this.handlers = this.handlers.filter(item => item !== fn)
},
notify: function (args) {
this.handlers.forEach(fn => fn(args))
}
}
function MaxCian (args) {
console.log(`[MaxCian] got message: ${args}`)
}
function JohnLee (args) {
console.log(`[JohnLee] got message: ${args}`)
}
function HHTu (args) {
console.log(`[HHTu] got message: ${args}`)
}
const cello = new CelloNews()
cello.subscribe(MaxCian)
cello.subscribe(JohnLee)
cello.notify('台塑家教專案上課啦')
// [MaxCian] got message: 台塑家教專案上課啦
// [JohnLee] got message: 台塑家教專案上課啦
console.log()
cello.unsubscribe(JohnLee)
cello.notify('吃飯啦')
// [MaxCian] got message: 吃飯啦
console.log()
cello.subscribe(HHTu)
cello.notify('爬山啦')
// [MaxCian] got message: 爬山啦
// [HHTu] got message: 爬山啦
```
### 6. The Mediator Pattern
- [GoF Design Patterns](https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA)) 中的一種
- 此模式指在需要交互通信、彼此依賴彼此的狀態的物件之間,封裝一個「中介者」,藉此減少物件之間的依賴、**降低耦合性**
- 例如飛機降落或起飛,是彼此依賴的關係
- 你可以選擇讓機師直接溝通來協調跑到,但這就是物件之間有高度耦合的情況
- 或是由「塔台」作為「中介者」,來掌握所有飛機起降的時機
- 可參考 [JavaScript Mediator Pattern](https://www.dofactory.com/javascript/design-patterns/mediator) 的解釋與實作
```javascript=
function Participant (name) {
this.name = name
this.chatroom = null
}
Participant.prototype = {
send: function (message, to) {
this.chatroom.send(message, this, to)
},
receive: function (message, from) {
console.log(`[${from.name}]->[${this.name}]: ${message}`)
}
}
function Chatroom () {
const participants = {}
return {
register: function (participant) {
participants[participant.name] = participant
participant.chatroom = this
},
send: function (message, from, to) {
if (to) { // single message
to.receive(message, from)
} else { // broadcast message
for (const key in participants) {
if (participants[key] !== from) {
participants[key].receive(message, from)
}
}
}
}
}
}
const chatroom = new Chatroom()
const maxcian = new Participant('Max Cian')
const johnlee = new Participant('John Lee')
const hhtu = new Participant('HH Tu')
chatroom.register(maxcian)
chatroom.register(johnlee)
chatroom.register(hhtu)
maxcian.send('要不要吃肥宅快樂餐', johnlee)
// [Max Cian]->[John Lee]: 要不要吃肥宅快樂餐
console.log()
johnlee.send('我就怕被罵咩', hhtu)
// [John Lee]->[HH Tu]: 我就怕被罵咩
console.log()
hhtu.send('開會囉')
// [HH Tu]->[Max Cian]: 開會囉
// [HH Tu]->[John Lee]: 開會囉
```
### 7. The Prototype Pattern
- [GoF Design Patterns](https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA)) 中的一種
- 關鍵觀念為:創建一個物件,不是從無開始實例化;而是從已存在的實體複製而來
- JS 作為 prototype based 的語言,當使用 `new` 與 `Object.prototype` 等語法時,底層其實就是根據此設計理念來實現
- [The Prototype Pattern in JavaScript](https://medium.com/better-programming/the-prototype-pattern-in-javascript-bfe9ff433e6c): 此篇展示了 JS 底層哪些語法使用了 prototype pattern
- [JavaScript Prototype Pattern](https://www.dofactory.com/javascript/design-patterns/prototype): 此篇則是根據最純粹的 prototype pattern 觀念實作給你看什麼叫做「創建實例是從已存在的實例複製而來」
### 8. The Factory Pattern
- [GoF Design Patterns](https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA)) 中的一種
- 將創建物件的工作交給工廠來做,而不是自己手工 `new` 一個
- 有時候你不希望較 high-level 的程式碼知道太多,所以將實際創建物件的過程封裝起來。且當未來若需求變更甚至直接抽換掉底層的物件,high-level 的程式碼甚至不需要做變更
```javascript=
function FullTime () {
this.hourly = '$12'
}
function PartTime () {
this.hourly = '$10'
}
function Contractor () {
this.hourly = '$15'
}
function JobTypeFactory () {
this.createEmployee = function (name, type) {
let employee
if (type === 'fulltime') {
employee = new FullTime()
} else if (type === 'parttime') {
employee = new PartTime()
} else if (type === 'contractor') {
employee = new Contractor()
} else {
throw new Error(`Company does not offer ${type} type job now`)
}
employee.name = name
employee.type = type
employee.say = function () {
console.log(`${employee.name} is a ${employee.type}-type employee, wage is ${employee.hourly}/hr`)
}
return employee
}
}
const employees = []
const hr = new JobTypeFactory()
employees.push(hr.createEmployee('Max', 'fulltime'))
employees.push(hr.createEmployee('John', 'parttime'))
employees.push(hr.createEmployee('HH', 'contractor'))
employees.forEach((employee) => { employee.say() })
// Max is a fulltime-type employee, wage is $12/hr
// John is a parttime-type employee, wage is $10/hr
// HH is a contractor-type employee, wage is $15/hr
```
### 9. The Mixin Pattern
- 此概念提供一種機制,在不允許多重繼承的語言中達到「多重繼承」的效果;也可避免「菱形繼承」的隱憂
- Mixin 與「繼承」的目的很像,但子類別與父類別之間並不需要有「is a」的關係
- 因為沒有「is a」的關係,此 pattern 更著重在只 **reuse desired functionality**、而**非父類別全部的屬性/方法**
- Mixin Pattern 其實也是 [Dependency inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) 的一種實現
- 簡單來說我只實作一個 mixin class,裡頭有一些方法用來操作某個 parent class,然後透過語法將 mixin 提供的方法複製給 parent class
- 簡單的實作方案可參考 [digitalocean tutorial](https://www.digitalocean.com/community/tutorials/js-using-js-mixins)
```javascript=
const Alligator = function (speed, direction) {
this.speed = speed
this.direction = direction
}
// The swim property here is the "mixin"
const swim = {
location () {
console.log(`Heading ${this.direction} at ${this.speed}`)
}
}
// 許多 mixin 文章的解說,會直接使用 Object.assign 來達成同樣的目的
Object.assign(Alligator.prototype, swim)
const aaa = new Alligator('10 mph', 'East')
aaa.location()
// Heading East at 10 mph
const bbb = new Alligator('20 mph', 'North')
bbb.location()
// Heading North at 20 mph
console.log(aaa.location === bbb.location)
// true
```
### 10. The Decorator Pattern
- [GoF Design Patterns](https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA)) 中的一種
- 此模式,可以更靈活地替某類別增加功能或動態地修改行為、而不用真的撰寫子類別、繼承等
```javascript=
function iMac () {
this.cost = function () {
return 51100
}
this.screenSize = function () {
return 21.5
}
}
// Decorator 1
function LinePointDiscount (imac) {
const v = imac.cost()
imac.cost = function () {
return v - 5110
}
}
// Decorator 2
function SellAirPods (imac) {
const v = imac.cost()
imac.cost = function () {
return v - 3600
}
}
// Decorator 3
function SellKeyboardMouse (imac) {
const v = imac.cost()
imac.cost = function () {
return v - 4000
}
}
const imac = new iMac()
console.log('Original cost:', imac.cost())
// Original cost: 51100
LinePointDiscount(imac)
SellAirPods(imac)
SellKeyboardMouse(imac)
console.log('Final cost:', imac.cost())
// Final cost: 38390
```