乐趣区

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

这篇文章是对于 设计模式系列 的第三篇文章,这一系列的每一篇文章都会通过生存中的小例子以及一些简略的比喻让大家明确每一个设计模式要 解决的是什么问题,而后通过什么形式解决的。心愿大家在看过每篇文章之后都可能了解文章中解说的设计模式,而后有所播种。话不多说,让咱们开始明天的冒险吧。

工厂模式的第一印象

对于首次据说这个设计模式的同学来说,你们的第一印象是什么呢?既然是工厂模式,那么必定跟工厂的一些性能或者行为有关系。那么工厂都有哪些性能和行为呢?首先工厂收集原始资料,而后将原始的资料进行加工,解决,设计之后就变成了一个残缺的产品或者部件。

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

所以依据下面的推论,类比到代码中咱们能够得出一些初步的论断:工厂模式封装了对象的创立过程,把创立和应用对象的过程进行了拆散,解耦代码中对具体对象创立类的依赖。让代码更好保护,更不便扩大

当然,如果想要晓得这个设计模式是如何封装了对象的创立过程,并且缩小了对具体类的依赖的话,咱们还是要实际一下,通过一些例子或者开发中的场景学习如何应用好这个设计模式。那就让咱们开始吧。

简略工厂

依据对代码 封装 形象 的水平,工厂模式的实现形式有三种,它们别离是:简略工厂 工厂办法 ,以及 形象工厂

咱们首先来学习和理解一下简略工厂吧,如果你当初接手了一个生产蛋糕的程序,程序的局部代码如下:

// 泡芙蛋糕
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 类从严格意义上说还不是一个形象的父类。不过关系不是很大,思路和性能还是可能实现的。

那咱们来总结一下工厂办法的个性:

  • 父类通过一个形象的办法封装了对象的创立过程,对象的创立过程被提早到子类中进行创立
  • 子类因为是从父类继承而来,所以能够应用父类曾经实现好的办法
  • 工厂办法将咱们的代码进行理解耦,创立组件的时候不须要再给父类传递对象的类型,由子类决定创立的对象的类型

形象工厂

接下来咱们来解说一下形象水平最高的 形象工厂 ,看过上一篇文章???? 设计模式大冒险第二关:装璜者模式,煎饼果子的主场 的同学应该晓得,因为上次你给煎饼果子的老板帮了个忙。所以他晓得你的编程程度不错,明天又来找你帮忙啦。

这次的问题是这样的,老板说他那边有一个获取煎饼果子原材料的程序,然而最近左近的一个菜市场提供的蔬菜不是很陈腐。所以想换一个菜市场去买原材料,然而更换菜市场的话,之前程序一些统计的数据就不精确了,所以须要让你帮忙批改一下现有的程序。当初的程序局部代码如下:

class PanCakeMaterials {constructor(vegetableMarketName) {this.vegetableMarketName = vegetableMarketName;}

  getEgg() {if (this.vegetableMarketName === "VEGETABLE_MARKET_NAME_A") {return "a_market_egg";}
    if (this.vegetableMarketName === "VEGETABLE_MARKET_NAME_B") {return "b_market_egg";}
  }

  // ... 其它的原料
}

const panCakeMaterials = new PanCakeMaterials("VEGETABLE_MARKET_NAME_A");
console.log(panCakeMaterials.getEgg());  // a_market_egg

咱们能够看到当初这个获取原材料的程序 尽管实现了获取原材料的性能,然而当初的扩展性太差。如果增加了新的菜市场或者移除不应用的菜市场的话,就须要批改程序 。所以又到了你大展身手的时候了,巧的是你刚刚学习完工厂模式的 形象工厂 这个解决方案。所以你晓得该如何重构当初的代码了。

首先原来的代码太依赖咱们给的类型值了,如果输出的类型值有问题的话,那么整个获取原材料的程序就没有方法运行起来。所以咱们须要将每种食材获取的过程封装起来,由一个 VegetableMarketProvider 类来负责,而后对于 PanCakeMaterials 来说,咱们只须要将 VegetableMarketProvider 子类的实例化对象当做传给 PanCakeMaterials 类的参数进行初始化就能够了

实现的代码如下所示:

class VegetableMarketProvider {provideEgg() {}}

class FirstVegetableMarketProvider extends VegetableMarketProvider {provideEgg() {return "a_market_egg";}
}

class SecondVegetableMarketProvider extends VegetableMarketProvider {provideEgg() {return "b_market_egg";}
}

class PanCakeMaterials {constructor(vegetableMarketProvider) {this.vegetableMarketProvider = vegetableMarketProvider;}

  getEgg() {return this.vegetableMarketProvider.provideEgg();
  }

  // ... 其它的原料
}

const firstVegetableMarketProvider = new FirstVegetableMarketProvider();
const secondVegetableMarketProvider = new SecondVegetableMarketProvider();
const panCakeMaterials = new PanCakeMaterials(firstVegetableMarketProvider);
console.log(panCakeMaterials.getEgg());  // a_market_egg
const secondPanCakeMaterials = new PanCakeMaterials(secondVegetableMarketProvider);
console.log(secondPanCakeMaterials.getEgg());  // b_market_egg

咱们来剖析一下优化后的代码,首先咱们写了一个形象的 VegetableMarketProvider 类,这个类外面的所有办法也都是形象的,须要由子类去实现具体的办法。每一个子类对应一个具体的菜市场,这样的话每一个菜市场提供的原材料也就晓得是什么了

对于 PanCakeMaterials 类来说,咱们不再传递一个示意菜市场类型的字符串了;取而代之的是,传递一个菜市场的实例。这样的话当咱们初始化 PanCakeMaterials 的时候,对应的菜市场也就确定了,那对应的原材料也就确定了

这样的益处有哪些呢?首先如果咱们要更换菜市场,再也不须要扭转 PanCakeMaterials 类的代码了,只须要更换传给 PanCakeMaterials 类的参数就能够了。而后如果须要增加新的菜市场的话,只须要新加一个 VegetableMarketProvider 的子类,在子类外面实现相应原材料的获取。这就体现了咱们程序设计中的一个准则,对批改敞开,对扩大凋谢

咱们通过下面的优化,把获取原材料的过程封装到 VegetableMarketProvider 的子类中,而后通过对象组合的形式实现了对菜市场的更换 ,这样的形式进一步解耦了咱们的代码, 每一个类都各司其职,保障了职责的繁多

那咱们再来简略总结一下 形象工厂 这个形式吧。

  • 通过应用一个抽象类,把相干的接口进行了定义,而后继承这个抽象类的子类都具备雷同的接口和属性。这样用到这些子类的类能够对这些类进行接口编程,而不是在针对具体的类
  • 对象的创立过程被封装在子类中,这样实现了代码的封装以及类依赖的解耦
  • 应用不同的子类,通过对象的组合,咱们能够实现咱们想要的创立不同对象的性能

文章到这里就完结了,如果大家有什么问题和疑难欢送大家在文章上面留言,或者在这里提出来。也欢送大家关注我的公众号关山不难越,获取更多对于设计模式解说的内容。

上面是这一系列的其它的文章,也欢送大家浏览,心愿大家都可能把握好这些设计模式的应用场景和解决的办法。如果这篇文章对你有所帮忙,那就点个赞,分享一下吧~

  • 设计模式大冒险第二关:装璜者模式,煎饼果子的主场
  • 设计模式大冒险第一关:观察者模式

文章封面图起源:unDraw

退出移动版