关于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中实现它:

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中实现它:

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都有一组共同的方法,它可以调用这些方法来执行实例化动作。

这就是使用前面的例子可以实现的方式:

// 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一起行动,以帮助制作披萨!

// 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模式来创建基于设定的模板文档的新文档:

// 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设计模式为你提供了一个抽象,在新类的方法和属性与旧类的方法和属性之间架起了桥梁。它具有与旧类相同的接口,但它包含将旧方法映射到新方法的逻辑,以执行类似的操作。这类似于一个电源插头插座如何在美式插头和欧式插头之间充当适配器。

这里有一个例子:

// 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设计模式为类和客户提供了独立的接口,这样即使在本地接口不兼容的情况下,它们也可以同时工作。

它有助于在这两种类型的对象之间开发一个非常松散的耦合接口。这也有助于提高接口及其实现的可扩展性,以获得最大的灵活性。

下面是你如何使用它:

// 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复合设计模式帮助你轻松地结构和管理类似的对象和实体。复合模式的基本思想是,对象和它们的逻辑容器可以用一个单一的抽象类来表示(可以存储与对象相关的数据/方法以及对容器本身的引用)。

当你的数据模型类似于树状结构时,使用复合模式是最有意义的。然而,你不应该仅仅为了使用复合模式而试图将一个非树形的数据模型变成一个树形的数据模型,因为这样做往往会失去很多灵活性。

在下面的例子中,你将看到如何使用复合设计模式来构建一个电子商务产品的包装系统,该系统还可以计算每个包装的总订单价值:

// 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 类中添加更多的功能:

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设计模式帮助你在调用上述操作的环境与完成这些操作的对象和方法之间创建一个抽象。这个抽象包含了初始化对象的逻辑,跟踪它们的依赖关系,以及其他重要的活动。调用环境没有关于操作如何被执行的信息。你可以自由地更新逻辑,而不对调用客户端做任何破坏性的改变。

下面是你如何在一个应用程序中使用它:

/**
* 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设计模式将一个大句子存储在内存中。程序并没有在每个字符出现时进行存储,而是确定了用于书写该段落的一组不同的字符及其类型(数字或字母),并为每个字符建立了可重复使用的飞轮,其中包含了存储哪个字符和类型的细节。

然后,主数组只需按照在句子中出现的顺序存储对这些飞轮的引用列表,而不是每次出现都存储一个字符对象的实例。

这样,句子所占用的内存就减少了一半。请记住,这只是对文本处理程序如何存储文本的一个非常基本的解释。

// 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

代理模式帮助你用一个对象替代另一个对象。换句话说,代理对象可以代替实际的对象(它们的代理),并控制对该对象的访问。这些代理对象可以被用来在调用请求传递给实际对象之前或之后执行一些动作。

在下面的例子中,你会看到如何通过一个代理来控制对数据库实例的访问,该代理在允许请求通过之前对请求进行一些基本的验证检查:

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设计中,多层组件可以处理一个用户输入事件,如触摸或滑动。

下面你将看到一个使用责任链模式的投诉升级的例子。该投诉将由处理者根据其严重程度进行处理:

// 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循环,可能会变得相当混乱–尤其是当你还在里面编写业务逻辑时。

迭代器模式可以帮助你将列表的迭代和处理逻辑与主要业务逻辑隔离开来。

下面是你如何在一个有多种类型元素的相当基本的列表中使用它:

// 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

你的应用设计有时可能需要你与大量不同的对象打交道,这些对象容纳了各种业务逻辑,并经常相互依赖。处理这些依赖关系有时会变得很棘手,因为你需要跟踪这些对象之间如何交换数据和控制。

调解器设计模式的目的是帮助你解决这个问题,它将这些对象的交互逻辑隔离到一个单独的对象中。

这个单独的对象被称为调解器,它负责让你的低层类完成工作。你的客户端或调用环境也将与调解器而不是低级别的类进行交互。

下面是一个关于调解器设计模式的实例:

// 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设计模式利用这些纪念品来保存对象的快照,因为它是随时间变化的。当你需要回滚到一个旧版本时,你可以简单地调出它的纪念品。

下面是你如何在一个文本处理应用程序中实现它:

// 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

观察者模式为多对象交互问题提供了另一种解决方案(之前在中介者模式中见过)。

观察者模式不允许每个对象通过一个指定的调解器相互通信,而是允许它们相互观察。对象被设计成在试图发送数据或控制时发出事件,而 “监听 “这些事件的其他对象可以接收这些事件并根据其内容进行交互。

下面是一个通过观察者模式向多人发送新闻简报的简单演示。

// 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,严重依赖状态模式来管理数据和基于这些数据的应用行为。

简单地说,状态设计模式在以下情况下很有帮助:你可以定义一个实体(可以是一个组件、一个页面、一个应用程序或一台机器)的明确状态,并且该实体对状态变化有预定的反应。

比方说,你正试图建立一个贷款申请流程。申请过程中的每一步都可以被定义为一个状态。

虽然客户通常会看到他们的申请的简化状态的小列表(待定、审查中、接受和拒绝),但内部可能还涉及其他步骤。在这些步骤中的每一步,申请将被分配给一个不同的人,并可能有独特的要求。

系统的设计方式是,在一个状态下的处理结束后,该状态会被更新为下一个状态,并开始下一组相关的步骤。

下面是你如何使用状态设计模式建立一个任务管理系统:

// 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

策略模式也被称为策略模式,它的目的是帮助你使用一个共同的接口对类进行封装和自由互换。这有助于保持客户端和类之间的松散耦合,并允许你添加你想要的许多实现。

众所周知,在需要使用不同方法/算法进行相同操作的情况下,或者在需要用更人性化的代码取代大量开关块的情况下,策略模式有很大的帮助。

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

// 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

访客模式的目的是帮助你使你的代码具有可扩展性。

这个想法是在类中提供一个方法,允许其他类的对象轻松地对当前类的对象进行修改。其他对象访问当前对象(也称为地方对象),或者当前类接受访问者对象,而地方对象适当地处理每个外部对象的访问。

下面是你如何使用它:

// 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设计模式是处理多个程序员长期以来所面临的问题的绝佳方式。它们提出了久经考验的解决方案,努力保持你的代码库的清洁和松散耦合。

今天,有数百种设计模式可以解决你在构建应用程序时遇到的几乎所有问题。然而,并非每一种设计模式都能真正解决你的问题。

就像其他的编程惯例一样,设计模式是作为解决问题的建议来使用的。它们并不是要一直遵循的法则,如果你把它们当作法则,你可能最终会对你的应用程序造成很大的损害。

评论留言