结构型(TS)

10 阅读6分钟

装饰器模式

在不修改原对象结构的前提下,动态地为对象添加职责能力
注意:Promise 扁平化是装饰器链成立的语言前提

原因: 装饰器模式要求 被装饰对象与装饰后的对象在类型层面是同态的

补充Promise规则:如果 Promise resolve 的值是 Promise,那么最终 Promise 的结果由内层 Promise 的状态和值决定

/**
 * =========================
 * 请求函数类型(协议)
 * =========================
 * 所有被装饰函数 & 装饰器
 * 都必须遵守这个函数签名
 */
type RequestFn = (url: string) => Promise<any>;

//(fn: RequestFn): RequestFn
//表示一个装饰器函数:
//输入和输出的函数类型完全一致,只增强能力,不改变使用方式。

/**
 * =========================
 * 原始请求函数(被装饰者)
 * =========================
 * 只负责:发请求、拿数据
 * 不关心 loading / token / error
 */
const request: RequestFn = async (url) => {
  const res = await fetch(url);

  if (!res.ok) {
    throw new Error('Network error');
  }

  return res.json();
};

/**
 * =========================
 * 装饰器 1:Loading
 * =========================
 * 在请求前后注入 loading 行为
 */
function withLoading(fn: RequestFn): RequestFn {
  //async 函数无论内部 return 什么,最终都会返回一个 Promise
  return async function (url: string) {
    console.log('loading start');
    try {
      const result = await fn(url);
      return result;
    } finally {
      // finally 保证异常时也能关闭 loading
      console.log('loading end');
    }
  };
}

/**
 * =========================
 * 装饰器 2:Token 校验
 * =========================
 * 在真正发请求前进行鉴权
 * 若失败,直接中断执行链
 */
function withTokenCheck(fn: RequestFn): RequestFn {
  return async function (url: string) {
    const token = localStorage.getItem('token');
    if (!token) {
      throw new Error('No token');
    }
    return fn(`${url}?token=${token}`);
  };
}

/**
 * =========================
 * 装饰器 3:错误处理
 * =========================
 * 统一捕获下游异常
 */
function withError(fn: RequestFn): RequestFn {
  return async function (url: string) {
    try {
      return await fn(url);
    } catch (e) {
      console.error('请求失败:', e);
      throw e;
    }
  };
}

/**
 * =========================
 * 装饰(组合)
 * =========================
 * 能力从里向外叠加
 * 执行从外向里进入
 */
const finalRequest: RequestFn =
  withError(
    withTokenCheck(
      withLoading(request)
    )
  );

/**
 * =========================
 * 使用
 * =========================
 */
finalRequest('/api/user');

通过函数包装的方式,

  • 能力是从里向外叠加的
  • 执行是从外向里进入的
  • 任意一层不满足条件,直接中断,不再向内执行
执行顺序:
finalRequest
↓ withError        (统一兜底)
↓ withTokenCheck   (决定能不能继续)
↓ withLoading      (before / after)
↓ request          (真正的业务)

适配器模式

使接口不兼容的对象能够相互合作

在不改变原有代码的情况下使用新的类

生活中的例子:耳机转接头

场景:
微信支付 / 支付宝 SDK 接口完全不一样
但你的业务代码 只想用一个统一的支付接口

/**
 * =========================
 * 目标接口(Target Interface)
 * =========================
 * 业务层只认这个类型:
 * - “我只关心能不能付钱”
 * - “不关心你是微信 / 支付宝 / 其他”
 *
 * 这就是被“冻结”的协议
 */
type Payment = (amount: number, userId: string) => void;

/**
 * =========================
 * 被适配者(Adaptee)
 * =========================
 * 第三方 SDK / 旧系统 / 不可控代码
 * 
 * 特点:
 * - 接口可能不统一
 * - 参数可能随时变化
 * - 业务层不应该直接依赖它
 */
function wechatPaySDK(amount: number, userId: string): void {
  console.log(`微信支付 ${amount} 元,用户 ${userId}`);
}

function aliPaySDK(amount: number, userId: string): void {
  console.log(`支付宝支付 ${amount} 元,用户 ${userId}`);
}

/**
 * =========================
 * 适配器(Adapter)
 * =========================
 * ⚠️ 注意:
 * 适配器本身【不执行支付】
 * 它的职责是:
 * 👉 生产一个“符合 Payment 协议的函数”
 *
 * 也就是说:
 * - wechatPayAdapter 不是 Payment
 * - wechatPayAdapter() 的返回值 才是 Payment
 */
function wechatPayAdapter(): Payment {
  return (amount: number, userId: string) => {
    // 在这里完成“接口翻译”
    wechatPaySDK(amount, userId);
  };
}

function aliPayAdapter(): Payment {
  return (amount: number, userId: string) => {
    aliPaySDK(amount, userId);
  };
}

/**
 * =========================
 * 业务层(Client)
 * =========================
 * 业务层只依赖 Payment 抽象
 * 不知道、也不关心:
 * - SDK 是谁
 * - Adapter 怎么实现
 *
 * 这就是依赖倒置原则
 */
function pay(
  payment: Payment,
  amount: number,
  userId: string
): void {
  payment(amount, userId);
}

/**
 * =========================
 * 使用
 * =========================
 * 先通过 Adapter 得到一个 Payment
 * 再交给业务层使用
 */
pay(wechatPayAdapter(), 100, "1234567890");
pay(aliPayAdapter(), 100, "1234567890");

场景:
H5 + 小程序 登录方式不同
业务层期望的统一接口

/**
 * =========================
 * 目标接口(Target Interface)
 * =========================
 * 业务层只认这个协议:
 * - “我只关心能不能登录成功”
 * - “不关心你是 H5 / 小程序 / App”
 *
 * 这是被「冻结」的登录协议
 */
//返回一个Promise resolve值的类型是User对象   TypeScript 目前无法对 reject 进行强类型约束
type Login = () => Promise<User>;

interface User {
  id: string;
  name: string;
  token: string;
}

/**
 * =========================
 * 被适配者(Adaptee)
 * =========================
 * 各个平台原生登录实现
 *
 * 特点:
 * - 接口形态不统一
 * - 返回字段不一致
 * - 业务层不应该直接依赖
 */

// H5:账号密码登录
async function h5Login(username: string, password: string) {
  return {
    user_id: '1001',
    nick_name: 'Tom',
    access_token: 'h5-token'
  };
}

// 小程序:code 登录
async function wxLogin() {
  return { code: 'wx-code' };
}

async function miniLogin(code: string) {
  return {
    uid: '2001',
    name: 'Jerry',
    token: 'mini-token'
  };
}

/**
 * =========================
 * 适配器(Adapter)
 * =========================
 * ⚠️ 关键点:
 * - Adapter 本身【不执行登录】
 * - Adapter 的职责是:
 *   👉 生产一个「符合 Login 协议的函数」
 *
 * 也就是说:
 * - h5LoginAdapter 不是 Login
 * - h5LoginAdapter() 的返回值 才是 Login
 */

// H5 登录适配器
function h5LoginAdapter(
  username: string,
  password: string
): Login {
  return async () => {
    const res = await h5Login(username, password);
    return {
      id: res.user_id,
      name: res.nick_name,
      token: res.access_token
    };
  };
}

// 小程序登录适配器
function miniProgramLoginAdapter(): Login {
  return async () => {
    const { code } = await wxLogin();
    const res = await miniLogin(code);
    return {
      id: res.uid,
      name: res.name,
      token: res.token
    };
  };
}

/**
 * =========================
 * 业务层(Client)
 * =========================
 * 业务层只依赖 Login 抽象
 * 完全不知道:
 * - 平台差异
 * - SDK 细节
 *
 * 这就是适配器模式 + 依赖倒置
 */
async function doLogin(login: Login) {
  const user = await login();
  console.log('登录成功:', user);
}

/**
 * =========================
 * 使用
 * =========================
 * 先通过 Adapter 得到一个 Login
 * 再交给业务层使用
 */

// H5 场景
doLogin(h5LoginAdapter('admin', '123456'));

// 小程序场景
doLogin(miniProgramLoginAdapter());

外观模式

给复杂系统提供一个简单、稳定、低心智负担的入口

/**
 * Storage 外观模式
 *
 * 目的:
 * - 为业务层提供统一、稳定、简单的存储接口
 * - 隐藏 localStorage / JSON / 异常等复杂度
 */

/**
 * 目标接口(Facade Interface)
 *
 * 被“冻结”的协议:
 * - 业务层只允许使用这 3 个方法
 * - 不允许直接访问 localStorage
 */
interface StorageFacade {
    get<T>(key: string): T | null;
    set<T>(key: string, value: T): void;
    remove(key: string): void;
}

/**
 * 工厂函数实现外观
 *
 * ⚠️ 使用闭包保存内部 state(如 prefix)
 * ⚠️ 返回对象就是外观
 */
function createStorageFacade(): StorageFacade {
    const prefix = '__app__';
    const rawStorage = window.localStorage;

    function formatKey(key: string) {
        return `${prefix}:${key}`;
    }

    function get<T>(key: string): T | null {
        try {
            const raw = rawStorage.getItem(formatKey(key));
            if (raw === null) return null;
            return JSON.parse(raw) as T;
        } catch (error) {
            console.error('[Storage.get] error:', error);
            return null;
        }
    }

    function set<T>(key: string, value: T): void {
        try {
            rawStorage.setItem(formatKey(key), JSON.stringify(value));
        } catch (error) {
            console.error('[Storage.set] error:', error);
        }
    }

    function remove(key: string): void {
        rawStorage.removeItem(formatKey(key));
    }

    return { get, set, remove };
}

/**
 * =====================================================
 * 对业务层暴露的唯一入口(Facade)
 * =====================================================
 */
export const storage = createStorageFacade();

/**
 * =====================================================
 * 业务层使用
 * =====================================================
 */
interface User {
    id: string;
    name: string;
}

// 写入
storage.set<User>('user', { id: '1001', name: 'Tom' });

// 读取
const user = storage.get<User>('user');
console.log('user name:', user?.name);

// 删除
storage.remove('user');

代理模式

为一个对象提供一个代理,通过代理控制对真实对象的访问,同时可以增加额外逻辑(缓存、权限、日志、节流等)。当外界打算访问真实对象的时候,该外界的感觉是访问的真实对象,但其实外界访问的是代理对象。

例:ES6中的Proxy构造方法

// 创建一个要被代理的对象
// 这是真实的对象
const target = {
  name: "John",
  age: 30,
};

// 定义代理的行为
// 代理对象就会对外界的访问需求进行过滤
const handler = {
  // 拦截对象属性的读取
  get(target, prop, receiver) {
    console.log(`[GET]: ${prop}`);
    return Reflect.get(target, prop, receiver);
  },
  // 拦截对象属性的设置
  set(target, prop, value, receiver) {
    console.log(`[SET]: ${prop} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  },
};

// 使用Proxy构造函数创建代理对象
// new Proxy 会返回一个对象,该对象就是针对真实对象的代理对象
const proxy = new Proxy(target, handler);

// 测试代理的行为
// 之后就通过代理对象来访问真实对象的成员
console.log(proxy.name); // 输出:[GET]: name 以及 John
proxy.age = 31; // 输出:[SET]: age = 31

场景:请求函数 增加token权限控制

/**
 * =========================
 * 请求函数类型(Target Interface)
 * 业务层只关心:
 * - 给 URL 和请求参数
 * - 返回一个 Promise
 *
 * requestParams:携带请求的 method/data/headers 等信息
 */
type RequestFn = (
    url: string,
    requestParams?: Record<string, any>
  ) => Promise<any>;
  
  /**
   * =========================
   * 真正的请求对象(Real Subject)
   * =========================
   * 只负责发送请求,不关心权限、token 等
   */
  const realRequest: RequestFn = async (url, requestParams) => {
    console.log(`[Real Request] 请求 ${url},参数:`, requestParams);
    return axios({ url, ...requestParams });
  };
  
  /**
   * =========================
   * 代理对象(Proxy)
   * =========================
   * 为 realRequest 增加权限校验逻辑
   *
   * 代理职责:
   * - 请求前检查 token
   * - 如果没有 token,直接抛错,阻止请求
   * - 如果有 token,则调用真实请求
   */
  function createAuthProxy(fn: RequestFn): RequestFn {
    return async (url, requestParams) => {
      // 获取 token(例如从 localStorage)
      const token = localStorage.getItem('token');
  
      // 没有 token,直接中断
      if (!token) {
        throw new Error('[Proxy] 无权限访问,请先登录');
      }
  
      // 有 token,则把 token 放入 headers 并调用真实请求
      const paramsWithToken = {
        ...requestParams,
        headers: {
          ...(requestParams?.headers || {}),
          Authorization: `Bearer ${token}`
        }
      };
  
      console.log(`[Proxy] 权限校验通过,携带 token 请求 ${url}`);
  
      return fn(url, paramsWithToken);
    };
  }
  
  /**
   * =========================
   * 使用示例
   * =========================
   * client 只调用代理函数,不关心内部是权限校验还是请求
   */
  const requestWithAuth = createAuthProxy(realRequest);
  
  (async () => {
    // 模拟设置 token
    localStorage.setItem('token', 'my-secret-token');
  
    // GET 请求
    console.log(await requestWithAuth('/api/user', { method: 'GET' }));
  
    // POST 请求
    console.log(
      await requestWithAuth('/api/product', {
        method: 'POST',
        data: { name: 'iPhone' }
      })
    );
  
    // 模拟 token 失效
    localStorage.removeItem('token');
  
    try {
      await requestWithAuth('/api/order', { method: 'GET' });
    } catch (e) {
      console.error(e.message); // 输出: [Proxy] 无权限访问,请先登录
    }
  })();