【前端八股】设计模式之一:创建型模式

58 阅读14分钟

一、设计模式的分类

总体来说设计模式分为三大类:创建型模式,结构型模式,行为型模式。

  1. 创建型模式(Creational) :关注对象的实例化过程,包括了如何实例化对象、隐藏对象的创建细节等。常见的创建型模式有构造器模式、单例模式、抽象工厂模式等。
  2. 结构型模式(Structural) :关注对象之间的组合方式,以达到构建更大结构的目标。这些模式帮助你定义对象之间的关系,从而实现更大的结构。常见的结构型模式有适配器模式、装饰器模式、代理模式等。
  3. 行为型模式(Behavioral) :关注对象之间的通信方式,以及如何合作共同完成任务。这些模式涉及到对象之间的交互、责任分配等。常见的行为型模式有观察者模式、策略模式、命令模式等。

二、创建型模式 (Creational Patterns)

创建型模式用来描述“如何创建对象”,它的主要特点是 “将对象的创建和使用分离”。

1. 构造器模式 (Builder Pattern)

构建器模式的目的是将复杂对象的构造与其表示分离,以便相同的构造过程可以创建不同的表示。这种类型的分离减少了对象的大小。随着每个实现包含在不同的构建器对象中,设计结果变得更加模块化。在JavaScript中,我们使用构造函数去初始化对象,就是应用了构造器模式。

分支.png 类图

image.png

  • 产品(Product) :表示正在构建的复杂对象。构造器模式的目标是构建这个产品。
  • 抽象构造器(Abstract Builder) :定义了构建产品的步骤和方法,但没有具体的实现。不同的具体构造器可以实现不同的构建步骤,从而创建不同的产品变体。
  • 具体构造器(Concrete Builder) :实现了抽象构造器定义的方法,完成了产品的构建过程。每个具体构造器负责构建特定的产品变体。
  • 指导者(Director) :负责控制构造的过程。它通过将客户端与具体构造器分离,确保产品的构建是按照一定顺序和规则进行的。

栗子.png 举个栗子

假如你是Riot公司的一名程序员,领导让你使用js重新开发lol这款游戏。

b2851248ceb866941d50dcd565046ac7.jpeg

// 首先创建一名英雄到系统内
const Aixi = {
    name: '艾希',
    title: '寒冰射手',
    role: '射手'
}

// 创建第二名英雄
const Zhaoxin = {
    name: '赵信',
    title: '德邦总管',
    role: '战士'
};

// 想想还有100多名英雄没创建呢,OMG.....

于是写个构造函数

function Hero(name, title, role) {
    this.name = name;
    this.title = title;
    this.role = role;
};

// 创建英雄就new一下就搞定了
const Jiakesi = new Hero('贾克斯', '武器大师', '战士');
const Yidashi = new Hero('易大师', '无极剑圣', '刺客');

效果.png 构造器模式的效果

  • 分离构建过程和表示:通过构造器模式 ,将复杂对象的构建过程与其最终表示分离,使得构建过程更加清晰可控。
  • 支持不同的表示:构造器模式允许通过不同的具体构造器 (Concrete Builder) 实现来创建具有不同表示的对象。
  • 更好的可扩展性:如果需要添加新的产品变体,只需创建一个新的具体建造者(Concrete Builder) 即可,而无需修改已有的代码。
  • 隐藏产品的内部结构:客户端 (user) 只需与抽象建造者 (Abstract Builder) 和指导者 (Director) 交互,无需关心产品的内部构建细节。

2. 原型模式(Prototype Pattern)

原型模式允许通过复制(或克隆)一个已经存在的实例来返回新的实例,而不是通过传统的构造函数来创建新实例。以更高效的方式创建新对象,同时避免了与对象类的直接耦合。核心概念是在原型对象的基础上进行克隆,使得新对象具有与原型相同的初始状态。

当对象创建成本高或创建过程很复杂时,通过复制一个现有的对象,然后基于复制出来的对象进行修改是一个非常好的方法。

借助Prototype来实现对象的创建和原型的继承,就是在应用原型模式。

分支.png 类图

image.png

  • 抽象原型(Prototype) :声明克隆方法,作为所有具体原型的基类或接口。
  • 具体原型(Concrete Prototype) :实现克隆方法,从自身创建一个副本。
  • 客户端(Client) :使用原型对象的客户端代码,在需要新对象时通过克隆现有对象来创建新实例。

代码.png 两种克隆方法的代码实现

在原型模式中,拷贝方式分为浅拷贝深拷贝两种:

(1)浅拷贝:只复制对象本身和对象中的基本类型(如数字、字符串)字段,对于对象类型字段,只复制引用但不复制引用的对象。 因此,两个对象中的引用类型字段实际上指向同一个实例。

image.png

function Prototype(field1,field2) {
    this.field1 = field1;
    this.field2 = field2;
    this.cloneShallow => () => {
    // 创建一个空对象,并将当前对象所有可枚举属性复制到新对象中
        return Object.assign({}.this)
    }
}

【情况一:原型对象只有基本类型】

//使用原型对象
let prototype1 = new Prototype('value1','value2');
let prototype2 = prototype1.cloneShallow();

console.log(prototype1); 
// 输出:{ field1: 'value1', field2: 'value2', cloneShallow: [Function] } 
console.log(prototype2); 
// 输出:{ field1: 'value1', field2: 'value2' }
//(注意:没有cloneShallow方法,因为它是原型链上的方法,不是对象自身的属性)

// 修改prototype2的属性,不会影响prototype1(对于基本类型字段) 
prototype2.field1 = "newValue1"; 
console.log(prototype1.field1); // 输出:"value1" 
console.log(prototype2.field1); // 输出:"newValue1"

【情况二:对象中包含对象类型字段,浅拷贝只会复制引用】

prototype1.nestedObject = { key: "originalValue" }; 
prototype2 = prototype1.cloneShallow(); // 重新进行浅拷贝以包含nestedObject 

prototype2.nestedObject.key = "newValue"; 
console.log(prototype1.nestedObject.key); 
// 输出:"newValue"(因为nestedObject的引用被复制了)

(2)深拷贝:复制对象本身和对象中的所有字段,包括引用类型字段引用的对象。 深拷贝需要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。

image.png

在JS中,可用递归或JSON.parse(JSON.stringify(obj)) 来实现深拷贝(尽管后者有局限性,如不能处理函数、undefined、Symbol等)。

以下是一个使用递归实现的深拷贝示例:

// 深拷贝函数
function deepClone(obj) {
    if(obj == null || typeof obj !== 'object') {
        return obj;
    }

    // 处理数组
    if (Array.isArray(obj)) { 
        let arrCopy = []; 
        for (let i = 0; i < obj.length; i++) { 
            arrCopy[i] = deepClone(obj[i]); 
        } 
        return arrCopy; 
    } 

    // 处理对象 
    let objCopy = {}; 
    for (let key in obj) { 
        if (obj.hasOwnProperty(key)) { 
            objCopy[key] = deepClone(obj[key]); 
        } 
    } 
    return objCopy; 
}


// 定义一个包含嵌套对象的原型对象 
function PrototypeWithNestedObject(field1, field2, nestedObject) { 
    this.field1 = field1; 
    this.field2 = field2; 
    this.nestedObject = nestedObject; 
    this.cloneDeep = function() { 
        return deepClone(this); 
    }; 
} 


// 使用原型对象 
let nestedObj = { key: "nestedValue" }; 
let nestedObjCopy = deepClone(nestedObj); // 对nestedObj进行深拷贝
let prototype1 = new PrototypeWithNestedObject("value1", "value2", nestedObjCopy);
let prototype2 = prototype1.cloneDeep(); 


// 修改prototype2的嵌套对象属性,不会影响prototype1 
prototype2.nestedObject.key = "newNestedValue"; 
console.log(prototype1.nestedObject.key); // "nestedValue" 
console.log(prototype2.nestedObject.key); // "newNestedValue"

效果.png 原型模式的效果

  • 减少对象创建的成本:避免了复杂对象的重复初始化过程,提高了创建对象的效率。
  • 避免与具体类耦合:客户端可以通过克隆方法创建新对象,而无需知道具体类的细节,降低了耦合度。
  • 灵活性增加:可以在运行时动态地添加或删除原型,适应不同的对象创建需求。
  • 支持动态配置:可以通过克隆来定制对象的不同配置,而无需修改其代码。

然而,也需要注意一些限制,如:

  • 深克隆问题:原型模式默认进行浅克隆,即复制对象本身和其引用。如果对象内部包含其他对象的引用,可能需要实现深克隆来复制整个对象结构。
  • 克隆方法的实现:某些对象可能不容易进行克隆,特别是涉及到文件、网络连接等资源的情况。

3.单例模式(Singleton Pattern)

单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如全局缓存、浏览器中的 window 对象等。单例模式用于保证一个类仅有一个实例,并提供一个访问它的全局访问点。

分支.png 类图

image.png

代码.png 代码实现

// 单例模式示例代码
class Singleton {
   constructor() {
        if (!Singleton.instance) { 
            //判断是否已经创建过一个实例
            Singleton.instance = this;
        }
        return Singleton.instance;
    }

    createInstance() {
        const object = { name: "example" };
        return object;
    }

    getInstance() {
        if (!Singleton.instance) {
            Singleton.instance = this.createInstance();
        }
        return Singleton.instance;
    }
}

// 使用示例
const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2); // true

4.工厂模式(Factory Pattern)

工厂模式其实就是将创建对象的过程单独封装,根据参数的不同创建不同对象的模式(实现无脑传参)。通过将对象的创建和使用分离,提高代码的灵活性和可维护性。在前端开发中,常用于创建不同类型的组件、插件等。

工厂模式可以分为:简单工厂模式、工厂方法模式和抽象工厂模式

(1)简单工厂模式(Simple Factory Pattern)

简单工厂模式又叫 静态方法模式,因为工厂类中定义了一个静态方法用于创建对象。

简单工厂让使用者不用知道具体的参数就可以创建出所需的 ”产品“ 类,即使用者可以直接消费产品而不需要知道产品的具体生产细节。

这种设计的弊端就是不遵守开闭原则,当新增了一个产品,需要修改工厂的代码。

分支.png 类图

image.png

  • 工厂(Factory):角色简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。

  • 抽象产品(Product)角色:简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。

  • 具体产品(Concrete Product)角色:是简单工厂模式的创建目标,所有创建的对象都是充当这个角色的某个具体类的实例

代码.png 代码实现

在下面这个示例中,我们定义了一个基础的 Button 构造函数和一个 createButton 工厂函数。Button 构造函数接受一个 type 参数,并将其存储在实例的 type 属性中。createButton 工厂函数根据传入的 type 参数来创建并返回相应类型的 Button 实例。如果传入的 type 参数不是已知的按钮类型,工厂函数将抛出一个错误。

//定义不同类型的按钮构造函数
function Button(type) {
    this.type = type;
}

Button.prototype.render = function() {
    console.log(`Rendering a ${this.type} button.`);
}


//定义一个工厂函数,用于创建按钮对象
function createButton(type) {
    let button;
    switch (type) {
        case 'text': 
        button = new Button('Text Button'); 
        break; 
    case 'submit': 
        button = new Button('Submit Button'); 
        break; 
    case 'reset': 
        button = new Button('Reset Button'); 
        break; 
    default: 
        throw new Error(`Unknown button type: ${type}`); 
    }
    return button;
}


// 使用工厂函数创建不同类型的按钮 
const textButton = createButton('text'); 
textButton.render(); // 输出: Rendering a Text Button. 

const submitButton = createButton('submit'); 
submitButton.render(); // 输出: Rendering a Submit Button. 

const resetButton = createButton('reset'); 
resetButton.render(); // 输出: Rendering a Reset Button. 


// 尝试创建一个未知类型的按钮,将会抛出错误 
try { 
    const unknownButton = createButton('unknown'); 
} catch (error) { 
    console.error(error.message); // 输出: Unknown button type: unknown 
}

(2)工厂方法模式(Factory Method Pattern)

工厂方法模式提供了一个创建对象的接口,但是将具体的对象创建延迟到子类中。这样,客户端代码不需要知道要创建的具体对象的类,只需要通过工厂方法来创建对象。这使得客户端代码与具体对象的创建解耦,提高了代码的灵活性和可维护性。

在工厂方法模式中,通常会定义一个抽象工厂类,其中包含一个创建对象的抽象方法,而具体的对象创建则由具体的子类实现。这样,每个具体的子类都可以根据需要创建不同类型的对象,而客户端代码只需要通过抽象工厂类来调用工厂方法,而不需要关心具体的对象创建细节。

分支.png 类图

image.png

栗子.png 举个栗子

定义多个具体工厂类,用户需要哪个产品就去调用哪个产品的工厂的方法,比如你需要华为手机,你就去华为手机的工厂要。一个产品就给了一个工厂,这样子如果产品变多,系统就会很复杂。

image.png

代码.png 代码实现

abstract class phoneFactory { 
    abstract producePhone(): Phone; 
} 

class ApplePhoneFactory extends phoneFactory { 
    producePhone(): Phone { 
        return new ApplePhone(); 
    } 
} 

class HuaWeiFactory extends phoneFactory { 
    producePhone(): Phone { 
        return new HuaWeiPhone(); 
    } 
}

在以上代码中,我们分别创建了 ApplePhoneFactoryHuaWeiFactory 两个工厂类,然后使用这两个类的实例来生产不同型号的车子。

const ApplePhoneFactory = new ApplePhoneFactory(); 
const HuaWeiFactory = new HuaWeiFactory(); 

const ApplePhone = ApplePhoneFactory.producePhone(); 
const HuaWeiPhone = HuaWeiFactory.producePhone(); 

ApplePhone.run(); 
HuaWeiPhone.run();

(3)抽象工厂模式(Abstract Factory Pattern)

抽象工厂模式提供了一个接口,用于创建一系列相关或相互依赖的对象。通过使用抽象工厂接口及其具体实现,可以将对象的创建与客户端代码分离,从而实现系统的松耦合。

分支.png 类图

image.png

  • 抽象工厂(Abstract Factory) :声明了一组用于创建不同产品的抽象方法。具体的工厂类必须实现这些方法来创建具体的产品对象。
  • 具体工厂(Concrete Factory) :实现抽象工厂接口,负责创建特定种类的产品对象。
  • 抽象产品(Abstract Product) :定义了产品的通用接口,具体产品必须实现这个接口。
  • 具体产品(Concrete Product) :实现抽象产品接口,是抽象工厂创建的实际对象。

栗子.png 举个栗子

抽象工厂模式适用于需要创建一系列相关产品并保证它们之间一致性的情况,例如图形界面库中的UI元素,不同操作系统下的界面组件等。通过使用抽象工厂模式,可以更好地管理和组织这些产品的创建过程。

在下面这个示例中,我们定义了抽象产品(AbstractButtonAbstractTextField),以及它们的具体实现(Windows风格和Mac风格的按钮和文本框)。我们还定义了抽象工厂(AbstractFactory),以及它的具体实现(WindowsFactory和MacFactory)。每个具体工厂都负责创建与其风格相对应具体产品。​

客户端代码通过调用具体工厂的 createButton 和 createTextField 方法来创建按钮和文本框,并调用它们的 render 方法来渲染UI。由于使用了抽象工厂模式,客户端代码与具体产品的实现解耦,因此可以轻松地切换不同的工厂来创建不同风格的UI,而无需修改客户端代码。

//定义抽象产品:按钮
function AbstractButton() {}
AbstractButton.prototype.render = function() {
    throw new Error('Abstract method connot be called');
}


//定义具体产品:windows风格按钮
function WindowsButton() {} 
WindowsButton.prototype = Object.create(AbstractButton.prototype);
WindowsButton.prototype.constructor = WindowsButton; 
WindowsButton.prototype.render = function() { 
    console.log('Rendering a Windows-style button.'); 
}; 


// 定义具体产品:Mac风格的按钮
function MacButton() {} 
MacButton.prototype = Object.create(AbstractButton.prototype); 
MacButton.prototype.constructor = MacButton; 
MacButton.prototype.render = function() { 
    console.log('Rendering a Mac-style button.'); 
};


// 定义抽象产品:文本框 
function AbstractTextField() {} 
AbstractTextField.prototype.render = function() { 
    throw new Error('Abstract method cannot be called'); 
}; 


// 定义具体产品:Windows风格的文本框
function WindowsTextField() {} 
WindowsTextField.prototype = Object.create(AbstractTextField.prototype);
WindowsTextField.prototype.constructor = WindowsTextField;
WindowsTextField.prototype.render = function() { 
    console.log('Rendering a Windows-style text field.'); 
}; 


// 定义具体产品:Mac风格的文本框
function MacTextField() {} 
MacTextField.prototype = Object.create(AbstractTextField.prototype);
MacTextField.prototype.constructor = MacTextField; 
MacTextField.prototype.render = function() { 
    console.log('Rendering a Mac-style text field.');
}; 

// 定义抽象工厂
function AbstractFactory() {} 
AbstractFactory.prototype.createButton = function() {
    throw new Error('Abstract method cannot be called'); 
}; 
AbstractFactory.prototype.createTextField = function() { 
    throw new Error('Abstract method cannot be called'); 
}; 


// 定义具体工厂:Windows工厂 
function WindowsFactory() {} 
WindowsFactory.prototype = Object.create(AbstractFactory.prototype); 
WindowsFactory.prototype.constructor = WindowsFactory; 
WindowsFactory.prototype.createButton = function() { 
    return new WindowsButton(); 
}; 
WindowsFactory.prototype.createTextField = function() { 
    return new WindowsTextField(); 
}; 



// 定义具体工厂:Mac工厂
function MacFactory() {} 
MacFactory.prototype = Object.create(AbstractFactory.prototype); 
MacFactory.prototype.constructor = MacFactory; 
MacFactory.prototype.createButton = function() { 
return new MacButton(); 
}; 
MacFactory.prototype.createTextField = function() { 
    return new MacTextField(); 
}; 


// 客户端代码 

function renderUI(factory) {
    const Button = factory.createButton();
    button.render();
    const textField = factory.createTextField(); 
    textField.render(); 
}

// 使用Windows工厂创建UI 
const windowsFactory = new WindowsFactory(); 
renderUI(windowsFactory); 

// 使用Mac工厂创建UI 
const macFactory = new MacFactory(); 
renderUI(macFactory);

效果.png 抽象工厂模式的效果

  • 产品族一致性:抽象工厂确保创建的产品是一组相关的产品族,保证了这些产品之间的一致性。
  • 松耦合:客户端代码不需要直接依赖于具体产品,只需要通过抽象工厂接口创建产品,从而降低了代码的耦合度。
  • 可扩展性:增加新的产品族或产品变得相对容易,只需要添加新的具体工厂和产品类即可,不需要修改现有代码。
  • 限制:抽象工厂模式要求系统中的每个产品族都必须有一个对应的具体工厂,这可能增加了系统的复杂性。