[译]JavaScript设计模式——常见设计模式举例

302 阅读6分钟

这是我参与更文挑战的第13天,活动详情查看:更文挑战

本文翻译自 《Design Patterns in JavaScript》

我们在开发应用的时候会遇到很多问题,有的可以自己解决,有的会问别人。随着应用更复杂,处理问题也会更困难,对于一些普遍的问题,更好的方式是找到通用的解决方法。

设计模式提供的就是一种可以通用的代码设计经验,方便我们解决更多通用问题。这一概念早就存在,但直到四人组在上世纪九十年代正式提出,才成为一种广为人知的规范。他们还写了一本书来详细介绍:《设计模式》

四人组是指Erich Gamma, Richard Helm, Ralph Johnson & John Vlissides,被称为Gang of Four,简称GoF

书里的26种设计模式可以被归为三类:

  • 创建型模式 —— 主要是关注对象的创建,将创建对象的过程进行了抽象和封装,使用者只需要去使用对象,无需关心创建过程的逻辑。
  • 结构型模式 —— 主要涉及的是对象的组合,在结构上调整和组装它们,以便对象之间的关系更为简单。
  • 行为型模式 —— 关注的是不同的对象之间如何去交互和通信。

下面会对每一类型举例讲解。

创建型

单例模式

单例模式的特点是限制类只存在一个实例对象。实例不存在的时候,就创建一个实例对象,如果已存在,就返回之前实例化的对象。

当我们需要通过给函数提供单点交互,来确保对象在应用内共享时,单例模式非常有用。

单例模式的应用包括日志记录、缓存、数据库连接等等。

我们看下面这个关于缓存的简单例子:

class MiniCache {
  constructor() {
    if (MiniCache.instance == null) {
      this.cache = {};
      MiniCache.instance = this;
    }
    return MiniCache.instance;
  }

  add(key, value) {
    this.cache[key] = value;
  }

  remove(key) {
    delete this.cache[key];
  }

  get(key) {
    return this.cache[key];
  }
}

const cache = new MiniCache();

Object.freeze(cache);

export { cache };
import { cache } from "./utils/cache";
export class First {
  constructor() {
    cache.add("book", "Harry Potter");
  }
}
import { cache } from "./utils/cache";
export class Second {
  constructor() {
    console.log(cache.get("book"));
  }
}

可以看到保存在First中的book可以在Second中取出,这是因为我们只允许在这两个类之间维持一个共享的cache对象。

尽管单例模式很有用,但如果你发现在应用中需要重复使用它的时候,就说明可能需要重新评估设计模式了。此外,单利模式由于耦合过紧也会给测试带来问题。

工厂模式

工厂模式也属于创造型设计模式的一种,它的核心也是创建对象。但是它不是用构造函数来创建对象,而是采用泛型接口来创建,这样我们可以指定对象的类型。

在这种更简单的模式下,我们只需要输入我们想要的对象类型和属性,工厂方法就会根据属性创建并返回一个合适的对象。这在对象创建过程复杂并且需要解耦的时候尤其有用。

工厂模式的一个典型例子是,根据输入的参数创建不同的UI组件。

结合下面的例子来理解。

class Entree {
  constructor(name) {
    this.name = name;
    this.type = "Entree";
  }
}

class MainCourse {
  constructor(name) {
    this.name = name;
    this.type = "Main Course";
  }
}

class Dessert {
  constructor(name) {
    this.name = name;
    this.type = "Dessert";
  }
}

export class DishFactory {
  createDish(name, type) {
    switch (type.toLowerCase()) {
      case "entree":
        return new Entree(name);
        break;
      case "main course":
        return new MainCourse(name);
        break;
      case "dessert":
        return new Dessert(name);
        break;
    }
  }
}
import { DishFactory } from "./dishFactory.js";

let dishFactory = new DishFactory();
let shrimpEntree = dishFactory.createDish("Shrimp Cutlets", "entree");
let lambMain = dishFactory.createDish("Lamb Chops", "main course");
let baklavaDessert = dishFactory.createDish("Baklava", "dessert");

console.log(shrimpEntree, lambMain, baklavaDessert);

这个例子里,dish factory可以根据不同的参数创建对应的 dish

通过将此设计模式应用于错误类型的问题,您可以轻松地将不必要的复杂性引入到您的应用程序中。您必须确保您的设计目标将通过正在实施的模式实现。

把这种模式应用于处理错误类型的问题,我们就可以把不必要的复杂代码通过factory引入到应用中。需要确认的是,采用这种模式能够达到我们的设计目的。

结构型

外观模式

外观模式的核心是给复杂的代码体提供一个高级接口,可以看作代码体的API,把复杂的地方封装起来,呈现一个简化的界面。因此,外观模式属于结构型模式。

这种模式可以和其他模式结合,例如,外观对象常常是一个单例对象,因为只需要一个外观对象。

使用外观模式的时候,需要注意性能开销,以及是否适合抽象出层级。

我们看下面这个涉及外观模式的例子:

// 采用普通模式
import axios from "axios";
export class PostService {
  getPosts() {
    return axios
      .get("https://jsonplaceholder.typicode.com/posts")
      .then(res => res.data);
  }

  getPostsById(id) {
    return axios
      .get(`https://jsonplaceholder.typicode.com/posts/${id}`)
      .then(res => res.data);
  }

  getIndividualPostComments(id) {
    return axios
      .get(`https://jsonplaceholder.typicode.com/posts/${id}/comments`)
      .then(res => res.data);
  }
}
// 采用外观模式
import axios from "axios";
export class PostServiceFacade {
  constructor() {
    this.postsFacade = new PostServiceBaseFacade();
  }
  getPosts() {
    return this.postsFacade.getAxiosPosts();
  }

  getPostsById(id) {
    return this.postsFacade.getAxiosPosts(id);
  }

  getIndividualPostComments(id) {
    return this.postsFacade.getAxiosPosts(id, true);
  }
}

class PostServiceBaseFacade {
  getAxiosPosts(id, comments) {
    let baseUrl = "https://jsonplaceholder.typicode.com/posts";
    if (id) baseUrl += `/${id}`;
    if (comments) baseUrl += "/comments";
    return axios.get(baseUrl).then(res => res.data);
  }
}

上面的例子里,外观模式把每个 HTTP 请求重复的代码整合到一起,抽象出了一个更高级的接口。

装饰模式

装饰模式被归类为结构型设计模式的原因是,它主要针对的是代码的复用性。装饰模式是用一个函数包装另一个函数来扩展现有能力,我们可以通过另一段代码来包装现有代码的方式「装饰」现有代码。对于熟悉复合函数和高阶函数的人来说,这个概念并不陌生。

装饰模式允许我们写更简洁的代码然后组合,也可以帮我们把相同的功能扩展到多个函数或类中,从而写出更容易调试和维护的代码。

装饰模式让我们的代码更专注,因为它可以把功能优化的代码从核心代码中分开,也便于增加新功能。

//decorator function
const allArgsValid = function(fn) {
  return function(...args) {
  if (args.length != fn.length) {
      throw new Error('Only submit required number of params');
    }
    const validArgs = args.filter(arg => Number.isInteger(arg));
    if (validArgs.length < fn.length) {
      throw new TypeError('Argument cannot be a non-integer');
    }
    return fn(...args);
  }
}

//ordinary multiply function
let multiply = function(a,b){
	return a*b;
}

//decorated multiply function that only accepts the required number of params and only integers
multiply = allArgsValid(multiply);

multiply(6, 8);
//48

multiply(6, 8, 7);
//Error: Only submit required number of params

multiply(3, null);
//TypeError: Argument cannot be a non-integer

multiply('',4);
//TypeError: Argument cannot be a non-integer

上述代码通过装饰模式把普通函数 multiply的功能进行了扩展。装饰器函数 allArgsValid接收一个函数当参数,然后返回一个新函数,这个新函数里包含了传入的函数和它的参数。allArgsValid的作用是校验传入multiply函数的参数是否符合要求(参数个数正确并且是整数),否则就直接报错不会调用multiply函数。

装饰模式让我们可以通过包裹一层函数来增加新的功能,而不需要担心改动原函数。这也可以让我们停止依赖很多子类去获取相同的功能。

另一方面,过度使用装饰模式会造成我们的命名空间里出现很多相似的小对象,对不熟悉的开发者来说,一开始也难以处理。

行为型

观察者模式

观察者模式就是一个对象(称为主体)维护着一个对这主体感兴趣的对象(称为观察者)列表。当主体状态变化的时候,就会通知给观察者们,并且包含一些相关数据。如果需要,观察者们也可以主动停止被通知。

Angular 开发者对这个模式一定不会陌生,流行的 RXJS 库也大量使用了这种模式。

通过下面的例子来看一下:

// 定义主题
export class Subject {
  constructor() {
    this.observables = [];
  }

  subscribe(fn) {
    this.observables.push(fn);
  }

  unsubscribe(fn) {
    this.observables = this.observables.filter(fns => {
      if (fns != fn) return fn;
    });
  }

  notifyAll() {
    this.observables.forEach(fn => {
      fn.call();
    });
  }
}
// 观察者订阅主题
import { Subject } from "./subject.js";

let subject = new Subject();

function Observer1() {
  console.log("Hi from Observable 1");
}

function Observer2() {
  console.log("Hi from Observable 2");
}

subject.subscribe(Observer1);
subject.subscribe(Observer2);

subject.notifyAll();

subject.unsubscribe(Observer2);
subject.notifyAll();

除了以上5种,你可能还了解其他设计模式。设计模式教我们的是解决一类问题的方法,而不是可以复制粘贴的方案。 如何利用设计模式去解决手头的问题,取决于开发者们自己的想法和能力。

还需要注意的是,用错误的设计模式解决错误的问题,可能会导致更差的效果。