边学边译JS工作机制--29.设计模式简介以及最佳实践

277 阅读6分钟

本系列其他译文请看[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 — 不要重复自己:让你的函数可复用。不需要到处重复写。
  • 关注分离点: 分离服务,让每一个子程序承担单一职责。