###### 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 ```