本系列其他译文请看[JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)] (juejin.cn/column/6988…
本文阅读指数:3
本文对设计模式介绍的比较简单。设计模式其实是非常重要的一个实践,但是大多数前端要么干脆不了解,要么了解了也很少实践。比较推荐大家深入学习一下设计模式。
总览
设计模式已经成了标准方案,来解决一些开发中的通用问题。我认为这些设计模式已经成为行业标准了
学习设计模式不仅会让你成为一个更牛掰的开发者,同时也会让你更好的理解一些框架是如何创建的。大多数的框架都采用了某种设计模式,学习之后会让你容易理解一些新出的框架。
可以用任何语言来实现设计模式。它的灵活性很好,你可以随意使用和拓展。
这一章我们会看看JS的设计模式,为什么需要这些模式,以及不同类型的设计模式。
设计模式是什么?我们为什么需要?
设计模式已经成为行业标准,我们可以称之为’templates‘。使用设计模式,可以让我们避免陷入疯狂的代码重复当中。
使用设计模式的主要原因如下:
- 帮助我们写干净且有组织的代码。因为设计模式可以让我们代码结构更干净,容易调试和维护
- 他们解决了类似的问题。当我们构建类,解耦代码,复用代码和对象时很容易产生问题。未来,代码的解耦会让开发对代码改动时减少很多Bug。
- 合理使用设计模式,可以节省很多时间。因为这些模式已经很成熟了,可以明显解决节省时间。
设计模式分类
设计模式主要分三类,创建型,结构型和行为型。 看看它们是如何被分类的。
创建型
这一类主要是为了创建对象。它为特殊的用户场景创建特殊的对象,并隐藏了创建的逻辑,只是暴露接口给我们。
总的来说,我们使用相关接口创建特殊场景下的对象。主要的模式包含:
- 单例
- 工厂
- 抽象工厂
- 建造者
- 原型 我们会看看单例模式的工作原理
单例模式
这种模式,确保一个类可以创建一个实例。
这个模式有一些容易误解的参数,但是还是很容易实现。主要的步骤如下:
- 你的类创建一个对象
- 创建一个实例
- 阻止应用在其他地方再次实例化这个对象
- 把实例当做资源分享
直接看代码吧,我们先创建一个类,然后稍后让它单例化。
Step 1: 声明一个 Manufacture
class Database {
constructor() {
this.connectionURL = {
name: "",
options: {}
}
}
// Our connect method taking in two arguments
connect(name, options) {
this.connectionURL.name = name;
this.connectionURL.options = options;
console.log(`DB: ${name} connected!`);
}
// Disconnect method
disconnect() {
console.log(`${this.connectionURL.name} is disconnected!`);
}
}
// Instantiating our class
const db = new Database()
console.log(db.connect("Facebook"))
Step 2:实例化之后,让你的属性无法被修改
class Database {
constructor() {
this.connectionURL = {
name: "",
options: {}
}
// This disallows modifying the instance we created
Object.freeze(this);
}
// Our connect method taking in two arguments
connect(name, options) {
this.connectionURL.name = name;
this.connectionURL.options = options;
console.log(`DB: ${name} connected!`);
}
// Disconnect method
disconnect() {
console.log(`${this.connectionURL.name} is disconnected!`);
}
}
// Instantiating our class
const db = new Database();
console.log(db.connect("Facebook"));
上面的代码中,不允许再增加或者改动属性了。在其他语言,比如JAVA中,我们可以创建一个 getInstance()方法 ,用来达成单例模式。在上面的JS中,我们使用了constructor() 来替代。
Step 3. 让我们的类自己实例化,并检查是否已经实例化过了。
class Database {
constructor() {
// Check if our first instance has already been created
if (Database.instance instanceof Database) {
return Database.instance;
}
this.connectionURL = {
name: "",
options: {}
}
// This disallows modifying the instance we created
Object.freeze(this);
// Make our class an instance of itself
Database.instance = this;
}
// Our connect method taking in two arguments
connect(name, options) {
this.connectionURL.name = name;
this.connectionURL.options = options;
console.log(`DB: ${name} connected!`);
}
// Disconnect method
disconnect() {
console.log(`${this.connectionURL.name} is disconnected!`);
}
}
// Instantiating our class
const db = new Database();
console.log(db.connect("Facebook"));
// Check if our first instance has already been created
if (Database.instance instanceof Database) {
return Database.instance;
}
上面的代码中,我们第一次实例化之后,就会检查是否之前已经被实例化了,如果实例化了,就会返回之前实例化的。由此,避免重复实例化。
我们创建两个实例,确认一下它们是否是一样的
class Database {
constructor() {
if (Database.instance instanceof Database) {
return Database.instance;
}
this.connectionURL = {
name: "",
options: {}
}
Database.instance = this;
}
connect(uri, options) {
this.connectionURL.name = name;
this.connectionURL.options = options;
console.log(`DB: ${uri} connected!`);
}
disconnect() {
console.log(`${this.connectionURL.name} is disconnected!`);
}
}
const db = new Database()
const db1 = new Database()
console.log(db === db1)
// true
可以看到创建另一个实例是不允许的。 使用这个模式还要注意一下问题
- 并发场景。当2个以上的线程想去访问单例中的共享资源,此时有可能不会被立刻获取,会有性能瓶颈。
- 单例非常像全局变量,所以很难面面俱到的测试到,因为应用中的每一部分都会用到。
结构型
这个模式主要表示实体之间的关系,主要是对象和类的组合。这种模式的两个关键词是组合和继承。
主要的模式包括:
- 适配模式
- 外观模式
- 桥接模式
- 代理模式
- 享元模式
我们主要看一下适配模式
适配模式
这种模式来桥接两个不兼容的类。我将这种模式当做一个包装器,把两个独立的接口拼接到一起。
比如在真实情况下,我们可以生成一个套接字适配器,连接套接字和不兼容的插件。这种桥接就是适配模式。 我们看看JS中如何实现。我们会简单解释一下,同时不要跟外观模式混淆。
import { first, middle, last } from "random-name";
class randomName {
generateFirstName() {
return first();
}
generateMiddleName() {
return middle();
}
generateLastName() {
return last();
}
}
export default new randomName();
上面的代码就是适配器,我们可以使用任意库。比如这样:
import name from "./random-name";
class PlugComponent {
constructor() {
this.firstName = name.generateFirstName();
this.middleName = name.generateMiddleName();
this.lastName = name.generateLastName();
}
generateFullName() {
return `${this.firstName} ${this.middleName} ${this.lastName}`
}
}
const names = new PlugComponent()
console.log(names.generateFullName()) // Victor Victor Jonah
这种模式可以提升复用性和灵活性。
行为型
这种模式聚焦于对象间通信。开发者在让对象间通信时,能够保持解耦性和灵活性。
这种模式主要包含:
- 责任链
- 命令模式
- 解释器
- 观察者
- 空对象模式
我们看一下空对象模式
空对象模式
这种模式避免返回null,它封装了null行为,并且返回客户端预期的值。大多时候,我们不允许使用null引用。所以我们要做Null检查,这会让我们的代码多很多if/else。使用了这种模式,就不用再写这种逻辑了。
当我们不想返回Null值时,空对象模式很有用。日常中用来捕捉异常也是非常好用的。 来看看实现:
class Cat {
sound() {
return 'meoow';
}
}
class NullAnimal {
sound() {
return "not an animal";
}
}
const getAnimal = (type) => {
return type === 'cat' ? new Cat() : new NullAnimal();
}
const results = ['cat', null];
const response = results.map((animal) => getAnimal(animal).sound());
// ["meoow", "not an animal"]
我们没有返回Null引用,返回一个预期的值。
最佳实践
要最佳实践,看看一些大原则
- 编码前设计: 在编码实现之前做设计,是一把利刃。
- KISS — 保持简单不做预设:如果你无法解释它,那它就不够简单。设计模式的目的是保持代码的简单和易于理解。
- DRY — 不要重复自己:让你的函数可复用。不需要到处重复写。
- 关注分离点: 分离服务,让每一个子程序承担单一职责。