程序开发中,功能实现方式灵活多样,但是代码质量或维护上不见得容易量化,那么设计模式就是一种让我们程序可以编写更加合理代码的有力工具。本节主要介绍设计模式中的单例,工厂和抽象工厂,建造者四种设计模式,利用这四种设计模式,我们可以清晰实例的创建考量,创建和实现的灵活分割。
单例模式
单例模式的概述
单例模式又称单体模式,保证类只有一个实例,并提供一个访问这个实例的方法。换句话说,我们第二次时候用同一个类创建一个实例,该实例应该与第一次的实例完全相同。
可能上面的术语太过生硬了,那让我们类比一下我们生活中一些示例。
- 我们大家都用过一些软件写过记录或者文章,当我们书写很多了之后由于一些原因退出了,第二次重新打开这个软件写我们之前的文章我们都希望是之前编辑的文章吧,而不是,每次打开软件我们的文章就都重新创建,那这就不是很郁闷嘛。所以我们每次都能能够编辑之前的文章用的就是存档(草稿)的功能,这篇文章就可以类比单例一个示例
- 编程中我们也经常用到,很多时候我们也都是希望使用同一个对象或者实例,比如数据库连接,配置文件缓存、浏览器中的
window/document等等,这种创建实例都会带来不少的资源浪费或者导致访问行为不一致。
从以上的情景,我们不难看出,这些例子都有这些特点:
- 每次访问者来访问,返回都是同一个实例
- 如果该实例一开始没有创建,那么当我们访问该实例的全局访问方法的时候会自行创建
所以我们不难写出这样的代码(为了方便学习,后续讲解都会使用es6讲解)
class Singleton {
static _modal = null;
constructor() {
if (Modal._modal) {
// 当前已经创建过实例了,直接返回缓存中的实例
return Modal._modal;
}
// 没有创建过实例,需要自行创建实例
return (Modal._modal = this);
}
// 全局可以访问实例的方法
static getInstance() {
if (Modal._modal) {
// 当前已经创建过实例了,直接返回缓存中的实例
return Modal._modal;
}
// 没有创建过实例,需要自行创建实例
return (Modal._modal = new Modal());
}
}
const modal1 = new Modal();
const modal2 = new Modal();
console.log(modal1 === modal2); // true
const modal3 = new Modal();
const modal4 = Modal.getInstance(); // 全局访问方法
console.log(modal3 === modal4); // true
稍微解释一下,这个构造函数在内部维护(或者直接挂载自己身上)一个实例,第一次执行 new 的时候判断这个实例有没有创建过,创建过就直接返回,否则走创建流程。从以上我们可以总结出单例模式的通用实现
Singleton:特定类,这是我们需要访问的类,访问者要拿到的是它的实例;_instance:单例,是特定类的实例,特定类一般会提供 getInstance 方法来获取该单例;getInstance:获取单例的方法,或者直接由 new 操作符获取;
单例的通用实现方式
但是上述的方法有个很大的问题,我们可以看到实例_instance是作为静态属性直接暴露在全局,我们在外部是可以随时修改的,我们以上面的例子进行修改
const modal3 = new Singleton();
console.log(modal3); // Singleton {}
modal3._instance = { text: "我是测试数据" };
console.log(modal3); // Singleton { _instance: { text: '我是测试数据' } }
可以看到实例在外部可随时被修改,这是不是很可怕。
那么如何解决这个问题呢? 解决这个问题呢,我们可以使用以下几种方式进行解决
-
使用IIFE方式创建单例模式(了解)
const Singleton = (function () { let _instance = null; // 存储单例 const Singleton = function () { if (_instance) return _instance; // 判断是否已有单例 _instance = this; return _instance; }; Singleton.getInstance = function () { if (_instance) return _instance; _instance = new Singleton(); return _instance; }; return Singleton; })(); const visitor1 = new Singleton(); const visitor2 = new Singleton(); // 既可以 new 获取单例 const visitor3 = Singleton.getInstance(); // 也可以 getInstance 获取单例 console.log(visitor1 === visitor2); // true console.log(visitor1 === visitor3); // true这种方式虽然使用
_instance变量保存实例,所以外部代码无法直接修改。但是这种方法会造成闭包的额外开销,而且可读性性变得很差,提高了复杂度。再者,内部的类才是我们想要的,该方式不过是返回了一个新的类 -
块级作用域方式创建单例(了解)
IIFE 方式本质还是通过函数作用域的方式来隐藏内部作用域的变量,有了 ES6 的
let/const之后,可以通过{ }块级作用域的方式来隐藏内部变量:let getInstance { let _instance = null // 存储单例 const Singleton = function() { if (_instance) return _instance // 判断是否已有单例 _instance = this this.init() // 初始化操作 return _instance } Singleton.prototype.init = function() { this.foo = 'Singleton Pattern' } getInstance = function() { if (_instance) return _instance _instance = new Singleton() return _instance } } const visitor1 = getInstance() const visitor2 = getInstance() console.log(visitor1 === visitor2)// true -
单一赋能方法(推荐写法)
/* Person 类 */ class Person { constructor(name, age) { this.name = name this.age = age } } /* 单例模式的赋能方法 */ function Singleton(FuncClass) { let _instance return new Proxy(FuncClass, { construct(target, args) { return _instance || (_instance = Reflect.construct(FuncClass, args)) // 使用 new FuncClass(...args) 也可以 } }) } const PersonInstance = Singleton(Person) const person1 = new PersonInstance('coder', 25) const person2 = new PersonInstance('lyh', 23) console.log(person1 === person2) // true采用单一职责原则,将业务类和单例模式进行逻辑解耦,利于扩展和后续维护。
惰性单例与饿汉单例
- 惰性单例: 又称懒汉式单例,指的是延迟创建类的单例,一般是在
new执行构造函数的时候才进行创建。解决的主要场景是:但我们实例化的过程开销大,耗性能的时候,但是使用的地方并不需要很及时的时候,就可以使用惰性创建。我们之前的例子呢,都是惰性单例的。 - 饿汉式单例: 在程序一开始执行的时候就进行单例的创建。这种应用的比较少,比如我们如果有多个任务执行的时候,我们会一个使用实例维护状态时候,那么需要快速同步,这时候饿汉式单例就是不错的选择。 那么这两种具体有什么区别呢?在写法上我们可以看一下对比
class People {
constructor(name) {
this.name = "coder-lyh";
}
}
const HungrySingleton = (function (People) {
// 区别在这里
// 初始化的时候就把实例创建了
let _instance = new People();
console.log("HungrySingleton初始化", _instance);
return function () {
return _instance;
};
})(People);
const LazySingleton = (function (People) {
// 区别在这里
let _instance;
console.log("LazySingleton初始化", _instance);
return function () {
// new 的过程才进行创建实例
return _instance || (_instance = new People());
};
})(People);
const HungryPeople = HungrySingleton(People);// HungrySingleton初始化 People { name: 'coder-lyh' }
const LazyPeople = LazySingleton(People);// LazySingleton初始化 undefined
单例模式的实际应用
在项目中,相信大家都是用过全屏loading蒙层,我们都知道使用这个效果都是只能创建出一个loading实例。接下来笔者以ElementUI为例,首先Element的全屏loading调用形式有以下形式
// 1. 指令形式
Vue.use(Loading.directive)
// 2. 服务形式
Vue.prototype.$loading = service
- 上面的是指令形式注册,使用的方式
<div :v-loading.fullscreen="true">...</div>; - 下面的是服务形式注册,使用的方式
this.$loading({ fullscreen: true });
用服务方式使用全屏
Loading是单例的,即在前一个全屏Loading关闭前再次调用全屏Loading,并不会创建一个新的Loading实例,而是返回现有全屏Loading的实例。
为了简便阅读,我们省略一些代码
import Vue from 'vue'
import loadingVue from './loading.vue'
const LoadingConstructor = Vue.extend(loadingVue)
let fullscreenLoading
const Loading = (options = {}) => {
// 之前已经创建过全局loading实例,直接返回
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading
}
let instance = new LoadingConstructor({
el: document.createElement('div'),
data: options
})
if (options.fullscreen) {
// 保存实例
fullscreenLoading = instance
}
return instance
}
export default Loading
解释一下以上的代码,这里的单例是我们fullscreenLoading,保存在全局中(闭包)。也可以类比是我们的缓存单例。这就是单例的经典运用
单例的优缺点
单例模式主要解决的是内存资源的问题,并保持访问的一致性。所以我们不难分析出它具有以下的优点:
- 内存开销小,因为内存中只会存在一个实例,不需要不断的创建或者销毁。
- 可以解决对资源的多重占用,比如文件操作的时候,可以避免对文件同时访问
- 可以减少垃圾回收的压力,CPU资源占用更少
虽然单例可以极大程度上帮我们节省资源,但是也是有缺点的,主要体现在代码扩展性
- 单例模式对扩展不友好,因为实例被自行实例化了
- 与单一原则冲突,类应该只关心内部逻辑,而不关心实例的创建
那么单例模式有什么样的使用场景呢?
- 当一个类的实例化过程消耗的资源过多,可以使用单例模式来避免性能浪费;
- 当项目中需要一个公共的状态,那么需要使用单例模式来保证访问一致性;
工厂模式
工厂模式的概述
工厂模式:根据不同输入返回不同类的实例,一般用来创建同一类型的对象。实现的主要是将类的实现和对象的创建进行分割。
举个例子来理解一下,我们去餐馆点菜,我们跟老板说来一份混沌和一份杂酱面,然后我们就坐在桌上等老板煮好拿给我们,我们不关心他是怎么包和煮的,反正我们就是等着吃就好了。
以上的过程呢,就很类似我们的工厂模式,它具有以下的特点:
- 访问者(我)只需要访问对应的属性(菜名),就可以从工厂类(老板)获取对应的实例(菜)
- 访问者不需要关心实例的创建过程
工厂模式的通用实现
根据上面的例子我们可以提炼一下工厂模式,饭店可以被认为是工厂类(Factory),菜品是产品(Product),如果我们希望获得菜品实例,通过工厂类就可以拿到产品实例,不用关注产品实例创建流程。主要有下面几个概念:
- Factory :工厂,负责返回产品实例;
- Product :产品,访问者从工厂拿到产品实例;
class Factory {
static getInstance(type) {
switch (type) {
case "Product1":
return new Product1();
case "Product2":
return new Product2();
default:
throw new Error("您输入的类型不存在");
}
}
}
class Product1 {
constructor() {
this.name = "Product1";
}
getName() {
return this.name;
}
}
class Product2 {
constructor() {
this.name = "Product2";
}
getName() {
return this.name;
}
}
const prod1 = Factory.getInstance("Product1");
console.log(prod1.getName()); //Product1
const prod2 = Factory.getInstance("Product3"); //Error: 您输入的类型不存在
当然以上是类的实现,我们可以看到每一个产品类都是在工厂类里面进行实例化,访问者不需要关心产品的创建和实现。
由于我们可能实际过程中呢,也有函数式的工厂,原理是一样的道理。
class Dog {
constructor(name) {
this.name = name;
}
run() {
console.log(`${this.name}正在奔跑`);
}
}
class Cat {
constructor(name) {
this.name = name;
}
run() {
console.log(`${this.name}正在奔跑`);
}
}
function createAnimal(type, name) {
switch (type) {
case "dog":
return new Dog(name);
case "cat":
return new Cat(name);
default:
throw new Error("传入类型不合法");
}
}
const wangcai = createAnimal("dog", "旺财");
wangcai.run(); // 旺财正在奔跑
const miaomiao = createAnimal("cat", "喵星人");
miaomiao.run();// 喵星人正在奔跑
源码中的工厂模式
-
Vue或者React中的工厂模式 我们都知道现在流行框架都是利用虚拟节点virtual node的方式进行减少真实的DOM操作。那么这些虚拟节点都是怎么创建的呢?我们都可以注意到源码中,都提供了createElement方法来生成VNode,用来映射真实DOM节点。// Vue中创建Image节点 createElement('img', { class: 'avatar', attrs: { src: '../avatar.jpg' } }) // React中创建Image节点 React.createElement('img', { src: '../avatar.jpg', className: 'avatar' })我们从中不难想象出,
createElement的方法应该是这样的结构class Vnode (tag, data, children) { ... } function createElement(tag, data, children) { return new Vnode(tag, data, children) }所以,对于使用者来说,调用
createElement十分的简单易懂,但是具体的实现和创建过程很复杂,是由框架实现,我们开发者无需关心。 -
vue-router的工厂模式// src/index.js export default class VueRouter { constructor(options) { this.mode = mode // 路由模式 switch (mode) { // 简单工厂 case 'history': // history 方式 this.history = new HTML5History(this, options.base) break case 'hash': // hash 方式 this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': // abstract 方式 this.history = new AbstractHistory(this, options.base) break default: // ... 初始化失败报错 } } }具体的路由模式,相信大家都很熟悉
hashRouter和historyRouter了,笔者这里就不过多解释。
工厂模式的优缺点
工厂模式实现了对象的创建和实现进行分离,具有以下优点:
- 封装性: 代码结构清晰,将类的实现进行了解耦,符合最少知识原则
- 高扩展性: 隔离了访问者和对象的创建流程,符合开放封闭原则
同时,它也带来了不可避免的缺点: 带来了额外的系统复杂度,容易造成不必要的抽象。
那么什么时候使用这个模式呢?
- 对象的创建比较复杂,而访问者无需知道创建的具体流程;
- 处理大量具有相同属性的小对象;
什么时候不该用工厂模式:滥用只是增加了不必要的系统复杂度,过犹不及。
抽象工厂模式
概述
抽象工厂模式相比工厂模式多了两个字“抽象”,没错,其实抽象工程最重要的能力就是将每个环节的类进行抽象化成类簇。
如何理解上面意思呢?还是以前面点餐的例子来说,我们先看我们点的菜这一个类别,是不是所有的菜都具有菜名,每个菜不管是菜,肉,汤我们都知道它是可以吃的。所以,所有的菜品我们可以抽象化成一个族类,这里暂时叫做”菜族“。(当然我这个菜鸟可能也是这一族的,😄)。OK,我们理解了,那么,我们在想一下,除了我们点餐这家餐馆,其他餐馆是不是也可以制作这几道菜呢?是吧,所以我们也把他抽象成了“店族”。
从中我们可以看出,抽象工厂其实对类的工厂进行抽象化,使其业务用于”菜族“的创建,而不是负责创建某一个菜的实例。关键在于使用抽象类制定了实例的结构,调用者直接面向实例的结构编程,从实例的具体实现中解耦。
说人话就是,以前我们的工厂不再负责创建某个产品了,而是负责定义每个产品的具体结构,由调用者自己去实现,哎,没错,这就是类似苹果公司的操作,哎,我不制作手机,我只给图纸,厂商自己去弄吧,弄完给我钱就完事了。
抽象工厂的通用实现
同样我们提炼一下抽象工程模式的特点
在上面的例子,不管饭店,还是菜品,都是被抽象化后的种类,实现抽象类的菜品是具体的产品,通过工厂拿到实现了不同抽象类的产品,这些产品可以根据实现的抽象类被区分为类簇。主要有下面几个概念:
- Factory :工厂,负责返回产品实例;
- AbstractFactory :虚拟工厂,制定工厂实例的结构;
- Product :产品,访问者从工厂中拿到的产品实例,实现抽象类;
- AbstractProduct :产品抽象类,由具体产品实现,制定产品实例的结构;
概略图如下:
是吧,真就一图胜千言,所以希望读者平时也能够自己画画图,这是学设计模式特别有用的技巧。
由于js并没有抽象类,但这不影响我们学习它,代码具体怎么实现呢,我们来看看:
class AbstractProduct {
constructor() {
if (new.target === AbstractProduct) {
throw new Error("抽象类不能被实例化");
}
}
operate() {
throw new Error("抽象方法不能被调用");
}
}
class Product1 extends AbstractProduct {
constructor(kind = "product1") {
super();
this.kind = kind;
}
operate() {
console.log(`${this.kind} can operate machine`);
}
}
class Product2 extends AbstractProduct {
constructor(kind = "product2") {
super();
this.kind = kind;
}
operate() {
console.log(`${this.kind} can operate machine`);
}
}
class AbstractFactory {
constructor() {
if (new.target === AbstractFactory) {
throw new Error("抽象类不能被实例化");
}
}
createProduct() {
throw new Error("抽象方法不能被调用");
}
}
class Factory extends AbstractFactory {
constructor(type) {
super();
this.type = type;
}
createProduct() {
const type = this.type;
switch (type) {
case "product1":
return new Product1();
case "product2":
return new Product2();
default:
return null;
}
}
}
const factory1 = new Factory("product1");
factory1.createProduct().operate(); //product1 can operate machine
const factory2 = new Factory("product2");
factory2.createProduct().operate();// product2 can operate machine
我们在实际编码中,看情况选择抽象工厂或者工厂模式,并不是每个工厂都需要继承抽象工厂类,如果只有一个工厂的时候,完全可以直接使用工厂模式,这需要开发者灵活选择。
抽象工厂模式的优缺点
抽象模式的优点:
抽象产品类将产品的结构抽象出来,访问者不需要知道产品的具体实现,只需要面向产品的结构编程即可,从产品的具体实现中解耦;
抽象模式的缺点:
- 扩展新类簇的产品类比较困难,因为需要创建新的抽象产品类,并且还要修改工厂类,违反开闭原则;
- 带来了系统复杂度,增加了新的类,和新的继承关系;
使用场景
如果一组实例都有相同的结构,那么就可以使用抽象工厂模式。工厂模式和抽象工厂模式的区别:
- 工厂模式主要关注单独的产品实例的创建;
- 抽象工厂模式主要关注产品类簇实例的创建,如果产品类簇只有一个产品,那么这时的抽象工厂模式就退化为工厂模式了;根据场景灵活使用即可。
建造者模式
概述
建造者模式又称生成器模式,分步进行构建一个复杂对象,并允许按步骤构造。同样的构建过程可以采用不同的表示,将一个复杂对象的构建层与表示层分离。
前文我们提过,工厂模式不关心对象的创建过程,只关心创建出来的结果,与其不同的是,建造者模式关注的是对象的创建过程。我们举个生活中的例子来解释一下建造者模式:
假定我们需要建造一个车,车这个产品是由多个部件组成,车身、引擎、轮胎。汽车制造厂一般不会自己完成每个部件的制造,而是把部件的制造交给对应的汽车零部件制造商,自己只进行装配,最后生产出整车。整车的每个部件都是一个相对独立的个体,都具有自己的生产过程,多个部件经过一系列的组装共同组成了一个完整的车。
在场景中,有以下特点:
- 整车制造厂(指挥者)无需知道零部件的生产过程,零部件的生产过程一般由零部件厂商(建造者)来完成;
- 整车制造厂(指挥者)决定以怎样的装配方式来组装零部件,以得到最终的产品;
建造模式的通用实现
我们提炼一下建造者模式,这里的厂家就相当于指挥者(Director),厂家负责将不同的部件组装成最后的产品(Product),而部件的生产者是部件厂家相当于建造者(Builder),我们通过指挥者就可以获得希望的复杂的产品对象,再通过访问不同指挥者获得装配方式不同的产品。主要有下面几个概念:
- Director: 指挥者,调用建造者中的部件具体实现进行部件装配,相当于整车组装厂,最终返回装配完毕的产品;
- Builder: 建造者,含有不同部件的生产方式给指挥者调用,是部件真正的生产者,但没有部件的装配流程;
- Product: 产品,要返回给访问者的复杂对象;
概念图如下:
具体代码怎么实现呢?我们看看:
// 建造者,部件生产
class ProductBuilder {
constructor(param) {
this.param = param
}
/* 生产部件,part1 */
buildPart1() {
// ... Part1 生产过程
this.part1 = 'part1'
}
/* 生产部件,part2 */
buildPart2() {
// ... Part2 生产过程
this.part2 = 'part2'
}
}
/* 指挥者,负责最终产品的装配 */
class Director {
constructor(param) {
const _product = new ProductBuilder(param)
_product.buildPart1()
_product.buildPart2()
return _product
}
}
// 获得产品实例
const product = new Director('param')
通常建造者模式会与链模式互相配合,具备可读性,链模式我们后文继续学,这里大家可以先看一下建造模式与链模式是怎么搭配使用的:
// 建造者,汽车部件厂家
class CarBuilder {
constructor(param) {
this.param = param
}
/* 生产部件,part1 */
buildPart1() {
this.part1 = 'part1'
return this
}
/* 生产部件,part2 */
buildPart2() {
this.part2 = 'part2'
return this
}
}
// 汽车装配,获得产品实例
const benchi1 = new CarBuilder('param')
.buildPart1()
.buildPart2()
实战
-
重构多参数函数 我们平时书写函数的时候,可能会遇到很多参数的构造函数,当我们想要设置值的时候,我们很难清晰的知道每个属性的位置。比如
// 汽车建造者 class CarBuilder { constructor(engine, weight, height, color, tyre, name, type) { this.engine = engine this.weight = weight this.height = height this.color = color this.tyre = tyre this.name = name this.type = type } } const benchi = new CarBuilder('大马力发动机', '2ton', 'white', '大号轮胎', '奔驰', 'AMG')
-
重构实现版本一:
class CarBuilder { constructor(engine, weight, height, color, tyre, name, type) { this.engine = engine; this.weight = weight; this.height = height; this.color = color; this.tyre = tyre; this.name = name; this.type = type; } setCarProperty(key, value) { if (Object.getOwnPropertyNames(this).includes(key)) { this[key] = value; return this; } throw new Error(`Key Error: ${key} 不是实例上的属性`); } } const benchi = new CarBuilder() .setCarProperty("engine", "发动机") .setCarProperty("weight", "2ton") .setCarProperty("height", "2000mm") .setCarProperty("color", "white") .setCarProperty("tyre", "大号轮胎") .setCarProperty("name", "奔驰") .setCarProperty("type", "AMG"); console.log(benchi); /** CarBuilder { engine: '发动机', weight: '2ton', height: '2000mm', color: 'white', tyre: '大号轮胎', name: '奔驰', type: 'AMG' } */
通过设置该方式我们可以不用关心属性的参数位置,只需要在对应的属性设置好对应的值即可。那么还有没有更加骚一点的操作呢,是有的,我们看看第二版的实现:
-
重构版本二
class CarBuilder { constructor(engine, weight, height, color, tyre, name, type) { this.engine = engine; this.weight = weight; this.height = height; this.color = color; this.tyre = tyre; this.name = name; this.type = type; } setPropertyByChain() { Object.getOwnPropertyNames(this).forEach((key) => { // 构造函数名 const funcName = `set${key.replace(/^\w/g, (str) => str.toUpperCase())}`; // 保存到实例中 this[funcName] = (value) => { this[key] = value; return this; }; }); return this; } } const baoma = new CarBuilder() .setPropertyByChain() .setEngine("大马力发动机") .setWeight("2ton") .setHeight("2000mm") .setColor("white") .setTyre("大号轮胎") .setName("奔驰") .setType("AMG"); console.log(baoma); /** CarBuilder { engine: '大马力发动机', weight: '2ton', height: '2000mm', color: 'white', tyre: '大号轮胎', name: '奔驰', type: 'AMG', setEngine: [Function (anonymous)], setWeight: [Function (anonymous)], setHeight: [Function (anonymous)], setColor: [Function (anonymous)], setTyre: [Function (anonymous)], setName: [Function (anonymous)], setType: [Function (anonymous)] } */我们直接把每个属性变成对应的设置函数,根据属性函数设置对应的值即可。但是这样有个比较大的问题,就是我们平白无故多了一些列的
setter,这就需要我们合理取舍,选择最适合的方式。
建造模式的优缺点
建造者模式的优点:
- 使用建造者模式可以使产品的构建流程和产品的表现分离,也就是将产品的创建算法和产品组成的实现隔离,访问者不必知道产品部件实现的细节;
- 扩展方便,如果希望生产一个装配顺序或方式不同的新产品,那么直接新建一个指挥者即可,不用修改既有代码,符合开闭原则;
- 更好的复用性,建造者模式将产品的创建算法和产品组成的实现分离,所以产品创建的算法可以复用,产品部件的实现也可以复用,带来很大的灵活性;
建造者模式的缺点:
- 建造者模式一般适用于产品之间组成部件类似的情况,如果产品之间差异性很大、复用性不高,那么不要使用建造者模式;
- 实例的创建增加了许多额外的结构,无疑增加了许多复杂度,如果对象粒度不大,那么我们最好直接创建对象;
建造者模式的适用场景
- 相同的方法,不同的执行顺序,产生不一样的产品时,可以采用建造者模式;
- 产品的组成部件类似,通过组装不同的组件获得不同产品时,可以采用建造者模式;
总结
在这一章,我们主要学习了四种创建型行为模式,分别是:单例模式,工厂模式,抽象工厂模式,建造者模式。其中我们可以理解他们之前的区别和联系
- 单例模式是一种确保类创建出来的实例在内存中只有一份,可以节约内存消耗,保证访问的统一性。但是同时也存在着扩展差和结构混乱的不足。其中,单例模式又分为懒汉单例和饿汉单例,懒汉式单例是使用的时候才进行实例的创建,饿汉式单例是程序初始化的时候就立马进行实例的创建。当我们需要考虑性能浪费和公共状态问题的时候,我们可以考虑单例模式。
- 工厂模式和抽象工厂模式是很相似的设计的模式,可以这么理解,工厂模式是抽象工厂模式的一种弱化模式。两种设计模式的共同点都是不关心实例的创建和实现过程,通过工厂方法将实现和创建过程进行了解耦,提高了扩展性。不足便是提高了复杂度,增加抽象性。而抽象工厂模式和工厂模式之间的区别在于工厂类的族簇化和具体化,抽象工厂的工厂类是一个族群,可以实现多种多样的工厂,而工厂模式的工厂类则只有一个。抽象工厂模式主要关注产品类簇实例的创建,如果产品类簇只有一个产品,那么这时的抽象工厂模式就退化为工厂模式了;根据场景灵活使用即可。
- 建造者模式和工厂模式最终都是创建一个完整的产品,但是在建造者模式中我们更关心对象创建的过程,将创建对象的方法模块化,从而更好地复用这些模块。当然建造者模式与工厂模式也是可以组合使用的,比如建造者中一般会提供不同的部件实现,那么这里就可以使用工厂模式来提供具体的部件对象,再通过指挥者来进行装配。