23种设计模式之工厂模式集合

173 阅读10分钟

工厂模式特性

工厂模式主要是为了创建对象实例或者类簇,只关心最终产物不关心过程

简单工厂模式【不是GoF 23 种设计模式之一】

别名

静态工厂方法

定义

是工厂模式的基础

用一个工厂类统一管理对象的创建过程,通过传入的参数构造出不同类型的实例。

这些实例拥有类似的方法与属性,这样后续使用时,不同实例的代码一致,高度复用。

优点

  • 代码高度复用:
    • 使用时直接使用抽象类的公共方法,不用管实例化细节。
    • 使用时对具体类的依赖少。
  • 代码可读性高:
    • 面对对象式编程,易于理解
    • 可以直接找到具体类的方法实现,不用关注条件分支
  • 集中管理创建逻辑
  • 在产品种类少的时候,可读性高
  • 符合单一职责

缺点

  • 在产品种类多的时候,可读性特别差,因为维护创建的代码集中在工厂函数内部。
  • 违反开闭原则:新增产品需要修改工厂类代码

开闭原则: 软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。换句话说,在不修改现有代码的基础上,应该能够添加新功能或者改变程序的行为。

  • 工厂类臃肿:产品类型过多时,条件判断逻辑复杂,维护困难

与直接写的最大差别

  • 高内聚:同模块的功能放在一起
  • 符合单一职责原则:使用简单工厂模式与直接将所有产品类型判断放在具体方法实现中对比,简单工厂模式符合单一职责原则,在更改维护特定产品代码时更易找到。

单一职责原则: 一个类或者模块应该只负责一个明确的功能或者职责。这有助于降低系统的复杂性,使代码更容易理解和维护。

应用场景

适用于实例类型稳定且较少的场景:

  • 当不同对象种类较少,且创建逻辑相对简单且固定时
  • 需要集中管理对象创建过程
  • 使用方不需要知道具体实例的创建细节,只需要调用同样的方法去实现功能,内部实现封装在每个不同类的内部。

如何实现

关键点

  1. 工厂类:用于封装对象的创建逻辑
  2. 抽象产品接口,用于定义所有产品的公共方法与属性。
  3. 具体产品类:用于进行各种公共方法的具体副作用实现。

实现方法

  1. 实现抽象产品类,该类用于后续使用简单工厂创建实例后,可以借助此类使用对应的工具方法。
  2. 实现各种具体产品类,用于处理不同类型的产品在运行相同公共方法时的不同副作用
  3. 实现工厂类,用于根据传参创建不同的具体产品类。此处也可以是一个函数方法,叫做工厂方法
  4. 使用工厂类创建的实例调用公共方法使用。

cookbook

当产品频繁扩展时,建议升级为工厂方法模式或结合反射机制优化,以遵循开闭原则

场景示例

复杂表格列渲染【前端】

场景描述

若是一个处理文件的工单表格,文件类型为xml、pdf、jpg、png、text

每个在初始化的工单都会有,开始、下载、前端预览 的功能按钮

但是文件类型不同导致:

  • 开始按钮【start方法】:要进行一些不同的数据前置处理,拿取关键信息传递后端
  • 下载按钮【download方法】:不同类型的文件有不同格式的命名规则,需要处理
  • 前端预览【preview方法】:不同类型要不同的组件实例的预览方法,或是要自己去手动处理要显示的内容【比如txt,后端让你手动高亮某一行某段文字】

此时要根据工单类型的不同去执行不同按钮的执行函数,我们可以采用工厂类或工厂函数去创建不同文件类型的不同js类,每个具体类都有【start方法】、【download方法】、【preview方法】的实现。这样就可以在点击按钮时直接根据数据生成抽象类的实例,然后调用对应方法。

抽象类型定义
// FileHandler.ts(抽象接口)
abstract class FileHandler {
  abstract start(file: File): Promise<InitData>;
  abstract download(file: File): void;
  abstract preview(file: File, container: HTMLElement): void;
}

// 类型定义
type FileType = 'xml' | 'pdf' | 'jpg' | 'png' | 'text';
interface InitData {
  metadata: Record<string, any>;
  backendEndpoint: string;
}
具体类型定义
// XmlHandler.ts
class XmlHandler extends FileHandler {
  async start(file: File) {
    const xmlContent = await file.text();
    const parser = new DOMParser();
    const doc = parser.parseFromString(xmlContent, "text/xml");
    // 后端接口
  }

  download(file: File) {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    saveAs(file, `xml_${timestamp}_${file.name}`);
  }

  preview(file: File, container: HTMLElement) {
    const codeViewer = document.createElement('code-editor');
    codeViewer.setAttribute('mode', 'xml');
    container.appendChild(codeViewer);
  }
}

// TextHandler.ts(其他类型类似)
class TextHandler extends FileHandler {
  async start(file: File) {
    const textContent = await file.text();
    // 后端接口
  }

  download(file: File) {
    saveAs(file, `text_${file.name.replace(/.[^/.]+$/, "")}.txt`);
  }

  preview(file: File, container: HTMLElement) {
    fetch(`/api/highlight?fileId=${file.name}`)
      .then(res => res.text())
      .then(html => {
        container.innerHTML = `<pre class="hljs">${html}</pre>`;
      });
  }
}
工厂类定义
// FileHandlerFactory.ts
class FileHandlerFactory {
  static createHandler(type: FileType): FileHandler {
    switch(type) {
      case 'xml':
        return new XmlHandler();
      case 'pdf':
        return new PdfHandler(); // 需实现
      case 'jpg':
      case 'png':
        return new ImageHandler();
      case 'text':
        return new TextHandler();
      default:
        throw new Error(`Unsupported file type: ${type}`);
    }
  }
}
实际使用
function onClickHandle(fileType, rawFile, btnType) {
  
  const handler = FileHandlerFactory.createHandler(fileType);
  
  if(btnType === ('start-btn')) {
    const initData = await handler.start(rawFile);
  } else if(btnType === ('download-btn')) {
    handler.download(rawFile);
  } else if(btnType === ('preview-btn')) {
    handler.preview(rawFile, document.getElementById('preview-container'));
  }
}

原生 node 实现对于不同方法的http请求的处理【后端】

实现一个http服务,内部实现多个请求方法的处理。

抽象类
// HttpHandler.js
class HttpHandler {
  handleRequest(req, res) {
    throw new Error("Abstract method must be implemented");
  }
}
module.exports = HttpHandler;
工具类

实现 post、get等http具体类实现

// 
const HttpHandler = require("./HttpHandlerClass");

class PostHandler extends HttpHandler {
  handleRequest(req, res) {
    let body = "";
    req.on("data", (chunk) => (body += chunk.toString()));
    req.on("end", () => {
      res.statusCode = 200;
      res.setHeader("Content-Type", "application/json");
      res.end(JSON.stringify({ method: "POST", body: JSON.parse(body) }));
    });
  }
}
module.exports = PostHandler;
// GetHandler.js
const HttpHandler = require("./HttpHandlerClass");

class GetHandler extends HttpHandler {
    handleRequest(req, res) {
        res.statusCode = 200;
        res.setHeader("Content-Type", "application/json");
        res.end(JSON.stringify({ method: "GET", query: req.query }));
    }
}
module.exports = GetHandler;
工厂类实现
// HandlerFactory.js
const GetHandler = require("./GetHandler");
const PostHandler = require("./PostHandler");
// const PutHandler = require("./PutHandler");
// const DeleteHandler = require("./DeleteHandler");

class HandlerFactory {
  static createHandler(method) {
    switch (method.toUpperCase()) {
      case "GET":
        return new GetHandler();
      case "POST":
        return new PostHandler();
    //   case "PUT":
    //     return new PutHandler();
    //   case "DELETE":
    //     return new DeleteHandler();
      default:
        throw new Error("Unsupported HTTP method");
    }
  }
}
module.exports = HandlerFactory;
具体使用
// server.js
const http = require("http");
const url = require("url");
const HandlerFactory = require("./HandlerFactory");

const server = http.createServer((req, res) => {
  const { method } = req;
  const parsedUrl = url.parse(req.url, true);

  try {
    const handler = HandlerFactory.createHandler(method);
    req.query = parsedUrl.query; // 注入 GET 参数
    handler.handleRequest(req, res);
  } catch (error) {
    res.statusCode = 405;
    res.end(JSON.stringify({ error: error.message }));
  }
});

server.listen(3000, () => {
  console.log("Server running at http://localhost:3000");
});

工厂模式

安全模式的类

  1. 使用 class 创建的类:不使用 new 直接调用的话会报错
  2. 普通函数进行 new.target 判断处理:

当不使用 class 创建的类,其实就是一个普通函数,函数可以直接调用。

但是我们设计之处,是想通过 new 创建实例的,而不是直接调用。

为了防止不使用 new 创建而直接将类当作函数使用,我们可以在构造函数中使用 new.target 是否为当前类,若是则说明使用 new 创建实例,正常使用。反之则应该手动调用 new 返回创建的实例。

function SafeClass(...args) {
  if (new.target !== SafeClass) return new SafeClass(...args);
  // 构造函数初始化逻辑
}

工厂模式实现思路

  1. 将所有的具体产品的类的实现挂载到工厂类下。
  • 若工厂类用 class 实现,则具体产品类则为工厂类的静态成员
  • 若工厂类用 function 实现,具体产品类为工厂类 prototype 上的属性。
class A{};

class Factory {
  static A = A;
}
  1. 工厂类实现成一个安全模式的类,要求传入第一个参数用于表示创建哪个具体产品,后续参数为传给具体产品类的构造参数。
function Factory(type, ...args) {
  if (new.target !== Factory) return new Factory[type](...args);
  // 构造函数初始化逻辑
}
  1. 之后就可以使用工厂类创建对应产品使用了。

我们简单实现的话,直接使用函数根据类型创建对应的示例,这其实也是属于工厂模式的,比如下方的main

function main() {
    let factory: UIFactory;

    if (process.platform === "darwin") {
        factory = new MacUIFactory();
    } else {
        factory = new WindowsUIFactory();
    }

    const client = new Client(factory);
    client.run();
}

与简单工厂模式相对比

  1. 应用场景:工厂模式适合种类多的产品【>= 5】,简单工厂模式适合种类少的产品
  2. 可读性:工厂模式在种类少时可读性弱于简单工厂模式,但是在种类多时,可读性大大强于简单工厂模式,但是多了可读性也很烂。
  3. 符合开闭原则:新产品只需要在原型上多挂载一个具体子类即可。
  4. 可维护性高:只用维护子类产品本身。

抽象工厂模式

定义

抽象工厂模式可以看作是工厂模式的一种扩展和抽象。

工厂模式主要用于创建单一产品,而抽象工厂模式对工厂模式上方加了一个抽象层用于创建相关产品族。

所以一个可以用工厂模式实现的系统,产品族关系中最少要有 4 层:

  • 具体产品
  • 抽象产品
  • 具体产品族
  • 抽象产品族

作用

用于创建一系列相关的抽象产品族,主要用于通过抽象接口隔离具体实现, 确定该族产品的具体公共方法与属性。

对于弱类型的语言来讲可能用处很小。仅仅是用于确认实例要实现的内容。

实现步骤

  1. 根据产品族关系确定:抽象产品、具体产品、具体的产品族、抽象的产品族,其中:
  • 抽象产品:定义了产品的通用接口
  • 抽象的产品族:定义了创建 产品族 类 的方法,但不负责具体的实现。
  1. 按照顺序依次实现:抽象产品族类、具体产品族类、抽象产品类、具体产品类的定义
  2. 之后就可以写客户端逻辑了,此时产品的创建可以结合工厂模式或简单工厂模式去实现。

特性

优点:

  • 高内聚、低耦合
  • 一致性保障:保障抽象产品族对应的各个实际产品族行为一致
  • 高扩展性:新增产品族无需修改已有代码
  • 解耦客户端:业务逻辑并不依赖类的实现

缺点:

  • 类数量膨胀:增加一个产品家族就得增加一个类

应用场景

适合多产品族、高复杂度场景

  • 实现 跨平台UI组件的涉及跨平台的操作集合。

将实现过程中涉及平台间兼容的所有操作变成一个处理集合。一个平台对应一个集合,对应一个类。每个集合中对应方法的名称、传参、返回值一致,这样在用的时候代码统一,只用提前创建好对应环境的操作实例即可。

此时:

  • 具体产品为 具体平台内具体 UI 组件的操作集合
  • 抽象产品为 具体 UI 组件在不同平台的统一行为集合 。
  • 具体产品族为 具体平台内所有 UI 组件的类的集合。
  • 抽象产品族为 不同平台统一的 UI 组件类的集合
  • 游戏中游戏角色的管理

游戏中有不同种族,不同种族中都有2种行为模式的人,英雄和随从。相同行为模式的人,有共同的可以进行的行为。

此时:

  • 具体产品为 具体种族具体行为模式的某个人
  • 抽象产品为 具体种族具体行为模式的这类人
  • 具体产品族为 具体种族的人
  • 抽象产品族为 人
  • 多主题组件

实现示例

实现跨平台UI组件时,涉及相关跨平台的操作集合。

  1. 定义抽象产品族
abstract class UIFactory {
    abstract createButton(): Button;
    abstract createTextBox(): TextBox;
}
  1. 定义具体产品族的类型
class MacUIFactory extends UIFactory {
    createButton(): Button {
        return new MacButton();
    }

    createTextBox(): TextBox {
        return new MacTextBox();
    }
}
  1. 定义抽象产品
abstract class Button {
    abstract render(): void;
}
  1. 定义具体产品
class MacButton extends Button {
    render(): void {
        console.log("Rendering Mac Button");
    }
}
  1. 客户端使用工厂模式应用
function main() {
    let factory: UIFactory;

    if (process.platform === "darwin") {
        factory = new MacUIFactory();
    } else {
        factory = new WindowsUIFactory();
    }

    const client = new Client(factory);
    client.run();
}

main();