文章

结构型模式深度解析

结构型模式三兄弟:适配器、代理、装饰器——意图差异、前端落点(HOC、Proxy、API 兼容层)。

结构型模式深度解析

一句话概括

结构型模式解决的是”类和对象怎么组合成更大的结构“。前端最常用的三个是适配器模式(把不兼容的接口翻译成统一的,如兼容旧 API)、代理模式(控制对目标的访问,如 ES6 Proxy、图片懒加载)、装饰器模式(动态给对象加功能,如高阶组件 HOC)。三者长得像(都是”包一层”),但意图完全不同——这是面试最爱考的辨析点。

核心知识点

1. 适配器模式:让不兼容的接口协同工作

适配器的本质是翻译——不改原有代码,加一个转换层把 A 接口适配成 B 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 旧接口:返回 { name, age }
// 新接口:需要 { userName, userAge }
class OldUserAPI {
  getUser() { return { name: '张三', age: 28 }; }
}

// 适配器:把旧接口翻译成新格式
class UserAdapter {
  constructor(oldAPI) { this.oldAPI = oldAPI; }
  getUser() {
    const { name, age } = this.oldAPI.getUser();
    return { userName: name, userAge: age }; // 字段映射
  }
}

// 前端真实落点:统一多端数据源
function normalizeUser(data) {
  // 不同后端返回不同字段名,统一成一套
  return {
    id: data.id ?? data.user_id,
    name: data.name ?? data.nickname ?? data.userName
  };
}

场景:对接多个第三方服务字段不一致、旧 API 重构后兼容老调用方、多端(iOS/Android/Web)数据格式统一。适配器让”改接口”不影响”用接口”的代码。

2. 代理模式:控制对目标的访问

代理和适配器的区别:代理和目标有相同接口,但代理在调用前后”插一脚”(拦截、校验、缓存、延迟):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ES6 Proxy:拦截对象操作
const user = { name: '张三', age: 28 };
const proxy = new Proxy(user, {
  get(target, key) {
    console.log(`读取 ${key}`);
    return target[key];
  },
  set(target, key, value) {
    if (key === 'age' && (value < 0 || value > 150)) {
      throw new Error('年龄不合法');
    }
    target[key] = value;
  }
});

proxy.name;       // 打印 "读取 name"
proxy.age = 200;  // 抛错:年龄不合法

// 图片懒加载:代理 img 的 src,进入视口才真正加载
function lazyImage(el) {
  const realSrc = el.dataset.src;      // 真实地址放 data-src
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(e => {
      if (e.isIntersecting) { el.src = realSrc; observer.unobserve(el); }
    });
  });
  observer.observe(el);
}

Vue 3 的响应式就是 Proxy 的经典应用——拦截属性的读取和赋值,实现依赖收集和触发更新。

3. 装饰器模式:动态扩展功能

装饰器不改原对象,在外部包裹一层,动态叠加功能。前端的典型就是高阶组件(HOC):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 高阶组件:给任意组件"装饰"上 loading 能力
function withLoading(WrappedComponent) {
  return function Enhanced({ loading, ...props }) {
    if (loading) return <div>加载中...</div>;
    return <WrappedComponent {...props} />;
  };
}

const UserListWithLoading = withLoading(UserList);
// <UserListWithLoading loading={true} /> → 显示"加载中"
// <UserListWithLoading loading={false} data={...} /> → 正常渲染

// 函数式装饰:组合多个能力
const withAuth = fn => (...args) => {
  if (!isLoggedIn()) throw new Error('未登录');
  return fn(...args);
};
const withLog = fn => (...args) => {
  console.log('调用参数', args);
  return fn(...args);
};
const run = withLog(withAuth(businessLogic));

装饰器的精髓是组合优于继承——不用继承一个臃肿的基类,而是按需”贴”功能,任意组合。

4. 三者辨析:适配器 vs 代理 vs 装饰器

这是面试最高频的辨析题,关键是意图:

模式核心意图接口是否变化典型例子
适配器让不兼容的接口能用接口变了(翻译格式)新旧 API 兼容层
代理控制访问、增强控制接口不变Vue 响应式、懒加载
装饰器动态加功能接口不变(增强)HOC、middleware

一句话记忆:适配器是”翻译官”(换接口)、代理是”门卫”(拦截访问)、装饰器是”贴膜”(加功能不改接口)。

5. 组合优于继承(Composition over Inheritance)

结构型模式背后的共同哲学是组合优先于继承:

1
2
3
4
5
6
7
8
9
// ❌ 继承:深度继承链,父类改动影响所有子类,且只能单继承
class Animal { eat() {} }
class Bird extends Animal { fly() {} }
class SwimBird extends Bird { swim() {} } // 想要"会游泳的鸟",继承链越来越深

// ✅ 组合:把能力拆成独立模块,按需拼装
const canFly = (obj) => ({ ...obj, fly: () => '飞' });
const canSwim = (obj) => ({ ...obj, swim: () => '游' });
const duck = canSwim(canFly({})); // 鸭子:会飞也会游

前端组件化(React/Vue)本身就是组合思想的极致体现——UI 由小组件拼装,而不是层层继承。结构型模式就是”用组合解决问题”的具体招式。

其实你每天都在用

  • Vue 3 的响应式:reactive() 返回 Proxy,你 obj.name = 'x' 时被拦截、触发视图更新,这就是代理模式
  • React 高阶组件:withRouter、connect(mapState) 给组件”贴”上路由/状态能力,就是装饰器
  • 对接第三方 SDK 字段不一致:后端返回 user_id,前端统一 normalize 成 id,就是适配器
  • 图片懒加载 / 骨架屏:先用占位图代理真实图,滚动到视口才真正 src,就是代理
  • axios 拦截器:请求前加 token、响应后统一处理错误,在”调用前后插一脚”,就是代理/装饰思想

常见误解(FAQ)

❌ 误区一:”适配器、代理、装饰器都是包一层,本质一样”

三者结构相似但意图天差地别:适配器是换接口(解决”不兼容”),代理是控访问(解决”何时/能否访问”),装饰器是加功能(解决”动态扩展”)。搞混意图,代码结构对了但设计就错了。面试答”都是包装器”只能拿一半分。

❌ 误区二:”装饰器模式就是高阶组件 HOC,一回事”

HOC 是装饰器思想在 React 里的一种实现,但不是唯一。装饰器还有:函数式中间件(withLog(withAuth(fn)))、ES 装饰器语法(@log)、AOP 切面。反过来,HOC 也不一定是装饰器——它也可能是代理(控制渲染)。模式是思想,HOC 是语法,别画等号。

❌ 误区三:”ES6 Proxy 和代理模式是同一个概念”

Proxy 是语言层面的 API(拦截对象操作的语法机制),代理模式是设计层面的思想(用中间对象控制访问)。Proxy 可以用来实现代理模式(Vue 响应式),但代理模式不一定要用 Proxy(懒加载用 IntersectionObserver 也是代理)。一个是工具,一个是思想。

❌ 误区四:”继承能实现代码复用,结构型模式是多余的”

继承带来的是强耦合 + 单继承限制 + 父类改动波及子类。组合(结构型模式的底层)则让能力独立、可复用、可任意拼装,且运行时也能动态组合。现代前端框架全部拥抱组合,继承只在”明确的 is-a 关系”下才用。结构型模式恰恰是”组合优于继承”的具体落地。

一句话总结

结构型模式的本质是用”组合 + 包一层”来代替”继承 + 改源码”——适配器换接口、代理控访问、装饰器加功能,三者共用一个招式,却各怀不同意图,掌握了”意图之别”才算真正掌握结构型模式。

本文由作者按照 CC BY 4.0 进行授权