關於JavaScript設計模式的詳盡指南

關於JavaScript設計模式的詳盡指南

在構建JavaScript應用程式時,你可能會遇到這樣的場景:你需要以某種預定義的方式構建物件,或者通過修改或調整一個普通的類來重複使用它,以適應多種使用情況。

當然,反覆解決這些問題並不方便。

這就是JavaScript設計模式來拯救你的地方。

JavaScript設計模式為你提供了一種結構化的、可重複的方法來解決JavaScript開發中經常出現的問題。

在本指南中,我們將看看什麼是JavaScript設計模式以及如何在你的JavaScript應用程式中使用它們。

  1. 什麼是JavaScript設計模式?
  2. 設計模式的型別
  3. 設計模式的要素
  4. 為什麼要使用設計模式?
  5. 需要掌握的20個JavaScript設計模式
  6. 實現設計模式的最佳實踐
  7. 什麼時候應該使用設計模式?

什麼是JavaScript設計模式?

JavaScript設計模式是針對JavaScript應用開發中經常出現的問題的可重複的模板解決方案。

這個想法很簡單。世界各地的程式設計師,自開發之初,在開發應用程式時都會遇到一些重複出現的問題。隨著時間的推移,一些開發者選擇記錄經過測試的方法來解決這些問題,以便其他人可以輕鬆地參考這些解決方案。

隨著越來越多的開發者選擇使用這些解決方案,並認識到它們在解決問題方面的效率,它們被接受為解決問題的標準方式,並被命名為 “設計模式”。

隨著人們對設計模式的重要性有了更深的認識,這些模式得到了進一步的發展和標準化。現在,大多數現代設計模式都有一個明確的結構,被組織在多個類別之下,並在電腦科學相關的學位中作為獨立的課題進行教學。

設計模式的型別

下面是一些最流行的JavaScript設計模式的分類。

創造性

創造性設計模式是那些幫助解決在JavaScript中建立和管理新物件例項的問題。它可以簡單到限制一個類只有一個物件,也可以複雜到定義一個複雜的方法來手工挑選和新增JavaScript物件中的每個特徵。

創造性設計模式的一些例子包括Singleton、Factory、Abstract Factory和Builder,等等。

結構性

結構性設計模式是那些幫助解決圍繞管理JavaScript物件的結構(或模式)的問題。這些問題可能包括在兩個不同的物件之間建立關係,或者為特定的使用者抽象出一個物件的某些特徵。

結構化設計模式的幾個例子包括Adapter、Bridge、Composite和Facade。

行為性

行為設計模式是那些幫助解決如何在不同物件之間傳遞控制(和責任)的問題。這些問題可能涉及到控制對一個連結列表的訪問,或者建立一個可以控制對多種型別物件訪問的單一實體。

行為設計模式的一些例子包括命令、迭代器、Memento和觀察者。

併發性

併發設計模式是那些有助於解決多執行緒和多工的問題。這些問題可能需要在多個可用物件中保持一個活動物件,或者通過解複用傳入的輸入並逐件處理,來處理提供給系統的多個事件。

併發設計模式的幾個例子包括活動物件、核反應和排程器。

架構性

架構設計模式是那些幫助解決廣義上的軟體設計問題的模式。這些通常與如何設計你的系統和確保高可用性、減少風險和避免效能瓶頸有關。

架構設計模式的兩個例子是MVC和MVVM。

設計模式的要素

幾乎所有的設計模式都可以被分解為一組四個重要的組成部分。它們是:

  • 模式名稱:這是在與其他使用者交流時用來識別設計模式的。例子包括 “singleton”、”prototype “等等。
  • 問題:這描述了設計模式的目的。它是對設計模式所要解決的問題的一個小描述。它甚至可以包括一個例子場景,以更好地解釋這個問題。它還可以包含一個設計模式要完全解決基本問題所需滿足的條件列表。
  • 解決方案:這是對當前問題的解決方案,由類、方法、介面等元素組成。這是一個設計模式的主要部分–它包含了各種元素的關係、責任和合作者,這些元素都是明確定義的。
  • 結果:這是對該模式能夠解決的問題的分析。諸如空間和時間的使用,以及解決同一問題的其他方法都會被討論。

如果你想了解更多關於設計模式及其誕生的資訊,MSU有一些簡單的學習材料,你可以參考一下。

為什麼要使用設計模式?

你想使用設計模式的原因有很多:

  • 它們是經過測試的:有了設計模式,你就有了一個經過測試的解決方案來解決你的問題(只要設計模式符合你問題的描述)。你不必浪費時間去尋找其他的解決方法,你可以放心,你有一個解決方案,為你解決基本的效能優化問題。
  • 它們很容易理解:設計模式就是要小而簡單,易於理解。你不需要成為一個在特定行業工作了幾十年的專業程式設計師來理解使用哪種設計模式。它們是有目的的通用的(不限於任何特定的程式語言),任何有足夠問題解決能力的人都可以理解。當你的技術團隊換人時,這也有幫助。一段依靠設計模式的程式碼對任何新的軟體開發人員來說都更容易理解。
  • 它們實施起來很簡單:大多數設計模式都非常簡單,正如你將在我們的文章中看到的那樣。你不需要知道多種程式設計概念就可以在你的程式碼中實現它們。
  • 它們提出的程式碼架構很容易被重用:整個科技行業都非常鼓勵程式碼的可重用性和簡約性,而設計模式可以幫助你實現這一點。由於這些模式是解決問題的標準方式,它們的設計者已經注意到確保所包含的應用程式架構保持可重用性、靈活性,並與大多數編寫程式碼的形式相容。
  • 它們可以節省時間和應用程式的大小:依靠一套標準的解決方案的最大好處之一是,它們將幫助你在實施時節省時間。很有可能你的整個開發團隊都很瞭解設計模式,所以他們在實施這些模式時,會更容易計劃、溝通和協作。經過嘗試和測試的解決方案意味著你很有可能在構建某些功能時不會出現資源洩露或走彎路的情況,從而為你節省時間和空間。另外,大多數程式語言為你提供了標準的模板庫,已經實現了一些常見的設計模式,如Iterator和Observer。

需要掌握的20個JavaScript設計模式

現在你明白了設計模式是由什麼組成的,以及為什麼你需要它們,讓我們更深入地瞭解一些最常用的JavaScript設計模式如何在JavaScript應用程式中實現。

創造性設計模式

讓我們從一些基本的、容易學習的創造型設計模式開始討論。

1. Singleton

Singleton模式是整個軟體開發行業最常用的設計模式之一。它所要解決的問題是隻維護一個類的單一例項。這在例項化資源密集型的物件時很方便,例如資料庫處理程式。

下面是你如何在JavaScript中實現它:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function SingletonFoo() {
let fooInstance = null;
// For our reference, let's create a counter that will track the number of active instances
let count = 0;
function printCount() {
console.log("Number of instances: " + count);
}
function init() {
// For our reference, we'll increase the count by one whenever init() is called
count++;
// Do the initialization of the resource-intensive object here and return it
return {}
}
function createInstance() {
if (fooInstance == null) {
fooInstance = init();
}
return fooInstance;
}
function closeInstance() {
count--;
fooInstance = null;
}
return {
initialize: createInstance,
close: closeInstance,
printCount: printCount
}
}
let foo = SingletonFoo();
foo.printCount() // Prints 0
foo.initialize()
foo.printCount() // Prints 1
foo.initialize()
foo.printCount() // Still prints 1
foo.initialize()
foo.printCount() // Still 1
foo.close()
foo.printCount() // Prints 0
function SingletonFoo() { let fooInstance = null; // For our reference, let's create a counter that will track the number of active instances let count = 0; function printCount() { console.log("Number of instances: " + count); } function init() { // For our reference, we'll increase the count by one whenever init() is called count++; // Do the initialization of the resource-intensive object here and return it return {} } function createInstance() { if (fooInstance == null) { fooInstance = init(); } return fooInstance; } function closeInstance() { count--; fooInstance = null; } return { initialize: createInstance, close: closeInstance, printCount: printCount } } let foo = SingletonFoo(); foo.printCount() // Prints 0 foo.initialize() foo.printCount() // Prints 1 foo.initialize() foo.printCount() // Still prints 1 foo.initialize() foo.printCount() // Still 1 foo.close() foo.printCount() // Prints 0
function SingletonFoo() {
let fooInstance = null;
// For our reference, let's create a counter that will track the number of active instances
let count = 0;
function printCount() {
console.log("Number of instances: " + count);
}
function init() {
// For our reference, we'll increase the count by one whenever init() is called
count++;
// Do the initialization of the resource-intensive object here and return it
return {}
}
function createInstance() {
if (fooInstance == null) {
fooInstance = init();
}
return fooInstance;
}
function closeInstance() {
count--;
fooInstance = null;
}
return {
initialize: createInstance,
close: closeInstance,
printCount: printCount
}
}
let foo = SingletonFoo();
foo.printCount() // Prints 0
foo.initialize()
foo.printCount() // Prints 1
foo.initialize()
foo.printCount() // Still prints 1
foo.initialize()
foo.printCount() // Still 1
foo.close()
foo.printCount() // Prints 0

雖然它的作用很好,但眾所周知,Singleton模式會使除錯變得困難,因為它掩蓋了依賴關係,並控制了初始化或銷燬一個類的例項的許可權。

2. Factory

Factory方法也是最流行的設計模式之一。Factory方法所要解決的問題是在不使用傳統建構函式的情況下建立物件。相反,它接收你想要的物件的配置(或描述)並返回新建立的物件。

下面是你如何在JavaScript中實現它:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function Factory() {
this.createDog = function (breed) {
let dog;
if (breed === "labrador") {
dog = new Labrador();
} else if (breed === "bulldog") {
dog = new Bulldog();
} else if (breed === "golden retriever") {
dog = new GoldenRetriever();
} else if (breed === "german shepherd") {
dog = new GermanShepherd();
}
dog.breed = breed;
dog.printInfo = function () {
console.log("\n\nBreed: " + dog.breed + "\nShedding Level (out of 5): " + dog.sheddingLevel + "\nCoat Length: " + dog.coatLength + "\nCoat Type: " + dog.coatType)
}
return dog;
}
}
function Labrador() {
this.sheddingLevel = 4
this.coatLength = "short"
this.coatType = "double"
}
function Bulldog() {
this.sheddingLevel = 3
this.coatLength = "short"
this.coatType = "smooth"
}
function GoldenRetriever() {
this.sheddingLevel = 4
this.coatLength = "medium"
this.coatType = "double"
}
function GermanShepherd() {
this.sheddingLevel = 4
this.coatLength = "medium"
this.coatType = "double"
}
function run() {
let dogs = [];
let factory = new Factory();
dogs.push(factory.createDog("labrador"));
dogs.push(factory.createDog("bulldog"));
dogs.push(factory.createDog("golden retriever"));
dogs.push(factory.createDog("german shepherd"));
for (var i = 0, len = dogs.length; i < len; i++) {
dogs[i].printInfo();
}
}
run()
/**
Output:
Breed: labrador
Shedding Level (out of 5): 4
Coat Length: short
Coat Type: double
Breed: bulldog
Shedding Level (out of 5): 3
Coat Length: short
Coat Type: smooth
Breed: golden retriever
Shedding Level (out of 5): 4
Coat Length: medium
Coat Type: double
Breed: german shepherd
Shedding Level (out of 5): 4
Coat Length: medium
Coat Type: double
*/
function Factory() { this.createDog = function (breed) { let dog; if (breed === "labrador") { dog = new Labrador(); } else if (breed === "bulldog") { dog = new Bulldog(); } else if (breed === "golden retriever") { dog = new GoldenRetriever(); } else if (breed === "german shepherd") { dog = new GermanShepherd(); } dog.breed = breed; dog.printInfo = function () { console.log("\n\nBreed: " + dog.breed + "\nShedding Level (out of 5): " + dog.sheddingLevel + "\nCoat Length: " + dog.coatLength + "\nCoat Type: " + dog.coatType) } return dog; } } function Labrador() { this.sheddingLevel = 4 this.coatLength = "short" this.coatType = "double" } function Bulldog() { this.sheddingLevel = 3 this.coatLength = "short" this.coatType = "smooth" } function GoldenRetriever() { this.sheddingLevel = 4 this.coatLength = "medium" this.coatType = "double" } function GermanShepherd() { this.sheddingLevel = 4 this.coatLength = "medium" this.coatType = "double" } function run() { let dogs = []; let factory = new Factory(); dogs.push(factory.createDog("labrador")); dogs.push(factory.createDog("bulldog")); dogs.push(factory.createDog("golden retriever")); dogs.push(factory.createDog("german shepherd")); for (var i = 0, len = dogs.length; i < len; i++) { dogs[i].printInfo(); } } run() /** Output: Breed: labrador Shedding Level (out of 5): 4 Coat Length: short Coat Type: double Breed: bulldog Shedding Level (out of 5): 3 Coat Length: short Coat Type: smooth Breed: golden retriever Shedding Level (out of 5): 4 Coat Length: medium Coat Type: double Breed: german shepherd Shedding Level (out of 5): 4 Coat Length: medium Coat Type: double */
function Factory() {
this.createDog = function (breed) {
let dog;
if (breed === "labrador") {
dog = new Labrador();
} else if (breed === "bulldog") {
dog = new Bulldog();
} else if (breed === "golden retriever") {
dog = new GoldenRetriever();
} else if (breed === "german shepherd") {
dog = new GermanShepherd();
}
dog.breed = breed;
dog.printInfo = function () {
console.log("\n\nBreed: " + dog.breed + "\nShedding Level (out of 5): " + dog.sheddingLevel + "\nCoat Length: " + dog.coatLength + "\nCoat Type: " + dog.coatType)
}
return dog;
}
}
function Labrador() {
this.sheddingLevel = 4
this.coatLength = "short"
this.coatType = "double"
}
function Bulldog() {
this.sheddingLevel = 3
this.coatLength = "short"
this.coatType = "smooth"
}
function GoldenRetriever() {
this.sheddingLevel = 4
this.coatLength = "medium"
this.coatType = "double"
}
function GermanShepherd() {
this.sheddingLevel = 4
this.coatLength = "medium"
this.coatType = "double"
}
function run() {
let dogs = [];
let factory = new Factory();
dogs.push(factory.createDog("labrador"));
dogs.push(factory.createDog("bulldog"));
dogs.push(factory.createDog("golden retriever"));
dogs.push(factory.createDog("german shepherd"));
for (var i = 0, len = dogs.length; i < len; i++) {
dogs[i].printInfo();
}
}
run()
/**
Output:
Breed: labrador
Shedding Level (out of 5): 4
Coat Length: short
Coat Type: double
Breed: bulldog
Shedding Level (out of 5): 3
Coat Length: short
Coat Type: smooth
Breed: golden retriever
Shedding Level (out of 5): 4
Coat Length: medium
Coat Type: double
Breed: german shepherd
Shedding Level (out of 5): 4
Coat Length: medium
Coat Type: double
*/

Factory設計模式控制了物件的建立方式,併為你提供了一個快速建立新物件的方法,以及一個定義了你的物件將擁有的屬性的統一介面。你可以新增任意多的狗種,但只要狗種型別所暴露的方法和屬性保持一致,它們就能完美地工作。

然而,請注意,Factory模式往往會導致大量的類難以管理。

3. Abstract Factory

Abstract Factory方法將工廠方法提升了一個層次,它使Factory變得抽象,因此可以在呼叫環境不知道具體使用的Factory或其內部工作原理的情況下進行替換。呼叫環境只知道所有的Factory都有一組共同的方法,它可以呼叫這些方法來執行例項化動作。

這就是使用前面的例子可以實現的方式:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// A factory to create dogs
function DogFactory() {
// Notice that the create function is now createPet instead of createDog, since we need
// it to be uniform across the other factories that will be used with this
this.createPet = function (breed) {
let dog;
if (breed === "labrador") {
dog = new Labrador();
} else if (breed === "pug") {
dog = new Pug();
}
dog.breed = breed;
dog.printInfo = function () {
console.log("\n\nType: " + dog.type + "\nBreed: " + dog.breed + "\nSize: " + dog.size)
}
return dog;
}
}
// A factory to create cats
function CatFactory() {
this.createPet = function (breed) {
let cat;
if (breed === "ragdoll") {
cat = new Ragdoll();
} else if (breed === "singapura") {
cat = new Singapura();
}
cat.breed = breed;
cat.printInfo = function () {
console.log("\n\nType: " + cat.type + "\nBreed: " + cat.breed + "\nSize: " + cat.size)
}
return cat;
}
}
// Dog and cat breed definitions
function Labrador() {
this.type = "dog"
this.size = "large"
}
function Pug() {
this.type = "dog"
this.size = "small"
}
function Ragdoll() {
this.type = "cat"
this.size = "large"
}
function Singapura() {
this.type = "cat"
this.size = "small"
}
function run() {
let pets = [];
// Initialize the two factories
let catFactory = new CatFactory();
let dogFactory = new DogFactory();
// Create a common petFactory that can produce both cats and dogs
// Set it to produce dogs first
let petFactory = dogFactory;
pets.push(petFactory.createPet("labrador"));
pets.push(petFactory.createPet("pug"));
// Set the petFactory to produce cats
petFactory = catFactory;
pets.push(petFactory.createPet("ragdoll"));
pets.push(petFactory.createPet("singapura"));
for (var i = 0, len = pets.length; i < len; i++) {
pets[i].printInfo();
}
}
run()
/**
Output:
Type: dog
Breed: labrador
Size: large
Type: dog
Breed: pug
Size: small
Type: cat
Breed: ragdoll
Size: large
Type: cat
Breed: singapura
Size: small
*/
// A factory to create dogs function DogFactory() { // Notice that the create function is now createPet instead of createDog, since we need // it to be uniform across the other factories that will be used with this this.createPet = function (breed) { let dog; if (breed === "labrador") { dog = new Labrador(); } else if (breed === "pug") { dog = new Pug(); } dog.breed = breed; dog.printInfo = function () { console.log("\n\nType: " + dog.type + "\nBreed: " + dog.breed + "\nSize: " + dog.size) } return dog; } } // A factory to create cats function CatFactory() { this.createPet = function (breed) { let cat; if (breed === "ragdoll") { cat = new Ragdoll(); } else if (breed === "singapura") { cat = new Singapura(); } cat.breed = breed; cat.printInfo = function () { console.log("\n\nType: " + cat.type + "\nBreed: " + cat.breed + "\nSize: " + cat.size) } return cat; } } // Dog and cat breed definitions function Labrador() { this.type = "dog" this.size = "large" } function Pug() { this.type = "dog" this.size = "small" } function Ragdoll() { this.type = "cat" this.size = "large" } function Singapura() { this.type = "cat" this.size = "small" } function run() { let pets = []; // Initialize the two factories let catFactory = new CatFactory(); let dogFactory = new DogFactory(); // Create a common petFactory that can produce both cats and dogs // Set it to produce dogs first let petFactory = dogFactory; pets.push(petFactory.createPet("labrador")); pets.push(petFactory.createPet("pug")); // Set the petFactory to produce cats petFactory = catFactory; pets.push(petFactory.createPet("ragdoll")); pets.push(petFactory.createPet("singapura")); for (var i = 0, len = pets.length; i < len; i++) { pets[i].printInfo(); } } run() /** Output: Type: dog Breed: labrador Size: large Type: dog Breed: pug Size: small Type: cat Breed: ragdoll Size: large Type: cat Breed: singapura Size: small */
// A factory to create dogs
function DogFactory() {
// Notice that the create function is now createPet instead of createDog, since we need
// it to be uniform across the other factories that will be used with this
this.createPet = function (breed) {
let dog;
if (breed === "labrador") {
dog = new Labrador();
} else if (breed === "pug") {
dog = new Pug();
}
dog.breed = breed;
dog.printInfo = function () {
console.log("\n\nType: " + dog.type + "\nBreed: " + dog.breed + "\nSize: " + dog.size)
}
return dog;
}
}
// A factory to create cats
function CatFactory() {
this.createPet = function (breed) {
let cat;
if (breed === "ragdoll") {
cat = new Ragdoll();
} else if (breed === "singapura") {
cat = new Singapura();
}
cat.breed = breed;
cat.printInfo = function () {
console.log("\n\nType: " + cat.type + "\nBreed: " + cat.breed + "\nSize: " + cat.size)
}
return cat;
}
}
// Dog and cat breed definitions
function Labrador() {
this.type = "dog"
this.size = "large"
}
function Pug() {
this.type = "dog"
this.size = "small"
}
function Ragdoll() {
this.type = "cat"
this.size = "large"
}
function Singapura() {
this.type = "cat"
this.size = "small"
}
function run() {
let pets = [];
// Initialize the two factories
let catFactory = new CatFactory();
let dogFactory = new DogFactory();
// Create a common petFactory that can produce both cats and dogs
// Set it to produce dogs first
let petFactory = dogFactory;
pets.push(petFactory.createPet("labrador"));
pets.push(petFactory.createPet("pug"));
// Set the petFactory to produce cats
petFactory = catFactory;
pets.push(petFactory.createPet("ragdoll"));
pets.push(petFactory.createPet("singapura"));
for (var i = 0, len = pets.length; i < len; i++) {
pets[i].printInfo();
}
}
run()
/**
Output:
Type: dog
Breed: labrador
Size: large
Type: dog
Breed: pug
Size: small
Type: cat
Breed: ragdoll
Size: large
Type: cat
Breed: singapura
Size: small
*/

Abstract Factory模式使你很容易交換具體的Factory,它有助於促進Factory和建立的產品之間的統一性。然而,引入新種類的產品會變得很困難,因為你必須對多個類進行修改以適應新方法/屬性。

4. Builder

Builder模式是最複雜但最靈活的JavaScript設計模式之一。它允許你在你的產品中逐一構建每個功能,為你提供對物件構建方式的完全控制,同時仍然抽象出內部細節。

在下面這個錯綜複雜的例子中,你會看到Builder設計模式與Director一起行動,以幫助製作披薩!

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Here's the PizzaBuilder (you can also call it the chef)
function PizzaBuilder() {
let base
let sauce
let cheese
let toppings = []
// The definition of pizza is hidden from the customers
function Pizza(base, sauce, cheese, toppings) {
this.base = base
this.sauce = sauce
this.cheese = cheese
this.toppings = toppings
this.printInfo = function() {
console.log("This pizza has " + this.base + " base with " + this.sauce + " sauce "
+ (this.cheese !== undefined ? "with cheese. " : "without cheese. ")
+ (this.toppings.length !== 0 ? "It has the following toppings: " + toppings.toString() : ""))
}
}
// You can request the PizzaBuilder (/chef) to perform any of the following actions on your pizza
return {
addFlatbreadBase: function() {
base = "flatbread"
return this;
},
addTomatoSauce: function() {
sauce = "tomato"
return this;
},
addAlfredoSauce: function() {
sauce = "alfredo"
return this;
},
addCheese: function() {
cheese = "parmesan"
return this;
},
addOlives: function() {
toppings.push("olives")
return this
},
addJalapeno: function() {
toppings.push("jalapeno")
return this
},
cook: function() {
if (base === null){
console.log("Can't make a pizza without a base")
return
}
return new Pizza(base, sauce, cheese, toppings)
}
}
}
// This is the Director for the PizzaBuilder, aka the PizzaShop.
// It contains a list of preset steps that can be used to prepare common pizzas (aka recipes!)
function PizzaShop() {
return {
makePizzaMargherita: function() {
pizzaBuilder = new PizzaBuilder()
pizzaMargherita = pizzaBuilder.addFlatbreadBase().addTomatoSauce().addCheese().addOlives().cook()
return pizzaMargherita
},
makePizzaAlfredo: function() {
pizzaBuilder = new PizzaBuilder()
pizzaAlfredo = pizzaBuilder.addFlatbreadBase().addAlfredoSauce().addCheese().addJalapeno().cook()
return pizzaAlfredo
},
makePizzaMarinara: function() {
pizzaBuilder = new PizzaBuilder()
pizzaMarinara = pizzaBuilder.addFlatbreadBase().addTomatoSauce().addOlives().cook()
return pizzaMarinara
}
}
}
// Here's where the customer can request pizzas from
function run() {
let pizzaShop = new PizzaShop()
// You can ask for one of the popular pizza recipes...
let pizzaMargherita = pizzaShop.makePizzaMargherita()
pizzaMargherita.printInfo()
// Output: This pizza has flatbread base with tomato sauce with cheese. It has the following toppings: olives
let pizzaAlfredo = pizzaShop.makePizzaAlfredo()
pizzaAlfredo.printInfo()
// Output: This pizza has flatbread base with alfredo sauce with cheese. It has the following toppings: jalapeno
let pizzaMarinara = pizzaShop.makePizzaMarinara()
pizzaMarinara.printInfo()
// Output: This pizza has flatbread base with tomato sauce without cheese. It has the following toppings: olives
// Or send your custom request directly to the chef!
let chef = PizzaBuilder()
let customPizza = chef.addFlatbreadBase().addTomatoSauce().addCheese().addOlives().addJalapeno().cook()
customPizza.printInfo()
// Output: This pizza has flatbread base with tomato sauce with cheese. It has the following toppings: olives,jalapeno
}
run()
// Here's the PizzaBuilder (you can also call it the chef) function PizzaBuilder() { let base let sauce let cheese let toppings = [] // The definition of pizza is hidden from the customers function Pizza(base, sauce, cheese, toppings) { this.base = base this.sauce = sauce this.cheese = cheese this.toppings = toppings this.printInfo = function() { console.log("This pizza has " + this.base + " base with " + this.sauce + " sauce " + (this.cheese !== undefined ? "with cheese. " : "without cheese. ") + (this.toppings.length !== 0 ? "It has the following toppings: " + toppings.toString() : "")) } } // You can request the PizzaBuilder (/chef) to perform any of the following actions on your pizza return { addFlatbreadBase: function() { base = "flatbread" return this; }, addTomatoSauce: function() { sauce = "tomato" return this; }, addAlfredoSauce: function() { sauce = "alfredo" return this; }, addCheese: function() { cheese = "parmesan" return this; }, addOlives: function() { toppings.push("olives") return this }, addJalapeno: function() { toppings.push("jalapeno") return this }, cook: function() { if (base === null){ console.log("Can't make a pizza without a base") return } return new Pizza(base, sauce, cheese, toppings) } } } // This is the Director for the PizzaBuilder, aka the PizzaShop. // It contains a list of preset steps that can be used to prepare common pizzas (aka recipes!) function PizzaShop() { return { makePizzaMargherita: function() { pizzaBuilder = new PizzaBuilder() pizzaMargherita = pizzaBuilder.addFlatbreadBase().addTomatoSauce().addCheese().addOlives().cook() return pizzaMargherita }, makePizzaAlfredo: function() { pizzaBuilder = new PizzaBuilder() pizzaAlfredo = pizzaBuilder.addFlatbreadBase().addAlfredoSauce().addCheese().addJalapeno().cook() return pizzaAlfredo }, makePizzaMarinara: function() { pizzaBuilder = new PizzaBuilder() pizzaMarinara = pizzaBuilder.addFlatbreadBase().addTomatoSauce().addOlives().cook() return pizzaMarinara } } } // Here's where the customer can request pizzas from function run() { let pizzaShop = new PizzaShop() // You can ask for one of the popular pizza recipes... let pizzaMargherita = pizzaShop.makePizzaMargherita() pizzaMargherita.printInfo() // Output: This pizza has flatbread base with tomato sauce with cheese. It has the following toppings: olives let pizzaAlfredo = pizzaShop.makePizzaAlfredo() pizzaAlfredo.printInfo() // Output: This pizza has flatbread base with alfredo sauce with cheese. It has the following toppings: jalapeno let pizzaMarinara = pizzaShop.makePizzaMarinara() pizzaMarinara.printInfo() // Output: This pizza has flatbread base with tomato sauce without cheese. It has the following toppings: olives // Or send your custom request directly to the chef! let chef = PizzaBuilder() let customPizza = chef.addFlatbreadBase().addTomatoSauce().addCheese().addOlives().addJalapeno().cook() customPizza.printInfo() // Output: This pizza has flatbread base with tomato sauce with cheese. It has the following toppings: olives,jalapeno } run()
// Here's the PizzaBuilder (you can also call it the chef)
function PizzaBuilder() {
let base
let sauce
let cheese
let toppings = []
// The definition of pizza is hidden from the customers
function Pizza(base, sauce, cheese, toppings) {
this.base = base
this.sauce = sauce
this.cheese = cheese
this.toppings = toppings
this.printInfo = function() {
console.log("This pizza has " + this.base + " base with " + this.sauce + " sauce "
+ (this.cheese !== undefined ? "with cheese. " : "without cheese. ")
+ (this.toppings.length !== 0 ? "It has the following toppings: " + toppings.toString() : ""))
}
}
// You can request the PizzaBuilder (/chef) to perform any of the following actions on your pizza
return {
addFlatbreadBase: function() {
base = "flatbread"
return this;
},
addTomatoSauce: function() {
sauce = "tomato"
return this;
},
addAlfredoSauce: function() {
sauce = "alfredo"
return this;
},
addCheese: function() {
cheese = "parmesan"
return this;
},
addOlives: function() {
toppings.push("olives")
return this
},
addJalapeno: function() {
toppings.push("jalapeno")
return this
},
cook: function() {
if (base === null){
console.log("Can't make a pizza without a base")
return
}
return new Pizza(base, sauce, cheese, toppings)
}
}
}
// This is the Director for the PizzaBuilder, aka the PizzaShop.
// It contains a list of preset steps that can be used to prepare common pizzas (aka recipes!)
function PizzaShop() {
return {
makePizzaMargherita: function() {
pizzaBuilder = new PizzaBuilder()
pizzaMargherita = pizzaBuilder.addFlatbreadBase().addTomatoSauce().addCheese().addOlives().cook()
return pizzaMargherita
},
makePizzaAlfredo: function() {
pizzaBuilder = new PizzaBuilder()
pizzaAlfredo = pizzaBuilder.addFlatbreadBase().addAlfredoSauce().addCheese().addJalapeno().cook()
return pizzaAlfredo
},
makePizzaMarinara: function() {
pizzaBuilder = new PizzaBuilder()
pizzaMarinara = pizzaBuilder.addFlatbreadBase().addTomatoSauce().addOlives().cook()
return pizzaMarinara
}
}
}
// Here's where the customer can request pizzas from
function run() {
let pizzaShop = new PizzaShop()
// You can ask for one of the popular pizza recipes...
let pizzaMargherita = pizzaShop.makePizzaMargherita()
pizzaMargherita.printInfo()
// Output: This pizza has flatbread base with tomato sauce with cheese. It has the following toppings: olives
let pizzaAlfredo = pizzaShop.makePizzaAlfredo()
pizzaAlfredo.printInfo()
// Output: This pizza has flatbread base with alfredo sauce with cheese. It has the following toppings: jalapeno
let pizzaMarinara = pizzaShop.makePizzaMarinara()
pizzaMarinara.printInfo()
// Output: This pizza has flatbread base with tomato sauce without cheese. It has the following toppings: olives
// Or send your custom request directly to the chef!
let chef = PizzaBuilder()
let customPizza = chef.addFlatbreadBase().addTomatoSauce().addCheese().addOlives().addJalapeno().cook()
customPizza.printInfo()
// Output: This pizza has flatbread base with tomato sauce with cheese. It has the following toppings: olives,jalapeno
}
run()

你可以將Builder與Director配對,如上面例子中的 PizzaShop 類所示,預先定義一套步驟,以便每次構建你的產品的標準變體,即你的比薩餅的特定配方。

這種設計模式的唯一問題是,它的設定和維護相當複雜。不過,通過這種方式新增新的功能比Factory方法更簡單。

5. Prototype

Prototype設計模式是一種通過克隆現有物件來建立新物件的快速而簡單的方法。

首先建立一個Prototype物件,它可以被多次克隆以建立新物件。當直接例項化一個物件比建立一個現有物件的副本更耗費資源時,它就會派上用場。

在下面的例子中,你將看到如何使用Prototype模式來建立基於設定的模板文件的新文件:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Defining how a document would look like
function Document() {
this.header = "Acme Co"
this.footer = "For internal use only"
this.pages = 2
this.text = ""
this.addText = function(text) {
this.text += text
}
// Method to help you see the contents of the object
this.printInfo = function() {
console.log("\n\nHeader: " + this.header + "\nFooter: " + this.footer + "\nPages: " + this.pages + "\nText: " + this.text)
}
}
// A protype (or template) for creating new blank documents with boilerplate information
function DocumentPrototype(baseDocument) {
this.baseDocument = baseDocument
// This is where the magic happens. A new document object is created and is assigned the values of the current object
this.clone = function() {
let document = new Document();
document.header = this.baseDocument.header
document.footer = this.baseDocument.footer
document.pages = this.baseDocument.pages
document.text = this.baseDocument.text
return document
}
}
function run() {
// Create a document to use as the base for the prototype
let baseDocument = new Document()
// Make some changes to the prototype
baseDocument.addText("This text was added before cloning and will be common in both documents. ")
let prototype = new DocumentPrototype(baseDocument)
// Create two documents from the prototype
let doc1 = prototype.clone()
let doc2 = prototype.clone()
// Make some changes to both objects
doc1.pages = 3
doc1.addText("This is document 1")
doc2.addText("This is document 2")
// Print their values
doc1.printInfo()
/* Output:
Header: Acme Co
Footer: For internal use only
Pages: 3
Text: This text was added before cloning and will be common in both documents. This is document 1
*/
doc2.printInfo()
/** Output:
Header: Acme Co
Footer: For internal use only
Pages: 2
Text: This text was added before cloning and will be common in both documents. This is document 2
*/
}
run()
// Defining how a document would look like function Document() { this.header = "Acme Co" this.footer = "For internal use only" this.pages = 2 this.text = "" this.addText = function(text) { this.text += text } // Method to help you see the contents of the object this.printInfo = function() { console.log("\n\nHeader: " + this.header + "\nFooter: " + this.footer + "\nPages: " + this.pages + "\nText: " + this.text) } } // A protype (or template) for creating new blank documents with boilerplate information function DocumentPrototype(baseDocument) { this.baseDocument = baseDocument // This is where the magic happens. A new document object is created and is assigned the values of the current object this.clone = function() { let document = new Document(); document.header = this.baseDocument.header document.footer = this.baseDocument.footer document.pages = this.baseDocument.pages document.text = this.baseDocument.text return document } } function run() { // Create a document to use as the base for the prototype let baseDocument = new Document() // Make some changes to the prototype baseDocument.addText("This text was added before cloning and will be common in both documents. ") let prototype = new DocumentPrototype(baseDocument) // Create two documents from the prototype let doc1 = prototype.clone() let doc2 = prototype.clone() // Make some changes to both objects doc1.pages = 3 doc1.addText("This is document 1") doc2.addText("This is document 2") // Print their values doc1.printInfo() /* Output: Header: Acme Co Footer: For internal use only Pages: 3 Text: This text was added before cloning and will be common in both documents. This is document 1 */ doc2.printInfo() /** Output: Header: Acme Co Footer: For internal use only Pages: 2 Text: This text was added before cloning and will be common in both documents. This is document 2 */ } run()
// Defining how a document would look like
function Document() {
this.header = "Acme Co"
this.footer = "For internal use only"
this.pages = 2
this.text = ""
this.addText = function(text) {
this.text += text
}
// Method to help you see the contents of the object
this.printInfo = function() {
console.log("\n\nHeader: " + this.header + "\nFooter: " + this.footer + "\nPages: " + this.pages + "\nText: " + this.text)
}
}
// A protype (or template) for creating new blank documents with boilerplate information
function DocumentPrototype(baseDocument) {
this.baseDocument = baseDocument
// This is where the magic happens. A new document object is created and is assigned the values of the current object
this.clone = function() {
let document = new Document();
document.header = this.baseDocument.header
document.footer = this.baseDocument.footer
document.pages = this.baseDocument.pages
document.text = this.baseDocument.text
return document
}
}
function run() {
// Create a document to use as the base for the prototype
let baseDocument = new Document()
// Make some changes to the prototype
baseDocument.addText("This text was added before cloning and will be common in both documents. ")
let prototype = new DocumentPrototype(baseDocument)
// Create two documents from the prototype
let doc1 = prototype.clone()
let doc2 = prototype.clone()
// Make some changes to both objects
doc1.pages = 3
doc1.addText("This is document 1")
doc2.addText("This is document 2")
// Print their values
doc1.printInfo()
/* Output:
Header: Acme Co
Footer: For internal use only
Pages: 3
Text: This text was added before cloning and will be common in both documents. This is document 1
*/
doc2.printInfo()
/** Output:
Header: Acme Co
Footer: For internal use only
Pages: 2
Text: This text was added before cloning and will be common in both documents. This is document 2
*/
}
run()

原型方法在很大一部分物件共享相同的值,或者完全建立一個新物件的成本很高的情況下非常有用。然而,在你不需要超過幾個類的例項的情況下,它就顯得有些矯枉過正。

結構性設計模式

結構化設計模式通過提供久經考驗的結構化類的方法來幫助你組織你的業務邏輯。有各種各樣的結構設計模式,每一種都是為了滿足獨特的使用情況。

6. Adapter

構建應用程式時,一個常見的問題是允許不相容的類之間的協作。

要理解這一點,一個很好的例子是在保持向後相容的同時。如果你寫了一個新版本的類,你自然希望它能在舊版本工作的所有地方都能輕鬆使用。然而,如果你做了一些破壞性的改變,比如刪除或更新對舊版本的功能至關重要的方法,你可能最終會得到一個需要更新其所有客戶端才能執行的類。

在這種情況下,Adapter設計模式可以提供幫助。

Adapter設計模式為你提供了一個抽象,在新類的方法和屬性與舊類的方法和屬性之間架起了橋樑。它具有與舊類相同的介面,但它包含將舊方法對映到新方法的邏輯,以執行類似的操作。這類似於一個電源插頭插座如何在美式插頭和歐式插頭之間充當介面卡。

這裡有一個例子:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Old bot
function Robot() {
this.walk = function(numberOfSteps) {
// code to make the robot walk
console.log("walked " + numberOfSteps + " steps")
}
this.sit = function() {
// code to make the robot sit
console.log("sit")
}
}
// New bot that does not have the walk function anymore
// but instead has functions to control each step independently
function AdvancedRobot(botName) {
// the new bot has a name as well
this.name = botName
this.sit = function() {
// code to make the robot sit
console.log("sit")
}
this.rightStepForward = function() {
// code to take 1 step from right leg forward
console.log("right step forward")
}
this.leftStepForward = function () {
// code to take 1 step from left leg forward
console.log("left step forward")
}
}
function RobotAdapter(botName) {
// No references to the old interfact since that is usually
// phased out of development
const robot = new AdvancedRobot(botName)
// The adapter defines the walk function by using the
// two step controls. You now have room to choose which leg to begin/end with,
// and do something at each step.
this.walk = function(numberOfSteps) {
for (let i=0; i<numberOfSteps; i++) {
if (i % 2 === 0) {
robot.rightStepForward()
} else {
robot.leftStepForward()
}
}
}
this.sit = robot.sit
}
function run() {
let robot = new Robot()
robot.sit()
// Output: sit
robot.walk(5)
// Output: walked 5 steps
robot = new RobotAdapter("my bot")
robot.sit()
// Output: sit
robot.walk(5)
// Output:
// right step forward
// left step forward
// right step forward
// left step forward
// right step forward
}
run()
// Old bot function Robot() { this.walk = function(numberOfSteps) { // code to make the robot walk console.log("walked " + numberOfSteps + " steps") } this.sit = function() { // code to make the robot sit console.log("sit") } } // New bot that does not have the walk function anymore // but instead has functions to control each step independently function AdvancedRobot(botName) { // the new bot has a name as well this.name = botName this.sit = function() { // code to make the robot sit console.log("sit") } this.rightStepForward = function() { // code to take 1 step from right leg forward console.log("right step forward") } this.leftStepForward = function () { // code to take 1 step from left leg forward console.log("left step forward") } } function RobotAdapter(botName) { // No references to the old interfact since that is usually // phased out of development const robot = new AdvancedRobot(botName) // The adapter defines the walk function by using the // two step controls. You now have room to choose which leg to begin/end with, // and do something at each step. this.walk = function(numberOfSteps) { for (let i=0; i<numberOfSteps; i++) { if (i % 2 === 0) { robot.rightStepForward() } else { robot.leftStepForward() } } } this.sit = robot.sit } function run() { let robot = new Robot() robot.sit() // Output: sit robot.walk(5) // Output: walked 5 steps robot = new RobotAdapter("my bot") robot.sit() // Output: sit robot.walk(5) // Output: // right step forward // left step forward // right step forward // left step forward // right step forward } run()
// Old bot
function Robot() {
this.walk = function(numberOfSteps) {
// code to make the robot walk
console.log("walked " + numberOfSteps + " steps")
}
this.sit = function() {
// code to make the robot sit
console.log("sit")
}
}
// New bot that does not have the walk function anymore
// but instead has functions to control each step independently
function AdvancedRobot(botName) {
// the new bot has a name as well
this.name = botName
this.sit = function() {
// code to make the robot sit
console.log("sit")
}
this.rightStepForward = function() {
// code to take 1 step from right leg forward
console.log("right step forward")
}
this.leftStepForward = function () {
// code to take 1 step from left leg forward
console.log("left step forward")
}
}
function RobotAdapter(botName) {
// No references to the old interfact since that is usually
// phased out of development
const robot = new AdvancedRobot(botName)
// The adapter defines the walk function by using the
// two step controls. You now have room to choose which leg to begin/end with,
// and do something at each step.
this.walk = function(numberOfSteps) {
for (let i=0; i<numberOfSteps; i++) {
if (i % 2 === 0) {
robot.rightStepForward()
} else {
robot.leftStepForward()
}
}
}
this.sit = robot.sit
}
function run() {
let robot = new Robot()
robot.sit()
// Output: sit
robot.walk(5)
// Output: walked 5 steps
robot = new RobotAdapter("my bot")
robot.sit()
// Output: sit
robot.walk(5)
// Output:
// right step forward
// left step forward
// right step forward
// left step forward
// right step forward
}
run()

這種設計模式的主要問題是,它增加了你的原始碼的複雜性。你已經需要維護兩個不同的類,而現在你又有一個類–介面卡–需要維護。

7. Bridge

在Adapter模式的基礎上,Bridge設計模式為類和客戶提供了獨立的介面,這樣即使在本地介面不相容的情況下,它們也可以同時工作。

它有助於在這兩種型別的物件之間開發一個非常鬆散的耦合介面。這也有助於提高介面及其實現的可擴充套件性,以獲得最大的靈活性。

下面是你如何使用它:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// The TV and speaker share the same interface
function TV() {
this.increaseVolume = function() {
// logic to increase TV volume
}
this.decreaseVolume = function() {
// logic to decrease TV volume
}
this.mute = function() {
// logic to mute TV audio
}
}
function Speaker() {
this.increaseVolume = function() {
// logic to increase speaker volume
}
this.decreaseVolume = function() {
// logic to decrease speaker volume
}
this.mute() = function() {
// logic to mute speaker audio
}
}
// The two remotes make use of the same common interface
// that supports volume up and volume down features
function SimpleRemote(device) {
this.pressVolumeDownKey = function() {
device.decreaseVolume()
}
this.pressVolumeUpKey = function() {
device.increaseVolume()
}
}
function AdvancedRemote(device) {
this.pressVolumeDownKey = function() {
device.decreaseVolume()
}
this.pressVolumeUpKey = function() {
device.increaseVolume()
}
this.pressMuteKey = function() {
device.mute()
}
}
function run() {
let tv = new TV()
let speaker = new Speaker()
let tvSimpleRemote = new SimpleRemote(tv)
let tvAdvancedRemote = new AdvancedRemote(tv)
let speakerSimpleRemote = new SimpleRemote(speaker)
let speakerAdvancedRemote = new AdvancedRemote(speaker)
// The methods listed in pair below will have the same effect
// on their target devices
tvSimpleRemote.pressVolumeDownKey()
tvAdvancedRemote.pressVolumeDownKey()
tvSimpleRemote.pressVolumeUpKey()
tvAdvancedRemote.pressVolumeUpKey()
// The advanced remote has additional functionality
tvAdvancedRemote.pressMuteKey()
speakerSimpleRemote.pressVolumeDownKey()
speakerAdvancedRemote.pressVolumeDownKey()
speakerSimpleRemote.pressVolumeUpKey()
speakerAdvancedRemote.pressVolumeUpKey()
speakerAdvancedRemote.pressMuteKey()
}
// The TV and speaker share the same interface function TV() { this.increaseVolume = function() { // logic to increase TV volume } this.decreaseVolume = function() { // logic to decrease TV volume } this.mute = function() { // logic to mute TV audio } } function Speaker() { this.increaseVolume = function() { // logic to increase speaker volume } this.decreaseVolume = function() { // logic to decrease speaker volume } this.mute() = function() { // logic to mute speaker audio } } // The two remotes make use of the same common interface // that supports volume up and volume down features function SimpleRemote(device) { this.pressVolumeDownKey = function() { device.decreaseVolume() } this.pressVolumeUpKey = function() { device.increaseVolume() } } function AdvancedRemote(device) { this.pressVolumeDownKey = function() { device.decreaseVolume() } this.pressVolumeUpKey = function() { device.increaseVolume() } this.pressMuteKey = function() { device.mute() } } function run() { let tv = new TV() let speaker = new Speaker() let tvSimpleRemote = new SimpleRemote(tv) let tvAdvancedRemote = new AdvancedRemote(tv) let speakerSimpleRemote = new SimpleRemote(speaker) let speakerAdvancedRemote = new AdvancedRemote(speaker) // The methods listed in pair below will have the same effect // on their target devices tvSimpleRemote.pressVolumeDownKey() tvAdvancedRemote.pressVolumeDownKey() tvSimpleRemote.pressVolumeUpKey() tvAdvancedRemote.pressVolumeUpKey() // The advanced remote has additional functionality tvAdvancedRemote.pressMuteKey() speakerSimpleRemote.pressVolumeDownKey() speakerAdvancedRemote.pressVolumeDownKey() speakerSimpleRemote.pressVolumeUpKey() speakerAdvancedRemote.pressVolumeUpKey() speakerAdvancedRemote.pressMuteKey() }
// The TV and speaker share the same interface
function TV() {
this.increaseVolume = function() {
// logic to increase TV volume
}
this.decreaseVolume = function() {
// logic to decrease TV volume
}
this.mute = function() {
// logic to mute TV audio
}
}
function Speaker() {
this.increaseVolume = function() {
// logic to increase speaker volume
}
this.decreaseVolume = function() {
// logic to decrease speaker volume
}
this.mute() = function() {
// logic to mute speaker audio
}
}
// The two remotes make use of the same common interface
// that supports volume up and volume down features
function SimpleRemote(device) {
this.pressVolumeDownKey = function() {
device.decreaseVolume()
}
this.pressVolumeUpKey = function() {
device.increaseVolume()
}
}
function AdvancedRemote(device) {
this.pressVolumeDownKey = function() {
device.decreaseVolume()
}
this.pressVolumeUpKey = function() {
device.increaseVolume()
}
this.pressMuteKey = function() {
device.mute()
}
}
function run() {
let tv = new TV()
let speaker = new Speaker()
let tvSimpleRemote = new SimpleRemote(tv)
let tvAdvancedRemote = new AdvancedRemote(tv)
let speakerSimpleRemote = new SimpleRemote(speaker)
let speakerAdvancedRemote = new AdvancedRemote(speaker)
// The methods listed in pair below will have the same effect
// on their target devices
tvSimpleRemote.pressVolumeDownKey()
tvAdvancedRemote.pressVolumeDownKey()
tvSimpleRemote.pressVolumeUpKey()
tvAdvancedRemote.pressVolumeUpKey()
// The advanced remote has additional functionality
tvAdvancedRemote.pressMuteKey()
speakerSimpleRemote.pressVolumeDownKey()
speakerAdvancedRemote.pressVolumeDownKey()
speakerSimpleRemote.pressVolumeUpKey()
speakerAdvancedRemote.pressVolumeUpKey()
speakerAdvancedRemote.pressMuteKey()
}

你可能已經猜到了,橋接模式大大增加了程式碼庫的複雜性。而且,大多數介面在現實世界的用例中通常最終只有一個實現,所以你並不能真正從程式碼的重用性中獲益。

8. Composite

Composite複合設計模式幫助你輕鬆地結構和管理類似的物件和實體。複合模式的基本思想是,物件和它們的邏輯容器可以用一個單一的抽象類來表示(可以儲存與物件相關的資料/方法以及對容器本身的引用)。

當你的資料模型類似於樹狀結構時,使用複合模式是最有意義的。然而,你不應該僅僅為了使用複合模式而試圖將一個非樹形的資料模型變成一個樹形的資料模型,因為這樣做往往會失去很多靈活性。

在下面的例子中,你將看到如何使用複合設計模式來構建一個電子商務產品的包裝系統,該系統還可以計算每個包裝的總訂單價值:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// A product class, that acts as a Leaf node
function Product(name, price) {
this.name = name
this.price = price
this.getTotalPrice = function() {
return this.price
}
}
// A box class, that acts as a parent/child node
function Box(name) {
this.contents = []
this.name = name
// Helper function to add an item to the box
this.add = function(content){
this.contents.push(content)
}
// Helper function to remove an item from the box
this.remove = function() {
var length = this.contents.length;
for (var i = 0; i < length; i++) {
if (this.contents[i] === child) {
this.contents.splice(i, 1);
return;
}
}
}
// Helper function to get one item from the box
this.getContent = function(position) {
return this.contents[position]
}
// Helper function to get the total count of the items in the box
this.getTotalCount = function() {
return this.contents.length
}
// Helper function to calculate the total price of all items in the box
this.getTotalPrice = function() {
let totalPrice = 0;
for (let i=0; i < this.getTotalCount(); i++){
totalPrice += this.getContent(i).getTotalPrice()
}
return totalPrice
}
}
function run() {
// Let's create some electronics
const mobilePhone = new Product("mobile phone," 1000)
const phoneCase = new Product("phone case," 30)
const screenProtector = new Product("screen protector," 20)
// and some stationery products
const pen = new Product("pen," 2)
const pencil = new Product("pencil," 0.5)
const eraser = new Product("eraser," 0.5)
const stickyNotes = new Product("sticky notes," 10)
// and put them in separate boxes
const electronicsBox = new Box("electronics")
electronicsBox.add(mobilePhone)
electronicsBox.add(phoneCase)
electronicsBox.add(screenProtector)
const stationeryBox = new Box("stationery")
stationeryBox.add(pen)
stationeryBox.add(pencil)
stationeryBox.add(eraser)
stationeryBox.add(stickyNotes)
// and finally, put them into one big box for convenient shipping
const package = new Box('package')
package.add(electronicsBox)
package.add(stationeryBox)
// Here's an easy way to calculate the total order value
console.log("Total order price: USD " + package.getTotalPrice())
// Output: USD 1063
}
run()
// A product class, that acts as a Leaf node function Product(name, price) { this.name = name this.price = price this.getTotalPrice = function() { return this.price } } // A box class, that acts as a parent/child node function Box(name) { this.contents = [] this.name = name // Helper function to add an item to the box this.add = function(content){ this.contents.push(content) } // Helper function to remove an item from the box this.remove = function() { var length = this.contents.length; for (var i = 0; i < length; i++) { if (this.contents[i] === child) { this.contents.splice(i, 1); return; } } } // Helper function to get one item from the box this.getContent = function(position) { return this.contents[position] } // Helper function to get the total count of the items in the box this.getTotalCount = function() { return this.contents.length } // Helper function to calculate the total price of all items in the box this.getTotalPrice = function() { let totalPrice = 0; for (let i=0; i < this.getTotalCount(); i++){ totalPrice += this.getContent(i).getTotalPrice() } return totalPrice } } function run() { // Let's create some electronics const mobilePhone = new Product("mobile phone," 1000) const phoneCase = new Product("phone case," 30) const screenProtector = new Product("screen protector," 20) // and some stationery products const pen = new Product("pen," 2) const pencil = new Product("pencil," 0.5) const eraser = new Product("eraser," 0.5) const stickyNotes = new Product("sticky notes," 10) // and put them in separate boxes const electronicsBox = new Box("electronics") electronicsBox.add(mobilePhone) electronicsBox.add(phoneCase) electronicsBox.add(screenProtector) const stationeryBox = new Box("stationery") stationeryBox.add(pen) stationeryBox.add(pencil) stationeryBox.add(eraser) stationeryBox.add(stickyNotes) // and finally, put them into one big box for convenient shipping const package = new Box('package') package.add(electronicsBox) package.add(stationeryBox) // Here's an easy way to calculate the total order value console.log("Total order price: USD " + package.getTotalPrice()) // Output: USD 1063 } run()
// A product class, that acts as a Leaf node
function Product(name, price) {
this.name = name
this.price = price
this.getTotalPrice = function() {
return this.price
}
}
// A box class, that acts as a parent/child node
function Box(name) {
this.contents = []
this.name = name
// Helper function to add an item to the box
this.add = function(content){
this.contents.push(content)
}
// Helper function to remove an item from the box
this.remove = function() {
var length = this.contents.length;
for (var i = 0; i < length; i++) {
if (this.contents[i] === child) {
this.contents.splice(i, 1);
return;
}
}
}
// Helper function to get one item from the box
this.getContent = function(position) {
return this.contents[position]
}
// Helper function to get the total count of the items in the box
this.getTotalCount = function() {
return this.contents.length
}
// Helper function to calculate the total price of all items in the box
this.getTotalPrice = function() {
let totalPrice = 0;
for (let i=0; i < this.getTotalCount(); i++){
totalPrice += this.getContent(i).getTotalPrice()
}
return totalPrice
}
}
function run() {
// Let's create some electronics
const mobilePhone = new Product("mobile phone," 1000)
const phoneCase = new Product("phone case," 30)
const screenProtector = new Product("screen protector," 20)
// and some stationery products
const pen = new Product("pen," 2)
const pencil = new Product("pencil," 0.5)
const eraser = new Product("eraser," 0.5)
const stickyNotes = new Product("sticky notes," 10)
// and put them in separate boxes
const electronicsBox = new Box("electronics")
electronicsBox.add(mobilePhone)
electronicsBox.add(phoneCase)
electronicsBox.add(screenProtector)
const stationeryBox = new Box("stationery")
stationeryBox.add(pen)
stationeryBox.add(pencil)
stationeryBox.add(eraser)
stationeryBox.add(stickyNotes)
// and finally, put them into one big box for convenient shipping
const package = new Box('package')
package.add(electronicsBox)
package.add(stationeryBox)
// Here's an easy way to calculate the total order value
console.log("Total order price: USD " + package.getTotalPrice())
// Output: USD 1063
}
run()

使用複合模式的最大缺點是,將來對元件介面的修改會非常具有挑戰性。設計介面需要時間和精力,而資料模型的樹狀特性會使你很難隨心所欲地進行修改。

9. Decorator

Decorator模式可以幫助你為現有的物件新增新的功能,只需將它們包裝在一個新的物件裡面。這類似於你可以用新的包裝紙來包裝一個已經包裝好的禮品盒,想包多少次都可以。每一次包裝都允許你增加你想要的功能,所以在靈活性方面是非常好的。

從技術角度來看,不涉及繼承,所以在設計業務邏輯時有更大的自由。

在下面的例子中,你會看到Decorator模式是如何幫助在一個標準的 Customer 類中新增更多的功能:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function Customer(name, age) {
this.name = name
this.age = age
this.printInfo = function() {
console.log("Customer:\nName : " + this.name + " | Age: " + this.age)
}
}
function DecoratedCustomer(customer, location) {
this.customer = customer
this.name = customer.name
this.age = customer.age
this.location = location
this.printInfo = function() {
console.log("Decorated Customer:\nName: " + this.name + " | Age: " + this.age + " | Location: " + this.location)
}
}
function run() {
let customer = new Customer("John," 25)
customer.printInfo()
// Output:
// Customer:
// Name : John | Age: 25
let decoratedCustomer = new DecoratedCustomer(customer, "FL")
decoratedCustomer.printInfo()
// Output:
// Customer:
// Name : John | Age: 25 | Location: FL
}
run()
function Customer(name, age) { this.name = name this.age = age this.printInfo = function() { console.log("Customer:\nName : " + this.name + " | Age: " + this.age) } } function DecoratedCustomer(customer, location) { this.customer = customer this.name = customer.name this.age = customer.age this.location = location this.printInfo = function() { console.log("Decorated Customer:\nName: " + this.name + " | Age: " + this.age + " | Location: " + this.location) } } function run() { let customer = new Customer("John," 25) customer.printInfo() // Output: // Customer: // Name : John | Age: 25 let decoratedCustomer = new DecoratedCustomer(customer, "FL") decoratedCustomer.printInfo() // Output: // Customer: // Name : John | Age: 25 | Location: FL } run()
function Customer(name, age) {
this.name = name
this.age = age
this.printInfo = function() {
console.log("Customer:\nName : " + this.name + " | Age: " + this.age)
}
}
function DecoratedCustomer(customer, location) {
this.customer = customer
this.name = customer.name
this.age = customer.age
this.location = location
this.printInfo = function() {
console.log("Decorated Customer:\nName: " + this.name + " | Age: " + this.age + " | Location: " + this.location)
}
}
function run() {
let customer = new Customer("John," 25)
customer.printInfo()
// Output:
// Customer:
// Name : John | Age: 25
let decoratedCustomer = new DecoratedCustomer(customer, "FL")
decoratedCustomer.printInfo()
// Output:
// Customer:
// Name : John | Age: 25 | Location: FL
}
run()

這種模式的缺點包括程式碼複雜度高,因為沒有定義標準模式來使用Decorator新增新功能。在軟體開發週期結束時,你可能會有很多不統一的和/或類似的Decorator。

如果你在設計裝飾器時不小心,你可能最終會把一些裝飾器設計成在邏輯上依賴於其他Decorator。如果不解決這個問題,以後刪除或重組裝飾器會對你的應用程式的穩定性造成嚴重的破壞。

10. Facade

在構建大多數現實世界的應用程式時,當你完成時,業務邏輯通常會變得相當複雜。你可能最終會有多個物件和方法參與執行你的應用程式中的核心操作。如果不正確地維護它們的初始化、依賴關係、方法執行的正確順序等,可能會相當棘手,而且容易出錯。

Facade設計模式幫助你在呼叫上述操作的環境與完成這些操作的物件和方法之間建立一個抽象。這個抽象包含了初始化物件的邏輯,跟蹤它們的依賴關係,以及其他重要的活動。呼叫環境沒有關於操作如何被執行的資訊。你可以自由地更新邏輯,而不對呼叫客戶端做任何破壞性的改變。

下面是你如何在一個應用程式中使用它:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
/**
* Let's say you're trying to build an online store. It will have multiple components and
* complex business logic. In the example below, you will find a tiny segment of an online
* store composed together using the Facade design pattern. The various manager and helper
* classes are defined first of all.
*/
function CartManager() {
this.getItems = function() {
// logic to return items
return []
}
this.clearCart = function() {
// logic to clear cart
}
}
function InvoiceManager() {
this.createInvoice = function(items) {
// logic to create invoice
return {}
}
this.notifyCustomerOfFailure = function(invoice) {
// logic to notify customer
}
this.updateInvoicePaymentDetails = function(paymentResult) {
// logic to update invoice after payment attempt
}
}
function PaymentProcessor() {
this.processPayment = function(invoice) {
// logic to initiate and process payment
return {}
}
}
function WarehouseManager() {
this.prepareForShipping = function(items, invoice) {
// logic to prepare the items to be shipped
}
}
// This is where facade comes in. You create an additional interface on top of your
// existing interfaces to define the business logic clearly. This interface exposes
// very simple, high-level methods for the calling environment.
function OnlineStore() {
this.name = "Online Store"
this.placeOrder = function() {
let cartManager = new CartManager()
let items = cartManager.getItems()
let invoiceManager = new InvoiceManager()
let invoice = invoiceManager.createInvoice(items)
let paymentResult = new PaymentProcessor().processPayment(invoice)
invoiceManager.updateInvoicePaymentDetails(paymentResult)
if (paymentResult.status === 'success') {
new WarehouseManager().prepareForShipping(items, invoice)
cartManager.clearCart()
} else {
invoiceManager.notifyCustomerOfFailure(invoice)
}
}
}
// The calling environment is unaware of what goes on when somebody clicks a button to
// place the order. You can easily change the underlying business logic without breaking
// your calling environment.
function run() {
let onlineStore = new OnlineStore()
onlineStore.placeOrder()
}
/** * Let's say you're trying to build an online store. It will have multiple components and * complex business logic. In the example below, you will find a tiny segment of an online * store composed together using the Facade design pattern. The various manager and helper * classes are defined first of all. */ function CartManager() { this.getItems = function() { // logic to return items return [] } this.clearCart = function() { // logic to clear cart } } function InvoiceManager() { this.createInvoice = function(items) { // logic to create invoice return {} } this.notifyCustomerOfFailure = function(invoice) { // logic to notify customer } this.updateInvoicePaymentDetails = function(paymentResult) { // logic to update invoice after payment attempt } } function PaymentProcessor() { this.processPayment = function(invoice) { // logic to initiate and process payment return {} } } function WarehouseManager() { this.prepareForShipping = function(items, invoice) { // logic to prepare the items to be shipped } } // This is where facade comes in. You create an additional interface on top of your // existing interfaces to define the business logic clearly. This interface exposes // very simple, high-level methods for the calling environment. function OnlineStore() { this.name = "Online Store" this.placeOrder = function() { let cartManager = new CartManager() let items = cartManager.getItems() let invoiceManager = new InvoiceManager() let invoice = invoiceManager.createInvoice(items) let paymentResult = new PaymentProcessor().processPayment(invoice) invoiceManager.updateInvoicePaymentDetails(paymentResult) if (paymentResult.status === 'success') { new WarehouseManager().prepareForShipping(items, invoice) cartManager.clearCart() } else { invoiceManager.notifyCustomerOfFailure(invoice) } } } // The calling environment is unaware of what goes on when somebody clicks a button to // place the order. You can easily change the underlying business logic without breaking // your calling environment. function run() { let onlineStore = new OnlineStore() onlineStore.placeOrder() }
/**
* Let's say you're trying to build an online store. It will have multiple components and
* complex business logic. In the example below, you will find a tiny segment of an online
* store composed together using the Facade design pattern. The various manager and helper
* classes are defined first of all.
*/
function CartManager() {
this.getItems = function() {
// logic to return items
return []
}
this.clearCart = function() {
// logic to clear cart
}
}
function InvoiceManager() {
this.createInvoice = function(items) {
// logic to create invoice
return {}
}
this.notifyCustomerOfFailure = function(invoice) {
// logic to notify customer
}
this.updateInvoicePaymentDetails = function(paymentResult) {
// logic to update invoice after payment attempt
}
}
function PaymentProcessor() {
this.processPayment = function(invoice) {
// logic to initiate and process payment
return {}
}
}
function WarehouseManager() {
this.prepareForShipping = function(items, invoice) {
// logic to prepare the items to be shipped
}
}
// This is where facade comes in. You create an additional interface on top of your
// existing interfaces to define the business logic clearly. This interface exposes
// very simple, high-level methods for the calling environment.
function OnlineStore() {
this.name = "Online Store"
this.placeOrder = function() {
let cartManager = new CartManager()
let items = cartManager.getItems()
let invoiceManager = new InvoiceManager()
let invoice = invoiceManager.createInvoice(items)
let paymentResult = new PaymentProcessor().processPayment(invoice)
invoiceManager.updateInvoicePaymentDetails(paymentResult)
if (paymentResult.status === 'success') {
new WarehouseManager().prepareForShipping(items, invoice)
cartManager.clearCart()
} else {
invoiceManager.notifyCustomerOfFailure(invoice)
}
}
}
// The calling environment is unaware of what goes on when somebody clicks a button to
// place the order. You can easily change the underlying business logic without breaking
// your calling environment.
function run() {
let onlineStore = new OnlineStore()
onlineStore.placeOrder()
}

使用Facade模式的一個缺點是,它在你的業務邏輯和客戶端之間增加了一個額外的抽象層,因此需要額外的維護。很多時候,這增加了程式碼庫的整體複雜性。

除此之外,Facade類成為你的應用程式運作的一個強制性依賴–意味著 Facade 類的任何錯誤都會直接影響你的應用程式的運作。

11. Flyweight

Flyweight模式通過幫助你重用物件池中的公共元件,幫助你解決涉及具有重複元件的物件的記憶體效率問題。這有助於減少對記憶體的負載,並導致更快的執行時間。

在下面的例子中,使用Flyweight設計模式將一個大句子儲存在記憶體中。程式並沒有在每個字元出現時進行儲存,而是確定了用於書寫該段落的一組不同的字元及其型別(數字或字母),併為每個字元建立了可重複使用的飛輪,其中包含了儲存哪個字元和型別的細節。

然後,主陣列只需按照在句子中出現的順序儲存對這些飛輪的引用列表,而不是每次出現都儲存一個字元物件的例項。

這樣,句子所佔用的記憶體就減少了一半。請記住,這只是對文字處理程式如何儲存文字的一個非常基本的解釋。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// A simple Character class that stores the value, type, and position of a character
function Character(value, type, position) {
this.value = value
this.type = type
this.position = position
}
// A Flyweight class that stores character value and type combinations
function CharacterFlyweight(value, type) {
this.value = value
this.type = type
}
// A factory to automatically create the flyweights that are not present in the list,
// and also generate a count of the total flyweights in the list
const CharacterFlyweightFactory = (function () {
const flyweights = {}
return {
get: function (value, type) {
if (flyweights[value + type] === undefined)
flyweights[value + type] = new CharacterFlyweight(value, type)
return flyweights[value + type]
},
count: function () {
let count = 0;
for (var f in flyweights) count++;
return count;
}
}
})()
// An enhanced Character class that uses flyweights to store references
// to recurring value and type combinations
function CharacterWithFlyweight(value, type, position) {
this.flyweight = CharacterFlyweightFactory.get(value, type)
this.position = position
}
// A helper function to define the type of a character
// It identifies numbers as N and everything as A (for alphabets)
function getCharacterType(char) {
switch (char) {
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9": return "N"
default:
return "A"
}
}
// A list class to create an array of Characters from a given string
function CharactersList(str) {
chars = []
for (let i = 0; i < str.length; i++) {
const char = str[i]
chars.push(new Character(char, getCharacterType(char), i))
}
return chars
}
// A list class to create an array of CharacterWithFlyweights from a given string
function CharactersWithFlyweightsList(str) {
chars = []
for (let i = 0; i " + charactersList.length)
// Output: Character count -> 656
// The number of flyweights created is only 31, since only 31 characters are used to write the
// entire paragraph. This means that to store 656 characters, a total of
// (31 * 2 + 656 * 1 = 718) memory blocks are used instead of (656 * 3 = 1968) which would have
// used by the standard array.
// (We have assumed each variable to take up one memory block for simplicity. This
// may vary in real-life scenarios)
console.log("Flyweights created -> " + CharacterFlyweightFactory.count())
// Output: Flyweights created -> 31
}
run()
// A simple Character class that stores the value, type, and position of a character function Character(value, type, position) { this.value = value this.type = type this.position = position } // A Flyweight class that stores character value and type combinations function CharacterFlyweight(value, type) { this.value = value this.type = type } // A factory to automatically create the flyweights that are not present in the list, // and also generate a count of the total flyweights in the list const CharacterFlyweightFactory = (function () { const flyweights = {} return { get: function (value, type) { if (flyweights[value + type] === undefined) flyweights[value + type] = new CharacterFlyweight(value, type) return flyweights[value + type] }, count: function () { let count = 0; for (var f in flyweights) count++; return count; } } })() // An enhanced Character class that uses flyweights to store references // to recurring value and type combinations function CharacterWithFlyweight(value, type, position) { this.flyweight = CharacterFlyweightFactory.get(value, type) this.position = position } // A helper function to define the type of a character // It identifies numbers as N and everything as A (for alphabets) function getCharacterType(char) { switch (char) { case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": return "N" default: return "A" } } // A list class to create an array of Characters from a given string function CharactersList(str) { chars = [] for (let i = 0; i < str.length; i++) { const char = str[i] chars.push(new Character(char, getCharacterType(char), i)) } return chars } // A list class to create an array of CharacterWithFlyweights from a given string function CharactersWithFlyweightsList(str) { chars = [] for (let i = 0; i " + charactersList.length) // Output: Character count -> 656 // The number of flyweights created is only 31, since only 31 characters are used to write the // entire paragraph. This means that to store 656 characters, a total of // (31 * 2 + 656 * 1 = 718) memory blocks are used instead of (656 * 3 = 1968) which would have // used by the standard array. // (We have assumed each variable to take up one memory block for simplicity. This // may vary in real-life scenarios) console.log("Flyweights created -> " + CharacterFlyweightFactory.count()) // Output: Flyweights created -> 31 } run()
// A simple Character class that stores the value, type, and position of a character
function Character(value, type, position) {
this.value = value
this.type = type
this.position = position
}
// A Flyweight class that stores character value and type combinations
function CharacterFlyweight(value, type) {
this.value = value
this.type = type
}
// A factory to automatically create the flyweights that are not present in the list,
// and also generate a count of the total flyweights in the list
const CharacterFlyweightFactory = (function () {
const flyweights = {}
return {
get: function (value, type) {
if (flyweights[value + type] === undefined)
flyweights[value + type] = new CharacterFlyweight(value, type)
return flyweights[value + type]
},
count: function () {
let count = 0;
for (var f in flyweights) count++;
return count;
}
}
})()
// An enhanced Character class that uses flyweights to store references
// to recurring value and type combinations
function CharacterWithFlyweight(value, type, position) {
this.flyweight = CharacterFlyweightFactory.get(value, type)
this.position = position
}
// A helper function to define the type of a character
// It identifies numbers as N and everything as A (for alphabets)
function getCharacterType(char) {
switch (char) {
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9": return "N"
default:
return "A"
}
}
// A list class to create an array of Characters from a given string
function CharactersList(str) {
chars = []
for (let i = 0; i < str.length; i++) {
const char = str[i]
chars.push(new Character(char, getCharacterType(char), i))
}
return chars
}
// A list class to create an array of CharacterWithFlyweights from a given string
function CharactersWithFlyweightsList(str) {
chars = []
for (let i = 0; i  " + charactersList.length)
// Output: Character count -> 656
// The number of flyweights created is only 31, since only 31 characters are used to write the
// entire paragraph. This means that to store 656 characters, a total of
// (31 * 2 + 656 * 1 = 718) memory blocks are used instead of (656 * 3 = 1968) which would have
// used by the standard array.
// (We have assumed each variable to take up one memory block for simplicity. This
// may vary in real-life scenarios)
console.log("Flyweights created -> " + CharacterFlyweightFactory.count())
// Output: Flyweights created -> 31
}
run()

你可能已經注意到了,Flyweight模式由於不是特別直觀而增加了你軟體設計的複雜性。因此,如果節省記憶體不是你的應用程式的一個緊迫問題,那麼Flyweight增加的複雜性可能會帶來更多的壞處而不是好處。

此外,Flyweight以記憶體換取處理效率,所以如果你缺少CPU週期,Flyweight對你來說不是一個好的解決方案。

12. Proxy

代理模式幫助你用一個物件替代另一個物件。換句話說,代理物件可以代替實際的物件(它們的代理),並控制對該物件的訪問。這些代理物件可以被用來在呼叫請求傳遞給實際物件之前或之後執行一些動作。

在下面的例子中,你會看到如何通過一個代理來控制對資料庫例項的訪問,該代理在允許請求通過之前對請求進行一些基本的驗證檢查:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function DatabaseHandler() {
const data = {}
this.set = function (key, val) {
data[key] = val;
}
this.get = function (key, val) {
return data[key]
}
this.remove = function (key) {
data[key] = null;
}
}
function DatabaseProxy(databaseInstance) {
this.set = function (key, val) {
if (key === "") {
console.log("Invalid input")
return
}
if (val === undefined) {
console.log("Setting value to undefined not allowed!")
return
}
databaseInstance.set(key, val)
}
this.get = function (key) {
if (databaseInstance.get(key) === null) {
console.log("Element deleted")
}
if (databaseInstance.get(key) === undefined) {
console.log("Element not created")
}
return databaseInstance.get(key)
}
this.remove = function (key) {
if (databaseInstance.get(key) === undefined) {
console.log("Element not added")
return
}
if (databaseInstance.get(key) === null) {
console.log("Element removed already")
return
}
return databaseInstance.remove(key)
}
}
function run() {
let databaseInstance = new DatabaseHandler()
databaseInstance.set("foo," "bar")
databaseInstance.set("foo," undefined)
console.log("#1: " + databaseInstance.get("foo"))
// #1: undefined
console.log("#2: " + databaseInstance.get("baz"))
// #2: undefined
databaseInstance.set("," "something")
databaseInstance.remove("foo")
console.log("#3: " + databaseInstance.get("foo"))
// #3: null
databaseInstance.remove("foo")
databaseInstance.remove("baz")
// Create a fresh database instance to try the same operations
// using the proxy
databaseInstance = new DatabaseHandler()
let proxy = new DatabaseProxy(databaseInstance)
proxy.set("foo," "bar")
proxy.set("foo," undefined)
// Proxy jumps in:
// Output: Setting value to undefined not allowed!
console.log("#1: " + proxy.get("foo"))
// Original value is retained:
// Output: #1: bar
console.log("#2: " + proxy.get("baz"))
// Proxy jumps in again
// Output:
// Element not created
// #2: undefined
proxy.set("," "something")
// Proxy jumps in again
// Output: Invalid input
proxy.remove("foo")
console.log("#3: " + proxy.get("foo"))
// Proxy jumps in again
// Output:
// Element deleted
// #3: null
proxy.remove("foo")
// Proxy output: Element removed already
proxy.remove("baz")
// Proxy output: Element not added
}
run()
function DatabaseHandler() { const data = {} this.set = function (key, val) { data[key] = val; } this.get = function (key, val) { return data[key] } this.remove = function (key) { data[key] = null; } } function DatabaseProxy(databaseInstance) { this.set = function (key, val) { if (key === "") { console.log("Invalid input") return } if (val === undefined) { console.log("Setting value to undefined not allowed!") return } databaseInstance.set(key, val) } this.get = function (key) { if (databaseInstance.get(key) === null) { console.log("Element deleted") } if (databaseInstance.get(key) === undefined) { console.log("Element not created") } return databaseInstance.get(key) } this.remove = function (key) { if (databaseInstance.get(key) === undefined) { console.log("Element not added") return } if (databaseInstance.get(key) === null) { console.log("Element removed already") return } return databaseInstance.remove(key) } } function run() { let databaseInstance = new DatabaseHandler() databaseInstance.set("foo," "bar") databaseInstance.set("foo," undefined) console.log("#1: " + databaseInstance.get("foo")) // #1: undefined console.log("#2: " + databaseInstance.get("baz")) // #2: undefined databaseInstance.set("," "something") databaseInstance.remove("foo") console.log("#3: " + databaseInstance.get("foo")) // #3: null databaseInstance.remove("foo") databaseInstance.remove("baz") // Create a fresh database instance to try the same operations // using the proxy databaseInstance = new DatabaseHandler() let proxy = new DatabaseProxy(databaseInstance) proxy.set("foo," "bar") proxy.set("foo," undefined) // Proxy jumps in: // Output: Setting value to undefined not allowed! console.log("#1: " + proxy.get("foo")) // Original value is retained: // Output: #1: bar console.log("#2: " + proxy.get("baz")) // Proxy jumps in again // Output: // Element not created // #2: undefined proxy.set("," "something") // Proxy jumps in again // Output: Invalid input proxy.remove("foo") console.log("#3: " + proxy.get("foo")) // Proxy jumps in again // Output: // Element deleted // #3: null proxy.remove("foo") // Proxy output: Element removed already proxy.remove("baz") // Proxy output: Element not added } run()
function DatabaseHandler() {
const data = {}
this.set = function (key, val) {
data[key] = val;
}
this.get = function (key, val) {
return data[key]
}
this.remove = function (key) {
data[key] = null;
}
}
function DatabaseProxy(databaseInstance) {
this.set = function (key, val) {
if (key === "") {
console.log("Invalid input")
return
}
if (val === undefined) {
console.log("Setting value to undefined not allowed!")
return
}
databaseInstance.set(key, val)
}
this.get = function (key) {
if (databaseInstance.get(key) === null) {
console.log("Element deleted")
}
if (databaseInstance.get(key) === undefined) {
console.log("Element not created")
}
return databaseInstance.get(key)
}
this.remove = function (key) {
if (databaseInstance.get(key) === undefined) {
console.log("Element not added")
return
}
if (databaseInstance.get(key) === null) {
console.log("Element removed already")
return
}
return databaseInstance.remove(key)
}
}
function run() {
let databaseInstance = new DatabaseHandler()
databaseInstance.set("foo," "bar")
databaseInstance.set("foo," undefined)
console.log("#1: " + databaseInstance.get("foo"))
// #1: undefined
console.log("#2: " + databaseInstance.get("baz"))
// #2: undefined
databaseInstance.set("," "something")
databaseInstance.remove("foo")
console.log("#3: " + databaseInstance.get("foo"))
// #3: null
databaseInstance.remove("foo")
databaseInstance.remove("baz")
// Create a fresh database instance to try the same operations
// using the proxy
databaseInstance = new DatabaseHandler()
let proxy = new DatabaseProxy(databaseInstance)
proxy.set("foo," "bar")
proxy.set("foo," undefined)
// Proxy jumps in:
// Output: Setting value to undefined not allowed!
console.log("#1: " + proxy.get("foo"))
// Original value is retained:
// Output: #1: bar
console.log("#2: " + proxy.get("baz"))
// Proxy jumps in again
// Output:
// Element not created
// #2: undefined
proxy.set("," "something")
// Proxy jumps in again
// Output: Invalid input
proxy.remove("foo")
console.log("#3: " + proxy.get("foo"))
// Proxy jumps in again
// Output:
// Element deleted
// #3: null
proxy.remove("foo")
// Proxy output: Element removed already
proxy.remove("baz")
// Proxy output: Element not added
}
run()

這種設計模式在整個行業中普遍使用,有助於輕鬆實現執行前和執行後的操作。然而,就像其他設計模式一樣,它也會增加你程式碼庫的複雜性,所以如果你不是真的需要它,儘量不要使用它。

你也要記住,由於在呼叫你的實際物件時涉及到一個額外的物件,由於增加了處理操作,可能會有一些延遲。現在優化你的主物件的效能也涉及到優化你的代理方法的效能。

行為設計模式

行為設計模式幫助你解決圍繞物件之間如何互動的問題。這可能涉及到在物件之間分享或傳遞責任/控制,以完成集合操作。它還可以涉及到以最有效的方式在多個物件之間傳遞/共享資料。

13. Chain of Responsibility

責任鏈模式(Chain of Responsibility)是最簡單的行為設計模式之一。當你為可以由多個處理程式處理的操作設計邏輯時,它就會派上用場。

類似於問題升級在支援團隊中的工作方式,控制通過一個處理程式鏈,負責採取行動的處理程式完成操作。這種設計模式經常被用於UI設計中,多層元件可以處理一個使用者輸入事件,如觸控或滑動。

下面你將看到一個使用責任鏈模式的投訴升級的例子。該投訴將由處理者根據其嚴重程度進行處理:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Complaint class that stores title and severity of a complaint
// Higher value of severity indicates a more severe complaint
function Complaint (title, severity) {
this.title = title
this.severity = severity
}
// Base level handler that receives all complaints
function Representative () {
// If this handler can not handle the complaint, it will be forwarded to the next level
this.nextLevel = new Management()
this.handleComplaint = function (complaint) {
if (complaint.severity === 0)
console.log("Representative resolved the following complaint: " + complaint.title)
else
this.nextLevel.handleComplaint(complaint)
}
}
// Second level handler to handle complaints of severity 1
function Management() {
// If this handler can not handle the complaint, it will be forwarded to the next level
this.nextLevel = new Leadership()
this.handleComplaint = function (complaint) {
if (complaint.severity === 1)
console.log("Management resolved the following complaint: " + complaint.title)
else
this.nextLevel.handleComplaint(complaint)
}
}
// Highest level handler that handles all complaints unhandled so far
function Leadership() {
this.handleComplaint = function (complaint) {
console.log("Leadership resolved the following complaint: " + complaint.title)
}
}
function run() {
// Create an instance of the base level handler
let customerSupport = new Representative()
// Create multiple complaints of varying severity and pass them to the base handler
let complaint1 = new Complaint("Submit button doesn't work," 0)
customerSupport.handleComplaint(complaint1)
// Output: Representative resolved the following complaint: Submit button doesn't work
let complaint2 = new Complaint("Payment failed," 1)
customerSupport.handleComplaint(complaint2)
// Output: Management resolved the following complaint: Payment failed
let complaint3 = new Complaint("Employee misdemeanour," 2)
customerSupport.handleComplaint(complaint3)
// Output: Leadership resolved the following complaint: Employee misdemeanour
}
run()
// Complaint class that stores title and severity of a complaint // Higher value of severity indicates a more severe complaint function Complaint (title, severity) { this.title = title this.severity = severity } // Base level handler that receives all complaints function Representative () { // If this handler can not handle the complaint, it will be forwarded to the next level this.nextLevel = new Management() this.handleComplaint = function (complaint) { if (complaint.severity === 0) console.log("Representative resolved the following complaint: " + complaint.title) else this.nextLevel.handleComplaint(complaint) } } // Second level handler to handle complaints of severity 1 function Management() { // If this handler can not handle the complaint, it will be forwarded to the next level this.nextLevel = new Leadership() this.handleComplaint = function (complaint) { if (complaint.severity === 1) console.log("Management resolved the following complaint: " + complaint.title) else this.nextLevel.handleComplaint(complaint) } } // Highest level handler that handles all complaints unhandled so far function Leadership() { this.handleComplaint = function (complaint) { console.log("Leadership resolved the following complaint: " + complaint.title) } } function run() { // Create an instance of the base level handler let customerSupport = new Representative() // Create multiple complaints of varying severity and pass them to the base handler let complaint1 = new Complaint("Submit button doesn't work," 0) customerSupport.handleComplaint(complaint1) // Output: Representative resolved the following complaint: Submit button doesn't work let complaint2 = new Complaint("Payment failed," 1) customerSupport.handleComplaint(complaint2) // Output: Management resolved the following complaint: Payment failed let complaint3 = new Complaint("Employee misdemeanour," 2) customerSupport.handleComplaint(complaint3) // Output: Leadership resolved the following complaint: Employee misdemeanour } run()
// Complaint class that stores title and severity of a complaint
// Higher value of severity indicates a more severe complaint
function Complaint (title, severity) {
this.title = title
this.severity = severity
}
// Base level handler that receives all complaints
function Representative () {
// If this handler can not handle the complaint, it will be forwarded to the next level
this.nextLevel = new Management()
this.handleComplaint = function (complaint) {
if (complaint.severity === 0)
console.log("Representative resolved the following complaint: " + complaint.title)
else
this.nextLevel.handleComplaint(complaint)
}
}
// Second level handler to handle complaints of severity 1
function Management() {
// If this handler can not handle the complaint, it will be forwarded to the next level
this.nextLevel = new Leadership()
this.handleComplaint = function (complaint) {
if (complaint.severity === 1)
console.log("Management resolved the following complaint: " + complaint.title)
else
this.nextLevel.handleComplaint(complaint)
}
}
// Highest level handler that handles all complaints unhandled so far
function Leadership() {
this.handleComplaint = function (complaint) {
console.log("Leadership resolved the following complaint: " + complaint.title)
}
}
function run() {
// Create an instance of the base level handler
let customerSupport = new Representative()
// Create multiple complaints of varying severity and pass them to the base handler
let complaint1 = new Complaint("Submit button doesn't work," 0)
customerSupport.handleComplaint(complaint1)
// Output: Representative resolved the following complaint: Submit button doesn't work
let complaint2 = new Complaint("Payment failed," 1)
customerSupport.handleComplaint(complaint2)
// Output: Management resolved the following complaint: Payment failed
let complaint3 = new Complaint("Employee misdemeanour," 2)
customerSupport.handleComplaint(complaint3)
// Output: Leadership resolved the following complaint: Employee misdemeanour
}
run()

這種設計的明顯問題是它是線性的,所以當大量的處理程式被連鎖在一起時,處理一個操作會有一些延遲。

追蹤所有的處理程式可能是另一個痛點,因為在處理程式達到一定數量後會變得相當混亂。除錯是另一個噩夢,因為每個請求都可能在不同的處理程式上結束,這使得你很難對日誌和除錯過程進行標準化。

14. Iterator

迭代器模式非常簡單,幾乎在所有的現代面嚮物件語言中都非常常用。如果你發現自己面臨的任務是瀏覽一個型別不盡相同的物件的列表,那麼普通的迭代方法,如for迴圈,可能會變得相當混亂–尤其是當你還在裡面編寫業務邏輯時。

迭代器模式可以幫助你將列表的迭代和處理邏輯與主要業務邏輯隔離開來。

下面是你如何在一個有多種型別元素的相當基本的列表中使用它:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Iterator for a complex list with custom methods
function Iterator(list) {
this.list = list
this.index = 0
// Fetch the current element
this.current = function() {
return this.list[this.index]
}
// Fetch the next element in the list
this.next = function() {
return this.list[this.index++]
}
// Check if there is another element in the list
this.hasNext = function() {
return this.index < this.list.length
}
// Reset the index to point to the initial element
this.resetIndex = function() {
this.index = 0
}
// Run a forEach loop over the list
this.forEach = function(callback) {
for (let element = this.next(); this.index <= this.list.length; element = this.next()) {
callback(element)
}
}
}
function run() {
// A complex list with elements of multiple data types
let list = ["Lorem ipsum," 9, ["lorem ipsum dolor," true], false]
// Create an instance of the iterator and pass it the list
let iterator = new Iterator(list)
// Log the first element
console.log(iterator.current())
// Output: Lorem ipsum
// Print all elements of the list using the iterator's methods
while (iterator.hasNext()) {
console.log(iterator.next())
/**
* Output:
* Lorem ipsum
* 9
* [ 'lorem ipsum dolor', true ]
* false
*/
}
// Reset the iterator's index to the first element
iterator.resetIndex()
// Use the custom iterator to pass an effect that will run for each element of the list
iterator.forEach(function (element) {
console.log(element)
})
/**
* Output:
* Lorem ipsum
* 9
* [ 'lorem ipsum dolor', true ]
* false
*/
}
run()
// Iterator for a complex list with custom methods function Iterator(list) { this.list = list this.index = 0 // Fetch the current element this.current = function() { return this.list[this.index] } // Fetch the next element in the list this.next = function() { return this.list[this.index++] } // Check if there is another element in the list this.hasNext = function() { return this.index < this.list.length } // Reset the index to point to the initial element this.resetIndex = function() { this.index = 0 } // Run a forEach loop over the list this.forEach = function(callback) { for (let element = this.next(); this.index <= this.list.length; element = this.next()) { callback(element) } } } function run() { // A complex list with elements of multiple data types let list = ["Lorem ipsum," 9, ["lorem ipsum dolor," true], false] // Create an instance of the iterator and pass it the list let iterator = new Iterator(list) // Log the first element console.log(iterator.current()) // Output: Lorem ipsum // Print all elements of the list using the iterator's methods while (iterator.hasNext()) { console.log(iterator.next()) /** * Output: * Lorem ipsum * 9 * [ 'lorem ipsum dolor', true ] * false */ } // Reset the iterator's index to the first element iterator.resetIndex() // Use the custom iterator to pass an effect that will run for each element of the list iterator.forEach(function (element) { console.log(element) }) /** * Output: * Lorem ipsum * 9 * [ 'lorem ipsum dolor', true ] * false */ } run()
// Iterator for a complex list with custom methods
function Iterator(list) {
this.list = list
this.index = 0
// Fetch the current element
this.current = function() {
return this.list[this.index]
}
// Fetch the next element in the list
this.next = function() {
return this.list[this.index++]
}
// Check if there is another element in the list
this.hasNext = function() {
return this.index < this.list.length
}
// Reset the index to point to the initial element
this.resetIndex = function() {
this.index = 0
}
// Run a forEach loop over the list
this.forEach = function(callback) {
for (let element = this.next(); this.index <= this.list.length; element = this.next()) {
callback(element)
}
}
}
function run() {
// A complex list with elements of multiple data types
let list = ["Lorem ipsum," 9, ["lorem ipsum dolor," true], false]
// Create an instance of the iterator and pass it the list
let iterator = new Iterator(list)
// Log the first element
console.log(iterator.current())
// Output: Lorem ipsum
// Print all elements of the list using the iterator's methods
while (iterator.hasNext()) {
console.log(iterator.next())
/**
* Output:
* Lorem ipsum
* 9
* [ 'lorem ipsum dolor', true ]
* false
*/
}
// Reset the iterator's index to the first element
iterator.resetIndex()
// Use the custom iterator to pass an effect that will run for each element of the list
iterator.forEach(function (element) {
console.log(element)
})
/**
* Output:
* Lorem ipsum
* 9
* [ 'lorem ipsum dolor', true ]
* false
*/
}
run()

不用說,這種模式對於沒有多種型別元素的列表來說可能是不必要的複雜。另外,如果一個列表中的元素型別太多,也會變得難以管理。

關鍵是要根據你的列表和它未來變化的可能性來確定你是否真的需要一個迭代器。更重要的是,迭代器模式只在列表中有用,而且列表有時會限制你的線性訪問模式。其他資料結構有時可以給你帶來更大的效能優勢。

15. Mediator

你的應用設計有時可能需要你與大量不同的物件打交道,這些物件容納了各種業務邏輯,並經常相互依賴。處理這些依賴關係有時會變得很棘手,因為你需要跟蹤這些物件之間如何交換資料和控制。

調解器設計模式的目的是幫助你解決這個問題,它將這些物件的互動邏輯隔離到一個單獨的物件中。

這個單獨的物件被稱為調解器,它負責讓你的低層類完成工作。你的客戶端或呼叫環境也將與調解器而不是低階別的類進行互動。

下面是一個關於調解器設計模式的例項:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Writer class that receives an assignment, writes it in 2 seconds, and marks it as finished
function Writer(name, manager) {
// Reference to the manager, writer's name, and a busy flag that the manager uses while assigning the article
this.manager = manager
this.name = name
this.busy = false
this.startWriting = function (assignment) {
console.log(this.name + " started writing \"" + assignment + "\"")
this.assignment = assignment
this.busy = true
// 2 s timer to replicate manual action
setTimeout(() => { this.finishWriting() }, 2000)
}
this.finishWriting = function () {
if (this.busy === true) {
console.log(this.name + " finished writing \"" + this.assignment + "\"")
this.busy = false
return this.manager.notifyWritingComplete(this.assignment)
} else {
console.log(this.name + " is not writing any article")
}
}
}
// Editor class that receives an assignment, edits it in 3 seconds, and marks it as finished
function Editor(name, manager) {
// Reference to the manager, writer's name, and a busy flag that the manager uses while assigning the article
this.manager = manager
this.name = name
this.busy = false
this.startEditing = function (assignment) {
console.log(this.name + " started editing \"" + assignment + "\"")
this.assignment = assignment
this.busy = true
// 3 s timer to replicate manual action
setTimeout(() => { this.finishEditing() }, 3000)
}
this.finishEditing = function () {
if (this.busy === true) {
console.log(this.name + " finished editing \"" + this.assignment + "\"")
this.manager.notifyEditingComplete(this.assignment)
this.busy = false
} else {
console.log(this.name + " is not editing any article")
}
}
}
// The mediator class
function Manager() {
// Store arrays of workers
this.editors = []
this.writers = []
this.setEditors = function (editors) {
this.editors = editors
}
this.setWriters = function (writers) {
this.writers = writers
}
// Manager receives new assignments via this method
this.notifyNewAssignment = function (assignment) {
let availableWriter = this.writers.find(function (writer) {
return writer.busy === false
})
availableWriter.startWriting(assignment)
return availableWriter
}
// Writers call this method to notify they're done writing
this.notifyWritingComplete = function (assignment) {
let availableEditor = this.editors.find(function (editor) {
return editor.busy === false
})
availableEditor.startEditing(assignment)
return availableEditor
}
// Editors call this method to notify they're done editing
this.notifyEditingComplete = function (assignment) {
console.log("\"" + assignment + "\" is ready to publish")
}
}
function run() {
// Create a manager
let manager = new Manager()
// Create workers
let editors = [
new Editor("Ed," manager),
new Editor("Phil," manager),
]
let writers = [
new Writer("Michael," manager),
new Writer("Rick," manager),
]
// Attach workers to manager
manager.setEditors(editors)
manager.setWriters(writers)
// Send two assignments to manager
manager.notifyNewAssignment("var vs let in JavaScript")
manager.notifyNewAssignment("JS promises")
/**
* Output:
* Michael started writing "var vs let in JavaScript"
* Rick started writing "JS promises"
*
* After 2s, output:
* Michael finished writing "var vs let in JavaScript"
* Ed started editing "var vs let in JavaScript"
* Rick finished writing "JS promises"
* Phil started editing "JS promises"
*
* After 3s, output:
* Ed finished editing "var vs let in JavaScript"
* "var vs let in JavaScript" is ready to publish
* Phil finished editing "JS promises"
* "JS promises" is ready to publish
*/
}
run()
// Writer class that receives an assignment, writes it in 2 seconds, and marks it as finished function Writer(name, manager) { // Reference to the manager, writer's name, and a busy flag that the manager uses while assigning the article this.manager = manager this.name = name this.busy = false this.startWriting = function (assignment) { console.log(this.name + " started writing \"" + assignment + "\"") this.assignment = assignment this.busy = true // 2 s timer to replicate manual action setTimeout(() => { this.finishWriting() }, 2000) } this.finishWriting = function () { if (this.busy === true) { console.log(this.name + " finished writing \"" + this.assignment + "\"") this.busy = false return this.manager.notifyWritingComplete(this.assignment) } else { console.log(this.name + " is not writing any article") } } } // Editor class that receives an assignment, edits it in 3 seconds, and marks it as finished function Editor(name, manager) { // Reference to the manager, writer's name, and a busy flag that the manager uses while assigning the article this.manager = manager this.name = name this.busy = false this.startEditing = function (assignment) { console.log(this.name + " started editing \"" + assignment + "\"") this.assignment = assignment this.busy = true // 3 s timer to replicate manual action setTimeout(() => { this.finishEditing() }, 3000) } this.finishEditing = function () { if (this.busy === true) { console.log(this.name + " finished editing \"" + this.assignment + "\"") this.manager.notifyEditingComplete(this.assignment) this.busy = false } else { console.log(this.name + " is not editing any article") } } } // The mediator class function Manager() { // Store arrays of workers this.editors = [] this.writers = [] this.setEditors = function (editors) { this.editors = editors } this.setWriters = function (writers) { this.writers = writers } // Manager receives new assignments via this method this.notifyNewAssignment = function (assignment) { let availableWriter = this.writers.find(function (writer) { return writer.busy === false }) availableWriter.startWriting(assignment) return availableWriter } // Writers call this method to notify they're done writing this.notifyWritingComplete = function (assignment) { let availableEditor = this.editors.find(function (editor) { return editor.busy === false }) availableEditor.startEditing(assignment) return availableEditor } // Editors call this method to notify they're done editing this.notifyEditingComplete = function (assignment) { console.log("\"" + assignment + "\" is ready to publish") } } function run() { // Create a manager let manager = new Manager() // Create workers let editors = [ new Editor("Ed," manager), new Editor("Phil," manager), ] let writers = [ new Writer("Michael," manager), new Writer("Rick," manager), ] // Attach workers to manager manager.setEditors(editors) manager.setWriters(writers) // Send two assignments to manager manager.notifyNewAssignment("var vs let in JavaScript") manager.notifyNewAssignment("JS promises") /** * Output: * Michael started writing "var vs let in JavaScript" * Rick started writing "JS promises" * * After 2s, output: * Michael finished writing "var vs let in JavaScript" * Ed started editing "var vs let in JavaScript" * Rick finished writing "JS promises" * Phil started editing "JS promises" * * After 3s, output: * Ed finished editing "var vs let in JavaScript" * "var vs let in JavaScript" is ready to publish * Phil finished editing "JS promises" * "JS promises" is ready to publish */ } run()
// Writer class that receives an assignment, writes it in 2 seconds, and marks it as finished
function Writer(name, manager) {
// Reference to the manager, writer's name, and a busy flag that the manager uses while assigning the article
this.manager = manager
this.name = name
this.busy = false
this.startWriting = function (assignment) {
console.log(this.name + " started writing \"" + assignment + "\"")
this.assignment = assignment
this.busy = true
// 2 s timer to replicate manual action
setTimeout(() => { this.finishWriting() }, 2000)
}
this.finishWriting = function () {
if (this.busy === true) {
console.log(this.name + " finished writing \"" + this.assignment + "\"")
this.busy = false
return this.manager.notifyWritingComplete(this.assignment)
} else {
console.log(this.name + " is not writing any article")
}
}
}
// Editor class that receives an assignment, edits it in 3 seconds, and marks it as finished
function Editor(name, manager) {
// Reference to the manager, writer's name, and a busy flag that the manager uses while assigning the article
this.manager = manager
this.name = name
this.busy = false
this.startEditing = function (assignment) {
console.log(this.name + " started editing \"" + assignment + "\"")
this.assignment = assignment
this.busy = true
// 3 s timer to replicate manual action
setTimeout(() => { this.finishEditing() }, 3000)
}
this.finishEditing = function () {
if (this.busy === true) {
console.log(this.name + " finished editing \"" + this.assignment + "\"")
this.manager.notifyEditingComplete(this.assignment)
this.busy = false
} else {
console.log(this.name + " is not editing any article")
}
}
}
// The mediator class
function Manager() {
// Store arrays of workers
this.editors = []
this.writers = []
this.setEditors = function (editors) {
this.editors = editors
}
this.setWriters = function (writers) {
this.writers = writers
}
// Manager receives new assignments via this method
this.notifyNewAssignment = function (assignment) {
let availableWriter = this.writers.find(function (writer) {
return writer.busy === false
})
availableWriter.startWriting(assignment)
return availableWriter
}
// Writers call this method to notify they're done writing
this.notifyWritingComplete = function (assignment) {
let availableEditor = this.editors.find(function (editor) {
return editor.busy === false
})
availableEditor.startEditing(assignment)
return availableEditor
}
// Editors call this method to notify they're done editing
this.notifyEditingComplete = function (assignment) {
console.log("\"" + assignment + "\" is ready to publish")
}
}
function run() {
// Create a manager
let manager = new Manager()
// Create workers
let editors = [
new Editor("Ed," manager),
new Editor("Phil," manager),
]
let writers = [
new Writer("Michael," manager),
new Writer("Rick," manager),
]
// Attach workers to manager
manager.setEditors(editors)
manager.setWriters(writers)
// Send two assignments to manager
manager.notifyNewAssignment("var vs let in JavaScript")
manager.notifyNewAssignment("JS promises")
/**
* Output:
* Michael started writing "var vs let in JavaScript"
* Rick started writing "JS promises"
* 
* After 2s, output:
* Michael finished writing "var vs let in JavaScript"
* Ed started editing "var vs let in JavaScript"
* Rick finished writing "JS promises"
* Phil started editing "JS promises"
*
* After 3s, output:
* Ed finished editing "var vs let in JavaScript"
* "var vs let in JavaScript" is ready to publish
* Phil finished editing "JS promises"
* "JS promises" is ready to publish
*/
}
run()

雖然調解器為你的應用設計提供瞭解耦和大量的靈活性,但在最後,它是另一個你需要維護的類。在編寫調解器之前,你必須評估你的設計是否真的能從調解器中受益,這樣你就不會最終給你的程式碼庫增加不必要的複雜性。

同樣重要的是要記住,即使調解器類不包含任何直接的業務邏輯,它仍然包含很多對你的應用程式的運作至關重要的程式碼,因此可以很快變得相當複雜。

16. Memento

版本控制物件是你在開發應用程式時面臨的另一個常見問題。有很多用例,你需要維護一個物件的歷史,支援簡單的回滾,有時甚至支援恢復這些回滾。為這樣的應用編寫邏輯可能很困難。

Memento設計模式就是為了輕鬆解決這個問題。

紀念品被認為是一個物件在某個時間點的快照。Memento設計模式利用這些紀念品來儲存物件的快照,因為它是隨時間變化的。當你需要回滾到一箇舊版本時,你可以簡單地調出它的紀念品。

下面是你如何在一個文字處理應用程式中實現它:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// The memento class that can hold one snapshot of the Originator class - document
function Text(contents) {
// Contents of the document
this.contents = contents
// Accessor function for contents
this.getContents = function () {
return this.contents
}
// Helper function to calculate word count for the current document
this.getWordCount = function () {
return this.contents.length
}
}
// The originator class that holds the latest version of the document
function Document(contents) {
// Holder for the memento, i.e., the text of the document
this.text = new Text(contents)
// Function to save new contents as a memento
this.save = function (contents) {
this.text = new Text(contents)
return this.text
}
// Function to revert to an older version of the text using a memento
this.restore = function (text) {
this.text = new Text(text.getContents())
}
// Helper function to get the current memento
this.getText = function () {
return this.text
}
// Helper function to get the word count of the current document
this.getWordCount = function () {
return this.text.getWordCount()
}
}
// The caretaker class that providers helper functions to modify the document
function DocumentManager(document) {
// Holder for the originator, i.e., the document
this.document = document
// Array to maintain a list of mementos
this.history = []
// Add the initial state of the document as the first version of the document
this.history.push(document.getText())
// Helper function to get the current contents of the documents
this.getContents = function () {
return this.document.getText().getContents()
}
// Helper function to get the total number of versions available for the document
this.getVersionCount = function () {
return this.history.length
}
// Helper function to get the complete history of the document
this.getHistory = function () {
return this.history.map(function (element) {
return element.getContents()
})
}
// Function to overwrite the contents of the document
this.overwrite = function (contents) {
let newVersion = this.document.save(contents)
this.history.push(newVersion)
}
// Function to append new content to the existing contents of the document
this.append = function (contents) {
let currentVersion = this.history[this.history.length - 1]
let newVersion
if (currentVersion === undefined)
newVersion = this.document.save(contents)
else
newVersion = this.document.save(currentVersion.getContents() + contents)
this.history.push(newVersion)
}
// Function to delete all the contents of the document
this.delete = function () {
this.history.push(this.document.save(""))
}
// Function to get a particular version of the document
this.getVersion = function (versionNumber) {
return this.history[versionNumber - 1]
}
// Function to undo the last change
this.undo = function () {
let previousVersion = this.history[this.history.length - 2]
this.document.restore(previousVersion)
this.history.push(previousVersion)
}
// Function to revert the document to a previous version
this.revertToVersion = function (version) {
let previousVersion = this.history[version - 1]
this.document.restore(previousVersion)
this.history.push(previousVersion)
}
// Helper function to get the total word count of the document
this.getWordCount = function () {
return this.document.getWordCount()
}
}
function run() {
// Create a document
let blogPost = new Document("")
// Create a caretaker for the document
let blogPostManager = new DocumentManager(blogPost)
// Change #1: Add some text
blogPostManager.append("Hello World!")
console.log(blogPostManager.getContents())
// Output: Hello World!
// Change #2: Add some more text
blogPostManager.append(" This is the second entry in the document")
console.log(blogPostManager.getContents())
// Output: Hello World! This is the second entry in the document
// Change #3: Overwrite the document with some new text
blogPostManager.overwrite("This entry overwrites everything in the document")
console.log(blogPostManager.getContents())
// Output: This entry overwrites everything in the document
// Change #4: Delete the contents of the document
blogPostManager.delete()
console.log(blogPostManager.getContents())
// Empty output
// Get an old version of the document
console.log(blogPostManager.getVersion(2).getContents())
// Output: Hello World!
// Change #5: Go back to an old version of the document
blogPostManager.revertToVersion(3)
console.log(blogPostManager.getContents())
// Output: Hello World! This is the second entry in the document
// Get the word count of the current document
console.log(blogPostManager.getWordCount())
// Output: 53
// Change #6: Undo the last change
blogPostManager.undo()
console.log(blogPostManager.getContents())
// Empty output
// Get the total number of versions for the document
console.log(blogPostManager.getVersionCount())
// Output: 7
// Get the complete history of the document
console.log(blogPostManager.getHistory())
/**
* Output:
* [
* '',
* 'Hello World!',
* 'Hello World! This is the second entry in the document',
* 'This entry overwrites everything in the document',
* '',
* 'Hello World! This is the second entry in the document',
* ''
* ]
*/
}
run()
// The memento class that can hold one snapshot of the Originator class - document function Text(contents) { // Contents of the document this.contents = contents // Accessor function for contents this.getContents = function () { return this.contents } // Helper function to calculate word count for the current document this.getWordCount = function () { return this.contents.length } } // The originator class that holds the latest version of the document function Document(contents) { // Holder for the memento, i.e., the text of the document this.text = new Text(contents) // Function to save new contents as a memento this.save = function (contents) { this.text = new Text(contents) return this.text } // Function to revert to an older version of the text using a memento this.restore = function (text) { this.text = new Text(text.getContents()) } // Helper function to get the current memento this.getText = function () { return this.text } // Helper function to get the word count of the current document this.getWordCount = function () { return this.text.getWordCount() } } // The caretaker class that providers helper functions to modify the document function DocumentManager(document) { // Holder for the originator, i.e., the document this.document = document // Array to maintain a list of mementos this.history = [] // Add the initial state of the document as the first version of the document this.history.push(document.getText()) // Helper function to get the current contents of the documents this.getContents = function () { return this.document.getText().getContents() } // Helper function to get the total number of versions available for the document this.getVersionCount = function () { return this.history.length } // Helper function to get the complete history of the document this.getHistory = function () { return this.history.map(function (element) { return element.getContents() }) } // Function to overwrite the contents of the document this.overwrite = function (contents) { let newVersion = this.document.save(contents) this.history.push(newVersion) } // Function to append new content to the existing contents of the document this.append = function (contents) { let currentVersion = this.history[this.history.length - 1] let newVersion if (currentVersion === undefined) newVersion = this.document.save(contents) else newVersion = this.document.save(currentVersion.getContents() + contents) this.history.push(newVersion) } // Function to delete all the contents of the document this.delete = function () { this.history.push(this.document.save("")) } // Function to get a particular version of the document this.getVersion = function (versionNumber) { return this.history[versionNumber - 1] } // Function to undo the last change this.undo = function () { let previousVersion = this.history[this.history.length - 2] this.document.restore(previousVersion) this.history.push(previousVersion) } // Function to revert the document to a previous version this.revertToVersion = function (version) { let previousVersion = this.history[version - 1] this.document.restore(previousVersion) this.history.push(previousVersion) } // Helper function to get the total word count of the document this.getWordCount = function () { return this.document.getWordCount() } } function run() { // Create a document let blogPost = new Document("") // Create a caretaker for the document let blogPostManager = new DocumentManager(blogPost) // Change #1: Add some text blogPostManager.append("Hello World!") console.log(blogPostManager.getContents()) // Output: Hello World! // Change #2: Add some more text blogPostManager.append(" This is the second entry in the document") console.log(blogPostManager.getContents()) // Output: Hello World! This is the second entry in the document // Change #3: Overwrite the document with some new text blogPostManager.overwrite("This entry overwrites everything in the document") console.log(blogPostManager.getContents()) // Output: This entry overwrites everything in the document // Change #4: Delete the contents of the document blogPostManager.delete() console.log(blogPostManager.getContents()) // Empty output // Get an old version of the document console.log(blogPostManager.getVersion(2).getContents()) // Output: Hello World! // Change #5: Go back to an old version of the document blogPostManager.revertToVersion(3) console.log(blogPostManager.getContents()) // Output: Hello World! This is the second entry in the document // Get the word count of the current document console.log(blogPostManager.getWordCount()) // Output: 53 // Change #6: Undo the last change blogPostManager.undo() console.log(blogPostManager.getContents()) // Empty output // Get the total number of versions for the document console.log(blogPostManager.getVersionCount()) // Output: 7 // Get the complete history of the document console.log(blogPostManager.getHistory()) /** * Output: * [ * '', * 'Hello World!', * 'Hello World! This is the second entry in the document', * 'This entry overwrites everything in the document', * '', * 'Hello World! This is the second entry in the document', * '' * ] */ } run()
// The memento class that can hold one snapshot of the Originator class - document
function Text(contents) {
// Contents of the document
this.contents = contents
// Accessor function for contents
this.getContents = function () {
return this.contents
}
// Helper function to calculate word count for the current document
this.getWordCount = function () {
return this.contents.length
}
}
// The originator class that holds the latest version of the document
function Document(contents) {
// Holder for the memento, i.e., the text of the document
this.text = new Text(contents)
// Function to save new contents as a memento
this.save = function (contents) {
this.text = new Text(contents)
return this.text
}
// Function to revert to an older version of the text using a memento
this.restore = function (text) {
this.text = new Text(text.getContents())
}
// Helper function to get the current memento
this.getText = function () {
return this.text
}
// Helper function to get the word count of the current document
this.getWordCount = function () {
return this.text.getWordCount()
}
}
// The caretaker class that providers helper functions to modify the document
function DocumentManager(document) {
// Holder for the originator, i.e., the document
this.document = document
// Array to maintain a list of mementos
this.history = []
// Add the initial state of the document as the first version of the document
this.history.push(document.getText())
// Helper function to get the current contents of the documents
this.getContents = function () {
return this.document.getText().getContents()
}
// Helper function to get the total number of versions available for the document
this.getVersionCount = function () {
return this.history.length
}
// Helper function to get the complete history of the document
this.getHistory = function () {
return this.history.map(function (element) {
return element.getContents()
})
}
// Function to overwrite the contents of the document
this.overwrite = function (contents) {
let newVersion = this.document.save(contents)
this.history.push(newVersion)
}
// Function to append new content to the existing contents of the document
this.append = function (contents) {
let currentVersion = this.history[this.history.length - 1]
let newVersion
if (currentVersion === undefined)
newVersion = this.document.save(contents)
else
newVersion = this.document.save(currentVersion.getContents() + contents)
this.history.push(newVersion)
}
// Function to delete all the contents of the document
this.delete = function () {
this.history.push(this.document.save(""))
}
// Function to get a particular version of the document
this.getVersion = function (versionNumber) {
return this.history[versionNumber - 1]
}
// Function to undo the last change
this.undo = function () {
let previousVersion = this.history[this.history.length - 2]
this.document.restore(previousVersion)
this.history.push(previousVersion)
}
// Function to revert the document to a previous version
this.revertToVersion = function (version) {
let previousVersion = this.history[version - 1]
this.document.restore(previousVersion)
this.history.push(previousVersion)
}
// Helper function to get the total word count of the document
this.getWordCount = function () {
return this.document.getWordCount()
}
}
function run() {
// Create a document
let blogPost = new Document("")
// Create a caretaker for the document
let blogPostManager = new DocumentManager(blogPost)
// Change #1: Add some text
blogPostManager.append("Hello World!")
console.log(blogPostManager.getContents())
// Output: Hello World!
// Change #2: Add some more text
blogPostManager.append(" This is the second entry in the document")
console.log(blogPostManager.getContents())
// Output: Hello World! This is the second entry in the document
// Change #3: Overwrite the document with some new text
blogPostManager.overwrite("This entry overwrites everything in the document")
console.log(blogPostManager.getContents())
// Output: This entry overwrites everything in the document
// Change #4: Delete the contents of the document
blogPostManager.delete()
console.log(blogPostManager.getContents())
// Empty output
// Get an old version of the document
console.log(blogPostManager.getVersion(2).getContents())
// Output: Hello World!
// Change #5: Go back to an old version of the document
blogPostManager.revertToVersion(3)
console.log(blogPostManager.getContents())
// Output: Hello World! This is the second entry in the document
// Get the word count of the current document
console.log(blogPostManager.getWordCount())
// Output: 53
// Change #6: Undo the last change
blogPostManager.undo()
console.log(blogPostManager.getContents())
// Empty output
// Get the total number of versions for the document
console.log(blogPostManager.getVersionCount())
// Output: 7
// Get the complete history of the document
console.log(blogPostManager.getHistory())
/**
* Output:
* [
*   '',
*   'Hello World!',
*   'Hello World! This is the second entry in the document',
*   'This entry overwrites everything in the document',
*   '',
*   'Hello World! This is the second entry in the document',
*   ''
* ]
*/
}
run()

雖然Memento設計模式對於管理物件的歷史是一個很好的解決方案,但它可能會變得非常耗費資源。由於每個Memento幾乎是一個物件的副本,如果不適度使用,它可能會迅速膨脹你的應用程式的記憶體。

有了大量的物件,它們的生命週期管理也可能是一個相當繁瑣的任務。除此之外, Originator 和 Caretaker 類通常是非常緊密的耦合,增加了你的程式碼庫的複雜性。

17. Observer

觀察者模式為多物件互動問題提供了另一種解決方案(之前在中介者模式中見過)。

觀察者模式不允許每個物件通過一個指定的調解器相互通訊,而是允許它們相互觀察。物件被設計成在試圖傳送資料或控制時發出事件,而 “監聽 “這些事件的其他物件可以接收這些事件並根據其內容進行互動。

下面是一個通過觀察者模式向多人傳送新聞簡報的簡單演示。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// The newsletter class that can send out posts to its subscribers
function Newsletter() {
// Maintain a list of subscribers
this.subscribers = []
// Subscribe a reader by adding them to the subscribers' list
this.subscribe = function(subscriber) {
this.subscribers.push(subscriber)
}
// Unsubscribe a reader by removing them from the subscribers' list
this.unsubscribe = function(subscriber) {
this.subscribers = this.subscribers.filter(
function (element) {
if (element !== subscriber) return element
}
)
}
// Publish a post by calling the receive function of all subscribers
this.publish = function(post) {
this.subscribers.forEach(function(element) {
element.receiveNewsletter(post)
})
}
}
// The reader class that can subscribe to and receive updates from newsletters
function Reader(name) {
this.name = name
this.receiveNewsletter = function(post) {
console.log("Newsletter received by " + name + "!: " + post)
}
}
function run() {
// Create two readers
let rick = new Reader("ed")
let morty = new Reader("morty")
// Create your newsletter
let newsletter = new Newsletter()
// Subscribe a reader to the newsletter
newsletter.subscribe(rick)
// Publish the first post
newsletter.publish("This is the first of the many posts in this newsletter")
/**
* Output:
* Newsletter received by ed!: This is the first of the many posts in this newsletter
*/
// Subscribe another reader to the newsletter
newsletter.subscribe(morty)
// Publish the second post
newsletter.publish("This is the second of the many posts in this newsletter")
/**
* Output:
* Newsletter received by ed!: This is the second of the many posts in this newsletter
* Newsletter received by morty!: This is the second of the many posts in this newsletter
*/
// Unsubscribe the first reader
newsletter.unsubscribe(rick)
// Publish the third post
newsletter.publish("This is the third of the many posts in this newsletter")
/**
* Output:
* Newsletter received by morty!: This is the third of the many posts in this newsletter
*/
}
run()
// The newsletter class that can send out posts to its subscribers function Newsletter() { // Maintain a list of subscribers this.subscribers = [] // Subscribe a reader by adding them to the subscribers' list this.subscribe = function(subscriber) { this.subscribers.push(subscriber) } // Unsubscribe a reader by removing them from the subscribers' list this.unsubscribe = function(subscriber) { this.subscribers = this.subscribers.filter( function (element) { if (element !== subscriber) return element } ) } // Publish a post by calling the receive function of all subscribers this.publish = function(post) { this.subscribers.forEach(function(element) { element.receiveNewsletter(post) }) } } // The reader class that can subscribe to and receive updates from newsletters function Reader(name) { this.name = name this.receiveNewsletter = function(post) { console.log("Newsletter received by " + name + "!: " + post) } } function run() { // Create two readers let rick = new Reader("ed") let morty = new Reader("morty") // Create your newsletter let newsletter = new Newsletter() // Subscribe a reader to the newsletter newsletter.subscribe(rick) // Publish the first post newsletter.publish("This is the first of the many posts in this newsletter") /** * Output: * Newsletter received by ed!: This is the first of the many posts in this newsletter */ // Subscribe another reader to the newsletter newsletter.subscribe(morty) // Publish the second post newsletter.publish("This is the second of the many posts in this newsletter") /** * Output: * Newsletter received by ed!: This is the second of the many posts in this newsletter * Newsletter received by morty!: This is the second of the many posts in this newsletter */ // Unsubscribe the first reader newsletter.unsubscribe(rick) // Publish the third post newsletter.publish("This is the third of the many posts in this newsletter") /** * Output: * Newsletter received by morty!: This is the third of the many posts in this newsletter */ } run()
// The newsletter class that can send out posts to its subscribers
function Newsletter() {
// Maintain a list of subscribers
this.subscribers = []
// Subscribe a reader by adding them to the subscribers' list
this.subscribe = function(subscriber) {
this.subscribers.push(subscriber)
}
// Unsubscribe a reader by removing them from the subscribers' list
this.unsubscribe = function(subscriber) {
this.subscribers = this.subscribers.filter(
function (element) {
if (element !== subscriber) return element
}
)
}
// Publish a post by calling the receive function of all subscribers
this.publish = function(post) {
this.subscribers.forEach(function(element) {
element.receiveNewsletter(post)
})
}
}
// The reader class that can subscribe to and receive updates from newsletters
function Reader(name) {
this.name = name
this.receiveNewsletter = function(post) {
console.log("Newsletter received by " + name + "!: " + post)
}
}
function run() {
// Create two readers
let rick = new Reader("ed")
let morty = new Reader("morty")
// Create your newsletter
let newsletter = new Newsletter()
// Subscribe a reader to the newsletter
newsletter.subscribe(rick)
// Publish the first post
newsletter.publish("This is the first of the many posts in this newsletter")
/**
* Output:
* Newsletter received by ed!: This is the first of the many posts in this newsletter
*/
// Subscribe another reader to the newsletter
newsletter.subscribe(morty)
// Publish the second post
newsletter.publish("This is the second of the many posts in this newsletter")
/**
* Output:
* Newsletter received by ed!: This is the second of the many posts in this newsletter
* Newsletter received by morty!: This is the second of the many posts in this newsletter
*/
// Unsubscribe the first reader
newsletter.unsubscribe(rick)
// Publish the third post
newsletter.publish("This is the third of the many posts in this newsletter")
/**
* Output:
* Newsletter received by morty!: This is the third of the many posts in this newsletter
*/
}
run()

雖然觀察者模式是一種傳遞控制和資料的巧妙方式,但它更適合於有大量的傳送者和接受者通過有限的連線相互作用的情況。如果這些物件都是一對一的連線,你就會失去通過釋出和訂閱事件得到的優勢,因為每個釋出者總是隻有一個訂閱者(而這時他們之間的直接通訊會處理得更好)。

此外,如果訂閱事件處理不當,觀察者設計模式會導致效能問題。如果一個物件在不需要的情況下繼續訂閱另一個物件,那麼它將沒有資格進行垃圾回收,並會增加應用程式的記憶體消耗。

18. State

狀態設計模式是整個軟體開發行業最常用的設計模式之一。流行的JavaScript框架,如ReactAngular,嚴重依賴狀態模式來管理資料和基於這些資料的應用行為。

簡單地說,狀態設計模式在以下情況下很有幫助:你可以定義一個實體(可以是一個元件、一個頁面、一個應用程式或一臺機器)的明確狀態,並且該實體對狀態變化有預定的反應。

比方說,你正試圖建立一個貸款申請流程。申請過程中的每一步都可以被定義為一個狀態。

雖然客戶通常會看到他們的申請的簡化狀態的小列表(待定、審查中、接受和拒絕),但內部可能還涉及其他步驟。在這些步驟中的每一步,申請將被分配給一個不同的人,並可能有獨特的要求。

系統的設計方式是,在一個狀態下的處理結束後,該狀態會被更新為下一個狀態,並開始下一組相關的步驟。

下面是你如何使用狀態設計模式建立一個任務管理系統:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Create titles for all states of a task
const STATE_TODO = "TODO"
const STATE_IN_PROGRESS = "IN_PROGRESS"
const STATE_READY_FOR_REVIEW = "READY_FOR_REVIEW"
const STATE_DONE = "DONE"
// Create the task class with a title, assignee, and duration of the task
function Task(title, assignee) {
this.title = title
this.assignee = assignee
// Helper function to update the assignee of the task
this.setAssignee = function (assignee) {
this.assignee = assignee
}
// Function to update the state of the task
this.updateState = function (state) {
switch (state) {
case STATE_TODO:
this.state = new TODO(this)
break
case STATE_IN_PROGRESS:
this.state = new IN_PROGRESS(this)
break
case STATE_READY_FOR_REVIEW:
this.state = new READY_FOR_REVIEW(this)
break
case STATE_DONE:
this.state = new DONE(this)
break
default:
return
}
// Invoke the callback function for the new state after it is set
this.state.onStateSet()
}
// Set the initial state of the task as TODO
this.updateState(STATE_TODO)
}
// TODO state
function TODO(task) {
this.onStateSet = function () {
console.log(task.assignee + " notified about new task \"" + task.title + "\"")
}
}
// IN_PROGRESS state
function IN_PROGRESS(task) {
this.onStateSet = function () {
console.log(task.assignee + " started working on the task \"" + task.title + "\"")
}
}
// READY_FOR_REVIEW state that updates the assignee of the task to be the manager of the developer
// for the review
function READY_FOR_REVIEW(task) {
this.getAssignee = function () {
return "Manager 1"
}
this.onStateSet = function () {
task.setAssignee(this.getAssignee())
console.log(task.assignee + " notified about completed task \"" + task.title + "\"")
}
}
// DONE state that removes the assignee of the task since it is now completed
function DONE(task) {
this.getAssignee = function () {
return ""
}
this.onStateSet = function () {
task.setAssignee(this.getAssignee())
console.log("Task \"" + task.title + "\" completed")
}
}
function run() {
// Create a task
let task1 = new Task("Create a login page," "Developer 1")
// Output: Developer 1 notified about new task "Create a login page"
// Set it to IN_PROGRESS
task1.updateState(STATE_IN_PROGRESS)
// Output: Developer 1 started working on the task "Create a login page"
// Create another task
let task2 = new Task("Create an auth server," "Developer 2")
// Output: Developer 2 notified about new task "Create an auth server"
// Set it to IN_PROGRESS as well
task2.updateState(STATE_IN_PROGRESS)
// Output: Developer 2 started working on the task "Create an auth server"
// Update the states of the tasks until they are done
task2.updateState(STATE_READY_FOR_REVIEW)
// Output: Manager 1 notified about completed task "Create an auth server"
task1.updateState(STATE_READY_FOR_REVIEW)
// Output: Manager 1 notified about completed task "Create a login page"
task1.updateState(STATE_DONE)
// Output: Task "Create a login page" completed
task2.updateState(STATE_DONE)
// Output: Task "Create an auth server" completed
}
run()
// Create titles for all states of a task const STATE_TODO = "TODO" const STATE_IN_PROGRESS = "IN_PROGRESS" const STATE_READY_FOR_REVIEW = "READY_FOR_REVIEW" const STATE_DONE = "DONE" // Create the task class with a title, assignee, and duration of the task function Task(title, assignee) { this.title = title this.assignee = assignee // Helper function to update the assignee of the task this.setAssignee = function (assignee) { this.assignee = assignee } // Function to update the state of the task this.updateState = function (state) { switch (state) { case STATE_TODO: this.state = new TODO(this) break case STATE_IN_PROGRESS: this.state = new IN_PROGRESS(this) break case STATE_READY_FOR_REVIEW: this.state = new READY_FOR_REVIEW(this) break case STATE_DONE: this.state = new DONE(this) break default: return } // Invoke the callback function for the new state after it is set this.state.onStateSet() } // Set the initial state of the task as TODO this.updateState(STATE_TODO) } // TODO state function TODO(task) { this.onStateSet = function () { console.log(task.assignee + " notified about new task \"" + task.title + "\"") } } // IN_PROGRESS state function IN_PROGRESS(task) { this.onStateSet = function () { console.log(task.assignee + " started working on the task \"" + task.title + "\"") } } // READY_FOR_REVIEW state that updates the assignee of the task to be the manager of the developer // for the review function READY_FOR_REVIEW(task) { this.getAssignee = function () { return "Manager 1" } this.onStateSet = function () { task.setAssignee(this.getAssignee()) console.log(task.assignee + " notified about completed task \"" + task.title + "\"") } } // DONE state that removes the assignee of the task since it is now completed function DONE(task) { this.getAssignee = function () { return "" } this.onStateSet = function () { task.setAssignee(this.getAssignee()) console.log("Task \"" + task.title + "\" completed") } } function run() { // Create a task let task1 = new Task("Create a login page," "Developer 1") // Output: Developer 1 notified about new task "Create a login page" // Set it to IN_PROGRESS task1.updateState(STATE_IN_PROGRESS) // Output: Developer 1 started working on the task "Create a login page" // Create another task let task2 = new Task("Create an auth server," "Developer 2") // Output: Developer 2 notified about new task "Create an auth server" // Set it to IN_PROGRESS as well task2.updateState(STATE_IN_PROGRESS) // Output: Developer 2 started working on the task "Create an auth server" // Update the states of the tasks until they are done task2.updateState(STATE_READY_FOR_REVIEW) // Output: Manager 1 notified about completed task "Create an auth server" task1.updateState(STATE_READY_FOR_REVIEW) // Output: Manager 1 notified about completed task "Create a login page" task1.updateState(STATE_DONE) // Output: Task "Create a login page" completed task2.updateState(STATE_DONE) // Output: Task "Create an auth server" completed } run()
// Create titles for all states of a task
const STATE_TODO = "TODO"
const STATE_IN_PROGRESS = "IN_PROGRESS"
const STATE_READY_FOR_REVIEW = "READY_FOR_REVIEW"
const STATE_DONE = "DONE"
// Create the task class with a title, assignee, and duration of the task
function Task(title, assignee) {
this.title = title
this.assignee = assignee
// Helper function to update the assignee of the task
this.setAssignee = function (assignee) {
this.assignee = assignee
}
// Function to update the state of the task
this.updateState = function (state) {
switch (state) {
case STATE_TODO:
this.state = new TODO(this)
break
case STATE_IN_PROGRESS:
this.state = new IN_PROGRESS(this)
break
case STATE_READY_FOR_REVIEW:
this.state = new READY_FOR_REVIEW(this)
break
case STATE_DONE:
this.state = new DONE(this)
break
default:
return
}
// Invoke the callback function for the new state after it is set
this.state.onStateSet()
}
// Set the initial state of the task as TODO
this.updateState(STATE_TODO)
}
// TODO state
function TODO(task) {
this.onStateSet = function () {
console.log(task.assignee + " notified about new task \"" + task.title + "\"")
}
}
// IN_PROGRESS state
function IN_PROGRESS(task) {
this.onStateSet = function () {
console.log(task.assignee + " started working on the task \"" + task.title + "\"")
}
}
// READY_FOR_REVIEW state that updates the assignee of the task to be the manager of the developer
// for the review
function READY_FOR_REVIEW(task) {
this.getAssignee = function () {
return "Manager 1"
}
this.onStateSet = function () {
task.setAssignee(this.getAssignee())
console.log(task.assignee + " notified about completed task \"" + task.title + "\"")
}
}
// DONE state that removes the assignee of the task since it is now completed
function DONE(task) {
this.getAssignee = function () {
return ""
}
this.onStateSet = function () {
task.setAssignee(this.getAssignee())
console.log("Task \"" + task.title + "\" completed")
}
}
function run() {
// Create a task
let task1 = new Task("Create a login page," "Developer 1")
// Output: Developer 1 notified about new task "Create a login page"
// Set it to IN_PROGRESS
task1.updateState(STATE_IN_PROGRESS)
// Output: Developer 1 started working on the task "Create a login page"
// Create another task
let task2 = new Task("Create an auth server," "Developer 2")
// Output: Developer 2 notified about new task "Create an auth server"
// Set it to IN_PROGRESS as well
task2.updateState(STATE_IN_PROGRESS)
// Output: Developer 2 started working on the task "Create an auth server"
// Update the states of the tasks until they are done
task2.updateState(STATE_READY_FOR_REVIEW)
// Output: Manager 1 notified about completed task "Create an auth server"
task1.updateState(STATE_READY_FOR_REVIEW)
// Output: Manager 1 notified about completed task "Create a login page"
task1.updateState(STATE_DONE)
// Output: Task "Create a login page" completed
task2.updateState(STATE_DONE)
// Output: Task "Create an auth server" completed
}
run()

雖然狀態模式在隔離流程中的步驟方面做得很好,但在有多個狀態的大型應用程式中,它可能變得非常難以維護。

此外,如果你的流程設計允許不僅僅是線性地通過所有的狀態,那麼你就需要編寫和維護更多的程式碼,因為每個狀態轉換都需要單獨處理。

19. Strategy

策略模式也被稱為策略模式,它的目的是幫助你使用一個共同的介面對類進行封裝和自由互換。這有助於保持客戶端和類之間的鬆散耦合,並允許你新增你想要的許多實現。

眾所周知,在需要使用不同方法/演算法進行相同操作的情況下,或者在需要用更人性化的程式碼取代大量開關塊的情況下,策略模式有很大的幫助。

下面是一個策略模式的例子:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// The strategy class that can encapsulate all hosting providers
function HostingProvider() {
// store the provider
this.provider = ""
// set the provider
this.setProvider = function(provider) {
this.provider = provider
}
// set the website configuration for which each hosting provider would calculate costs
this.setConfiguration = function(configuration) {
this.configuration = configuration
}
// the generic estimate method that calls the provider's unique methods to calculate the costs
this.estimateMonthlyCost = function() {
return this.provider.estimateMonthlyCost(this.configuration)
}
}
// Foo Hosting charges for each second and KB of hosting usage
function FooHosting (){
this.name = "FooHosting"
this.rate = 0.0000027
this.estimateMonthlyCost = function(configuration){
return configuration.duration * configuration.workloadSize * this.rate
}
}
// Bar Hosting charges per minute instead of seconds
function BarHosting (){
this.name = "BarHosting"
this.rate = 0.00018
this.estimateMonthlyCost = function(configuration){
return configuration.duration / 60 * configuration.workloadSize * this.rate
}
}
// Baz Hosting assumes the average workload to be of 10 MB in size
function BazHosting (){
this.name = "BazHosting"
this.rate = 0.032
this.estimateMonthlyCost = function(configuration){
return configuration.duration * this.rate
}
}
function run() {
// Create a website configuration for a website that is up for 24 hours and takes 10 MB of hosting space
let workloadConfiguration = {
duration: 84700,
workloadSize: 10240
}
// Create the hosting provider instances
let fooHosting = new FooHosting()
let barHosting = new BarHosting()
let bazHosting = new BazHosting()
// Create the instance of the strategy class
let hostingProvider = new HostingProvider()
// Set the configuration against which the rates have to be calculated
hostingProvider.setConfiguration(workloadConfiguration)
// Set each provider one by one and print the rates
hostingProvider.setProvider(fooHosting)
console.log("FooHosting cost: " + hostingProvider.estimateMonthlyCost())
// Output: FooHosting cost: 2341.7856
hostingProvider.setProvider(barHosting)
console.log("BarHosting cost: " + hostingProvider.estimateMonthlyCost())
// Output: BarHosting cost: 2601.9840
hostingProvider.setProvider(bazHosting)
console.log("BarHosting cost: " + hostingProvider.estimateMonthlyCost())
// Output: BarHosting cost: 2710.4000
}
run()
// The strategy class that can encapsulate all hosting providers function HostingProvider() { // store the provider this.provider = "" // set the provider this.setProvider = function(provider) { this.provider = provider } // set the website configuration for which each hosting provider would calculate costs this.setConfiguration = function(configuration) { this.configuration = configuration } // the generic estimate method that calls the provider's unique methods to calculate the costs this.estimateMonthlyCost = function() { return this.provider.estimateMonthlyCost(this.configuration) } } // Foo Hosting charges for each second and KB of hosting usage function FooHosting (){ this.name = "FooHosting" this.rate = 0.0000027 this.estimateMonthlyCost = function(configuration){ return configuration.duration * configuration.workloadSize * this.rate } } // Bar Hosting charges per minute instead of seconds function BarHosting (){ this.name = "BarHosting" this.rate = 0.00018 this.estimateMonthlyCost = function(configuration){ return configuration.duration / 60 * configuration.workloadSize * this.rate } } // Baz Hosting assumes the average workload to be of 10 MB in size function BazHosting (){ this.name = "BazHosting" this.rate = 0.032 this.estimateMonthlyCost = function(configuration){ return configuration.duration * this.rate } } function run() { // Create a website configuration for a website that is up for 24 hours and takes 10 MB of hosting space let workloadConfiguration = { duration: 84700, workloadSize: 10240 } // Create the hosting provider instances let fooHosting = new FooHosting() let barHosting = new BarHosting() let bazHosting = new BazHosting() // Create the instance of the strategy class let hostingProvider = new HostingProvider() // Set the configuration against which the rates have to be calculated hostingProvider.setConfiguration(workloadConfiguration) // Set each provider one by one and print the rates hostingProvider.setProvider(fooHosting) console.log("FooHosting cost: " + hostingProvider.estimateMonthlyCost()) // Output: FooHosting cost: 2341.7856 hostingProvider.setProvider(barHosting) console.log("BarHosting cost: " + hostingProvider.estimateMonthlyCost()) // Output: BarHosting cost: 2601.9840 hostingProvider.setProvider(bazHosting) console.log("BarHosting cost: " + hostingProvider.estimateMonthlyCost()) // Output: BarHosting cost: 2710.4000 } run()
// The strategy class that can encapsulate all hosting providers
function HostingProvider() {
// store the provider
this.provider = ""
// set the provider
this.setProvider = function(provider) {
this.provider = provider
}
// set the website configuration for which each hosting provider would calculate costs
this.setConfiguration = function(configuration) {
this.configuration = configuration
}
// the generic estimate method that calls the provider's unique methods to calculate the costs
this.estimateMonthlyCost = function() {
return this.provider.estimateMonthlyCost(this.configuration)
}
}
// Foo Hosting charges for each second and KB of hosting usage
function FooHosting (){
this.name = "FooHosting"
this.rate = 0.0000027
this.estimateMonthlyCost = function(configuration){
return configuration.duration * configuration.workloadSize * this.rate
}
}
// Bar Hosting charges per minute instead of seconds
function BarHosting (){
this.name = "BarHosting"
this.rate = 0.00018
this.estimateMonthlyCost = function(configuration){
return configuration.duration / 60 * configuration.workloadSize * this.rate
}
}
// Baz Hosting assumes the average workload to be of 10 MB in size
function BazHosting (){
this.name = "BazHosting"
this.rate = 0.032
this.estimateMonthlyCost = function(configuration){
return configuration.duration * this.rate
}
}
function run() {
// Create a website configuration for a website that is up for 24 hours and takes 10 MB of hosting space
let workloadConfiguration = {
duration: 84700,
workloadSize: 10240
}
// Create the hosting provider instances
let fooHosting = new FooHosting()
let barHosting = new BarHosting()
let bazHosting = new BazHosting()
// Create the instance of the strategy class
let hostingProvider = new HostingProvider()
// Set the configuration against which the rates have to be calculated
hostingProvider.setConfiguration(workloadConfiguration)
// Set each provider one by one and print the rates
hostingProvider.setProvider(fooHosting)
console.log("FooHosting cost: " + hostingProvider.estimateMonthlyCost())
// Output: FooHosting cost: 2341.7856
hostingProvider.setProvider(barHosting)
console.log("BarHosting cost: " + hostingProvider.estimateMonthlyCost())
// Output: BarHosting cost: 2601.9840
hostingProvider.setProvider(bazHosting)
console.log("BarHosting cost: " + hostingProvider.estimateMonthlyCost())
// Output: BarHosting cost: 2710.4000
}
run()

當涉及到為一個實體引入新的變化而又不怎麼改變客戶時,策略模式是很好的。然而,如果你只有少量的變體需要實現,它就會顯得有些矯枉過正。

而且,封裝帶走了關於每個變體內部邏輯的更多細節,所以你的客戶不知道一個變體將如何表現。

20. Visitor

訪客模式的目的是幫助你使你的程式碼具有可擴充套件性。

這個想法是在類中提供一個方法,允許其他類的物件輕鬆地對當前類的物件進行修改。其他物件訪問當前物件(也稱為地方物件),或者當前類接受訪問者物件,而地方物件適當地處理每個外部物件的訪問。

下面是你如何使用它:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Visitor class that defines the methods to be called when visiting each place
function Reader(name, cash) {
this.name = name
this.cash = cash
// The visit methods can access the place object and invoke available functions
this.visitBookstore = function(bookstore) {
console.log(this.name + " visited the bookstore and bought a book")
bookstore.purchaseBook(this)
}
this.visitLibrary = function() {
console.log(this.name + " visited the library and read a book")
}
// Helper function to demonstrate a transaction
this.pay = function(amount) {
this.cash -= amount
}
}
// Place class for a library
function Library () {
this.accept = function(reader) {
reader.visitLibrary()
}
}
// Place class for a bookstore that allows purchasing book
function Bookstore () {
this.accept = function(reader) {
reader.visitBookstore(this)
}
this.purchaseBook = function (visitor) {
console.log(visitor.name + " bought a book")
visitor.pay(8)
}
}
function run() {
// Create a reader (the visitor)
let reader = new Reader("Rick," 30)
// Create the places
let booksInc = new Bookstore()
let publicLibrary = new Library()
// The reader visits the library
publicLibrary.accept(reader)
// Output: Rick visited the library and read a book
console.log(reader.name + " has $" + reader.cash)
// Output: Rick has $30
// The reader visits the bookstore
booksInc.accept(reader)
// Output: Rick visited the bookstore and bought a book
console.log(reader.name + " has $" + reader.cash)
// Output: Rick has $22
}
run()
// Visitor class that defines the methods to be called when visiting each place function Reader(name, cash) { this.name = name this.cash = cash // The visit methods can access the place object and invoke available functions this.visitBookstore = function(bookstore) { console.log(this.name + " visited the bookstore and bought a book") bookstore.purchaseBook(this) } this.visitLibrary = function() { console.log(this.name + " visited the library and read a book") } // Helper function to demonstrate a transaction this.pay = function(amount) { this.cash -= amount } } // Place class for a library function Library () { this.accept = function(reader) { reader.visitLibrary() } } // Place class for a bookstore that allows purchasing book function Bookstore () { this.accept = function(reader) { reader.visitBookstore(this) } this.purchaseBook = function (visitor) { console.log(visitor.name + " bought a book") visitor.pay(8) } } function run() { // Create a reader (the visitor) let reader = new Reader("Rick," 30) // Create the places let booksInc = new Bookstore() let publicLibrary = new Library() // The reader visits the library publicLibrary.accept(reader) // Output: Rick visited the library and read a book console.log(reader.name + " has $" + reader.cash) // Output: Rick has $30 // The reader visits the bookstore booksInc.accept(reader) // Output: Rick visited the bookstore and bought a book console.log(reader.name + " has $" + reader.cash) // Output: Rick has $22 } run()
// Visitor class that defines the methods to be called when visiting each place
function Reader(name, cash) {
this.name = name
this.cash = cash
// The visit methods can access the place object and invoke available functions
this.visitBookstore = function(bookstore) {
console.log(this.name + " visited the bookstore and bought a book")
bookstore.purchaseBook(this)
}
this.visitLibrary = function() {
console.log(this.name + " visited the library and read a book")
}
// Helper function to demonstrate a transaction
this.pay = function(amount) {
this.cash -= amount
}
}
// Place class for a library
function Library () {
this.accept = function(reader) {
reader.visitLibrary()
}
}
// Place class for a bookstore that allows purchasing book
function Bookstore () {
this.accept = function(reader) {
reader.visitBookstore(this)
}
this.purchaseBook = function (visitor) {
console.log(visitor.name + " bought a book")
visitor.pay(8)
}
}
function run() {
// Create a reader (the visitor)
let reader = new Reader("Rick," 30)
// Create the places
let booksInc = new Bookstore()
let publicLibrary = new Library()
// The reader visits the library
publicLibrary.accept(reader)
// Output: Rick visited the library and read a book
console.log(reader.name + " has $" + reader.cash)
// Output: Rick has $30
// The reader visits the bookstore
booksInc.accept(reader)
// Output: Rick visited the bookstore and bought a book
console.log(reader.name + " has $" + reader.cash)
// Output: Rick has $22
}
run()

這種設計的唯一缺陷是,每當有新的地點被新增或修改時,每個訪問者類都需要被更新。在多個訪問者和地點物件同時存在的情況下,這可能難以維護。

除此以外,該方法對於動態增強類的功能非常有效。

實現設計模式的最佳實踐

現在你已經看到了JavaScript中最常見的設計模式,這裡有一些提示,你在實現它們時應該記住。

要特別注意瞭解模式是否適合於解決方案

在你將一個設計模式實施到你的原始碼中之前,這個提示應該被應用。雖然看起來一個設計模式是你所有煩惱的終結者,但花點時間來批判性地分析一下這是否是真的。

有許多模式可以解決同樣的問題,但採取不同的方法,產生不同的後果。因此,你選擇設計模式的標準不應該僅僅是它是否能解決你的問題–還應該是它解決你的問題的能力如何,以及是否有其他模式能提出更有效的解決方案。

在開始之前瞭解實施一個模式的成本

雖然設計模式似乎是所有工程問題的最佳解決方案,但你不應該馬上跳到原始碼中實施它們。

在判斷實施一個解決方案的後果時,你還需要考慮到自己的情況。你是否有一個由軟體開發人員組成的龐大團隊,能夠很好地理解和維護設計模式?或者你是一個早期階段的創始人,擁有一個最小的開發團隊,希望快速釋出產品的MVP?如果你對最後一個問題的回答是肯定的,那麼設計模式可能不是最適合你的開發方式。

設計模式不會導致大量的程式碼重用,除非它們在應用設計的早期階段就被規劃好。在不同的階段隨機使用設計模式會導致不必要的複雜的應用架構,你不得不花幾周時間來簡化。

一個設計模式的有效性不能通過任何形式的測試來判斷。是你的團隊的經驗和自省會讓你知道它們是否有效。如果你有時間和資源來分配給這些方面,只有這樣,設計模式才能真正解決你的問題。

不要把每個解決方案都變成一個模式

另一個要記住的經驗法則是,不要試圖把每一個小的問題-解決方案對變成一個設計模式,並在你看到有空間的地方使用它。

當你遇到類似的問題時,確定標準的解決方案並記住它們是很好的,但你遇到的新問題很有可能不符合與舊問題完全相同的描述。在這種情況下,你可能最終會實施一個次優的解決方案,浪費資源。

設計模式在今天被確立為問題-解決方案對的領先例子,因為它們已經被成百上千的程式設計師長期測試,並被儘可能地普及。如果你試圖複製這種努力,只看一堆問題和解決方案,並稱它們是相似的,你最終可能會對你的程式碼造成比你預期的更大的損害。

你什麼時候應該使用設計模式?

總而言之,這裡有一些你應該注意的使用設計模式的線索。並非所有的都適用於每一個應用程式的開發,但它們應該讓你知道在考慮使用設計模式時應該注意什麼。

  • 你有一個強大的內部開發團隊,他們對設計模式非常瞭解。
  • 你所遵循的SDLC模式允許圍繞你的應用程式的架構進行深入的討論,而設計模式已經在這些討論中出現了。
  • 同樣的問題在你的設計討論中出現過多次,你知道適合這種情況的設計模式。
  • 你已經嘗試用設計模式來獨立解決你的問題的一個較小的變體。
  • 有了設計模式,你的程式碼看起來就不會過於複雜了。

如果一個設計模式能夠解決你的問題,並幫助你寫出簡單、可重用、模組化、鬆散耦合、沒有 “程式碼氣味 “的程式碼,那麼它可能是正確的方法。

另一個要記住的好建議是避免把所有的事情都和設計模式掛鉤。設計模式是用來幫助你解決問題的。它們不是要遵守的法律或要嚴格遵守的規則。最終的規則和法律仍然是一樣的:保持你的程式碼乾淨、簡單、可讀和可擴充套件。如果一個設計模式能幫助你做到這一點,同時解決你的問題,你就應該很好地使用它。

小結

JavaScript設計模式是處理多個程式設計師長期以來所面臨的問題的絕佳方式。它們提出了久經考驗的解決方案,努力保持你的程式碼庫的清潔和鬆散耦合。

今天,有數百種設計模式可以解決你在構建應用程式時遇到的幾乎所有問題。然而,並非每一種設計模式都能真正解決你的問題。

就像其他的程式設計慣例一樣,設計模式是作為解決問題的建議來使用的。它們並不是要一直遵循的法則,如果你把它們當作法則,你可能最終會對你的應用程式造成很大的損害。

評論留言