设计模式大冒险第三关:工厂模式,封装和解耦你的代码

Posted dreamapplehappy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式大冒险第三关:工厂模式,封装和解耦你的代码相关的知识,希望对你有一定的参考价值。

这篇文章是关于设计模式系列的第三篇文章,这一系列的每一篇文章都会通过生活中的小例子以及一些简单的比喻让大家明白每一个设计模式要解决的是什么问题,然后通过什么方式解决的。希望大家在看过每篇文章之后都能够理解文章中讲解的设计模式,然后有所收获。话不多说,让我们开始今天的冒险吧。

工厂模式的第一印象

对于初次听说这个设计模式的同学来说,你们的第一印象是什么呢?既然是工厂模式,那么肯定跟工厂的一些功能或者行为有关系。那么工厂都有哪些功能和行为呢?首先工厂收集原始材料,然后将原始的材料进行加工,处理,设计之后就变成了一个完整的产品或者部件。

这个过程对于产品的销售店,或者用户来说是不可见的。对于商家来说如果你想卖这个产品,你只需要去跟厂家沟通买一批这样的产品就行了。那对于用户来说,你想使用这个产品,只需要到卖这个产品的店里把它买回来就好了。

所以根据上面的推论,类比到代码中我们可以得出一些初步的结论:工厂模式封装了对象的创建过程,把创建和使用对象的过程进行了分离,解耦代码中对具体对象创建类的依赖。让代码更好维护,更方便扩展。

当然,如果想要知道这个设计模式是如何封装了对象的创建过程,并且减少了对具体类的依赖的话,我们还是要实践一下,通过一些例子或者开发中的场景学习如何使用好这个设计模式。那就让我们开始吧。

简单工厂

根据对代码封装和抽象的程度,工厂模式的实现方式有三种,它们分别是:简单工厂,工厂方法,以及抽象工厂。

我们首先来学习和了解一下简单工厂吧,假如你现在接手了一个生产蛋糕的程序,程序的部分代码如下:

// 泡芙蛋糕
const PUFF_CAKE = "PUFF_CAKE";
// 奶酪蛋糕
const CHEESE_CAKE = "CHEESE_CAKE";

class PuffCake {
  constructor() {
    this.name = "(泡芙蛋糕)";
  }
}
class CheeseCake {
  constructor() {
    this.name = "(奶酪蛋糕)";
  }
}

class CakeMaker {
  constructor(type) {
    if (type === PUFF_CAKE) {
      this.cake = new PuffCake();
    } else {
      this.cake = new CheeseCake();
    }
  }

  // 搅拌原料
  stirIngredients() {
    console.log(`开始搅拌${this.cake.name}`);
  }

  // 倒入模具中
  pourIntoMold() {
    console.log(`将${this.cake.name}倒入模具`);
  }

  // 烘烤蛋糕
  bakeCake() {
    console.log(`开始烘焙${this.cake.name}蛋糕`);
  }
}

// 制作蛋糕
const cakeMaker = new CakeMaker(PUFF_CAKE);
cakeMaker.stirIngredients();
cakeMaker.pourIntoMold();
cakeMaker.bakeCake();

现在这个制作蛋糕的程序需要新添加一种海绵蛋糕,你要怎么去修改这个程序,让它能够支持生产海绵蛋糕呢?也许你的第一反应就是将CakeMaker的构造函数进行修改,新增加一个类型的判断,比如像下面这样:

// ...
constructor(type) {
    if (type === PUFF_CAKE) {
      this.cake = new PuffCake();
    } else if (type === CHEESE_CAKE) {
      this.cake = new CheeseCake();
    } else {
      this.cake = new SpongeCake();
    }
}
// ...

这时,我们可以思考一下,虽然上面的方法的确可以帮助我们实现添加海绵蛋糕的功能,但是这样做会有一些问题。会有哪些问题呢?

首先,如果按照这个方式的话,我们以后只要添加新种类的蛋糕或者移除不受欢迎的蛋糕就必须要修改CakeMaker的构造函数。

这样做实在不是一个好的方案,而且每当我们在CakeMaker中新增加一个具体的蛋糕类的话,就相当于给这个类新增加了一个依赖。这样我们CakeMaker的依赖会越来越多,任何一个依赖类发生改变都可能导致我们的CakeMaker类不能够正常工作,出错的几率大大增加。

那么我们应该如何修改呢?我们应该减少CakeMaker类中对具体类的依赖,然后将生成蛋糕种类的过程从CakeMaker中移除。我们可以这样做:

// ...
// 封装蛋糕的创建过程
function cakeCategoryMaker(type) {
  let cake;
  if (type === PUFF_CAKE) {
    cake = new PuffCake();
  } else if (type === CHEESE_CAKE) {
    cake = new CheeseCake();
  } else {
    cake = new SpongeCake();
  }
  return cake;
}
// ...
class CakeMaker {
  constructor(type) {
    this.cake = cakeCategoryMaker(type);
  }
  // ...
}
// ...

当你看完了上面的代码,你可能会说,这不只是把代码从一个地方移到了另一个地方,好像没有发生什么根本的变化呀。的确是这样,但是我们来看一下,一旦我们把生成蛋糕种类的代码移到外面,我们的CakeMaker是不是减少了对具体蛋糕类的依赖。现在对于CakeMaker类来说,它的依赖只有cakeCategoryMakerCakeMaker不需要管你给我的蛋糕是什么类型的,我只负责对其进行加工制作,并不关心蛋糕的原料和种类。

而且,我们的cakeCategoryMaker还可以被其它的蛋糕加工程序所共享;如果以后还需要增加或者移除蛋糕种类的话,我们只需要在这一个地方修改就可以了。而不需要在每个加工蛋糕的代码中分别进行修改。这就是一个很好的编码习惯。

在实际的开发中,我们的程序中可能存在需要根据不同场景创建不同类型对象的功能,但是这些对象具有同样的属性和接口,或者需要根据不同的数据源创建相同的对象。那么这个时候,我们就可以把这一部分的逻辑抽离出来,然后在全局中进行使用。

这就是我们所说的简单工厂了,当然严格意义上来说,简单工厂不算是一个真正的设计模式。但是它很有用,它封装了根据不同类型来创建不同对象的过程,将我们的程序进行了解耦,这样便于程序的维护和扩展。是一个不错的编程习惯和技巧,值得我们学习和使用。

工厂方法

接下来我们来了解并学习工厂方法这种更高一级别的封装和抽象。在实际的开发中我们有时会写一些通用的组件,方便我们后续的业务开发使用。假如下面两个组件是已经开发好的组件:

class Toast {
  constructor(text) {
    this.text = text;
  }
  show() {
    console.log(`toast show: ${this.text}`);
  }
  hide() {
    console.log("toast hide");
  }
}

class Modal {
  constructor(text) {
    this.text = text;
  }
  show() {
    console.log(`modal show: ${this.text}`);
  }
  hide() {
    console.log("modal hide");
  }
}

const toast = new Toast("hello");
toast.show();
toast.hide();
// modal
const modal = new Modal("world");
modal.show();
modal.hide();

上面关于组件的代码是没有什么太大问题的,但是我们再仔细思考一下也许会觉得好像这两个组件都有showhide这两个方法。那么这就相当于是重复代码了,一般情况下如果出现了重复的代码那么说明我们还是有优化的地方的。

并且如果在不改变现有的思路的情况下,我们要再开发一个新的提示类型的组件的话,还是会在代码中重复这两个方法。那么有没有办法解决这个问题呢?当然有办法了,我们知道这种类型的组件都有showhide这两个方法。那么我们可以通过继承的方式从父类那里继承这两个方法,关于组件的具体创建过程我们可以在子类中进行实现。

具体实现的代码如下:

class CustomComponent {
  createComponent() {
    // TODO 需要被子类实现
  }
  show() {
    this.concreteComponent = this.createComponent();
    console.log(
      `this ${this.concreteComponent.name} show: ${this.concreteComponent.text}`
    );
  }
  hide() {
    console.log(`this component hide`);
  }
}

class ToastComponent extends CustomComponent {
  createComponent() {
    return {
      name: "toast",
      text: "hello",
    };
  }
}

class ModalComponent extends CustomComponent {
  createComponent() {
    return {
      name: "modal",
      text: "world",
    };
  }
}

const toast = new ToastComponent();
toast.show();
toast.hide();
const modal = new ModalComponent();
modal.show();
modal.hide();

这个解决方案的思路就是:我们在父类中把子类的一些通用的操作进行实现。然后具体组件的创建细节交给子类去解决。那么这样做就相当于把组件创建的过程进行了封装,父类不需要知道这个组件是如何创建的,被谁创建的。但是这个子类组件已经继承了父类的那些方法,所以可以直接使用父类的方法进行展示和隐藏。

因为在javascript中暂时还没有实现抽象类的功能,所以我们上面代码中的CustomComponent类从严格意义上说还不是一个抽象的父类。不过关系不是很大,思路和功能还是能够实现的。

那我们来总结一下工厂方法的特性:

  • 父类通过一个抽象的方法封装了对象的创建过程,对象的创建过程被延迟到子类中进行创建
  • 子类因为是从父类继承而来,所以可以使用父类已经实现好的方法
  • 工厂方法将我们的代码进行了解耦,创建组件的时候不需要再给父类传递对象的类型,由子类决定创建的对象的类型

抽象工厂

接下来我们来讲解一下抽象程度最高的抽象工厂,看过上一篇文章

以上是关于设计模式大冒险第三关:工厂模式,封装和解耦你的代码的主要内容,如果未能解决你的问题,请参考以下文章

工厂模式

设计模式大冒险第四关:单例模式,如何成为你的“唯一”

设计模式之工厂模式

创建和使用解耦——工厂模式详解(工厂方法+抽象工厂)

案例分析:设计模式与代码的结构特性——工厂方法模式

设计模式-创建型