前端知库

工厂模式和适配器模式在前端里怎样用?

工厂模式把“创建哪种对象”的判断收进一个函数,调用方只传类型和参数,比如按类型创建不同的图表或表单控件。适配器模式把一种接口转换成调用方期望的接口,比如把新旧两套后端返回的数据整理成同一种格式,或者给第三方库包一层统一的调用方式。

参考答案

简答

一个报表页要根据配置显示折线图、柱状图或饼图。如果每个用到图表的地方都写 if (type === 'line') new LineChart() else if ...,加一种图表就得改很多处。工厂模式是把这段判断收进一个 createChart(type, data) 函数,调用方只管传类型,不用知道具体是哪个类。

适配器模式解决的是接口对不上的问题。老接口返回 user_name,新接口返回 profile.name,页面组件只想要 { name, age }。适配器就是中间那层转换函数,把各种来源的数据转成组件期望的格式,组件本身不用改。

两者的区别在于时机和目的。工厂关心的是“创建哪个对象”,发生在对象诞生时;适配器关心的是“已有的东西怎样配上现在的接口”,发生在使用已有对象或数据时。

要注意,类型只有一两种时直接 new 更清楚,不必加工厂;适配器最好放在数据进入应用的边界上,比如请求层,不要散落在各个组件里。

用工厂按类型创建对象

下面用一张映射表代替 if-else,createChart 按类型名找到对应的类再创建实例:

JavaScript
class LineChart {
  constructor(data) { this.data = data; }
  render() { return `折线图:${this.data.length} 个点`; }
}
class BarChart {
  constructor(data) { this.data = data; }
  render() { return `柱状图:${this.data.length} 根柱子`; }
}

const chartTypes = { line: LineChart, bar: BarChart };

function createChart(type, data) {
  const Chart = chartTypes[type];
  if (!Chart) throw new Error(`不支持的图表类型:${type}`);
  return new Chart(data);
}

console.log(createChart('line', [1, 2, 3]).render()); // 折线图:3 个点
console.log(createChart('bar', [5, 8]).render()); // 柱状图:2 根柱子

// 两个后端返回的用户数据结构不同
const fromOldApi = { user_name: '张三', user_age: '28' };
const fromNewApi = { profile: { name: '李四', age: 32 } };

const adapters = {
  old: (raw) => ({ name: raw.user_name, age: Number(raw.user_age) }),
  new: (raw) => ({ name: raw.profile.name, age: raw.profile.age }),
};

console.log(adapters.old(fromOldApi)); // { name: '张三', age: 28 }
console.log(adapters.new(fromNewApi)); // { name: '李四', age: 32 }

调用 createChart 的代码拿到的对象都有 render 方法,它不需要知道背后是 LineChart 还是 BarChart。新增饼图时,写一个 PieChart 类再往 chartTypes 里加一项就行,所有调用方都不用动。

为什么找不到类型时要抛错?因为配置里拼错了类型名,如果默默返回 undefined,错误会推迟到调用 render 时才暴露,报错信息也看不出原因。

前端里很多地方都是工厂的思路:document.createElement('div') 按标签名创建不同的元素,React 的 createElement 按传入的类型创建元素,低代码平台按 JSON 配置里的 type 渲染对应的组件。

用适配器统一数据格式

上面例子的后半段就是两个适配器。老接口的年龄是字符串,新接口的字段嵌在 profile 里,经过适配后都变成 { name, age },页面组件只认这一种结构。

适配器放在哪里最合适?放在请求函数里,接口一返回就转换。这样后端字段改名时,只需改适配器一处;如果组件里到处直接读 raw.user_name,改名就要全局搜索替换。

另一种常见用法是包装第三方库。比如项目里同时用过两个埋点 SDK,一个叫 track(event, data),一个叫 logEvent({ name, params }),可以写一个统一的 report(name, data),内部按当前 SDK 调用对应的方法。以后换 SDK,业务代码不受影响。

工厂和适配器容易和什么混淆

工厂和策略模式都常用映射表,区别在于表里放的东西。工厂表里是“怎样创建对象”,返回的是一个实例;策略表里是“怎样完成一件事”,比如不同的校验规则,见《策略模式和代理模式分别解决什么问题?前端怎样使用?》。

适配器和代理、装饰器都是包一层,区别在于接口变不变。适配器会把接口改成另一个样子;代理和装饰器保持接口不变,只是在调用前后加控制或功能,见《装饰器模式是什么?和高阶函数、高阶组件有什么关系?》。

常见追问

工厂函数和构造函数有什么区别?

构造函数要用 new 调用,返回的一定是这个类的实例。工厂函数是普通函数,内部决定创建哪个类,甚至可以返回缓存的对象或普通对象字面量,调用方不需要知道细节,也不用写 new。

后端字段名和前端不一致,一定要写适配器吗?

字段少、只在一处用时,直接在用的地方取值也可以。字段多、多个页面共用、或者后端还会调整时,在请求层统一转换更省事,也方便写类型定义。

适配器会不会让代码变复杂?

会多一层,但这层换来的是隔离:外部接口的变化只影响适配器。如果外部接口很稳定,又只有一个使用方,就没必要加。