02-结构型模式

13 阅读20分钟

第二部分:结构型模式

结构型模式关注类和对象的组合,帮助你构建更灵活、更易维护的代码结构

目录


装饰器模式

💡 模式定义

装饰器模式(Decorator Pattern)允许在不改变对象自身的基础上,动态地给对象添加新的功能。它是通过创建包装对象来实现的。

🤔 为什么需要装饰器模式?

问题场景:给组件添加权限控制、日志、性能监控等功能

假设你有一个用户信息页面,现在需要添加权限控制、加载状态、错误边界等功能:

❌ 不使用装饰器模式的痛点
// UserProfile.jsx - 所有功能都耦合在一起
function UserProfile({ userId }) {
  const [user, setUser] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [hasPermission, setHasPermission] = React.useState(false);

  React.useEffect(() => {
    // 权限检查逻辑
    const checkPermission = async () => {
      try {
        const permission = await checkUserPermission();
        setHasPermission(permission);
      } catch (err) {
        setError('权限检查失败');
      }
    };

    // 数据加载逻辑
    const loadUser = async () => {
      if (!hasPermission) return;

      try {
        setLoading(true);

        // 性能监控
        const startTime = performance.now();

        // 日志记录
        console.log('开始加载用户:', userId);

        const data = await fetchUser(userId);
        setUser(data);

        // 性能监控
        const endTime = performance.now();
        console.log(`加载用户耗时: ${endTime - startTime}ms`);

        // 日志记录
        console.log('用户加载成功:', data);
      } catch (err) {
        setError(err.message);
        console.error('用户加载失败:', err);
      } finally {
        setLoading(false);
      }
    };

    checkPermission().then(loadUser);
  }, [userId, hasPermission]);

  // 错误边界逻辑
  if (error) {
    return <div className="error">错误: {error}</div>;
  }

  // 权限检查逻辑
  if (!hasPermission) {
    return <div className="no-permission">您没有权限查看此页面</div>;
  }

  // 加载状态逻辑
  if (loading) {
    return <div className="loading">加载中...</div>;
  }

  // 核心业务逻辑
  return (
    <div className="user-profile">
      <h1>{user.name}</h1>
      <p>邮箱: {user.email}</p>
      <p>年龄: {user.age}</p>
    </div>
  );
}

问题

  • 组件职责过多:权限、日志、性能、错误处理、业务逻辑全部混在一起
  • 难以复用:其他组件也需要权限、加载、错误处理时,只能复制粘贴
  • 难以测试:测试业务逻辑必须同时处理所有辅助逻辑
  • 难以维护:修改加载逻辑可能影响权限检查

✅ 使用装饰器模式(HOC)解决
// UserProfile.jsx - 只关注核心业务逻辑
function UserProfile({ user }) {
  return (
    <div className="user-profile">
      <h1>{user.name}</h1>
      <p>邮箱: {user.email}</p>
      <p>年龄: {user.age}</p>
    </div>
  );
}

export default UserProfile;
// decorators/withAuth.jsx - 权限装饰器
function withAuth(requiredPermission) {
  return function(Component) {
    return function WithAuthComponent(props) {
      const [hasPermission, setHasPermission] = React.useState(null);

      React.useEffect(() => {
        checkUserPermission(requiredPermission).then(setHasPermission);
      }, []);

      if (hasPermission === null) {
        return <div>检查权限中...</div>;
      }

      if (!hasPermission) {
        return <div className="no-permission">您没有权限查看此页面</div>;
      }

      return <Component {...props} />;
    };
  };
}

export default withAuth;
// decorators/withLoading.jsx - 加载状态装饰器
function withLoading(fetchDataFn) {
  return function(Component) {
    return function WithLoadingComponent(props) {
      const [data, setData] = React.useState(null);
      const [loading, setLoading] = React.useState(true);

      React.useEffect(() => {
        setLoading(true);
        fetchDataFn(props)
          .then(setData)
          .finally(() => setLoading(false));
      }, [props.userId]); // 根据实际情况调整依赖

      if (loading) {
        return <div className="loading">加载中...</div>;
      }

      return <Component {...props} data={data} />;
    };
  };
}

export default withLoading;
// decorators/withErrorBoundary.jsx - 错误边界装饰器
function withErrorBoundary(Component) {
  return class WithErrorBoundary extends React.Component {
    constructor(props) {
      super(props);
      this.state = { hasError: false, error: null };
    }

    static getDerivedStateFromError(error) {
      return { hasError: true, error };
    }

    componentDidCatch(error, errorInfo) {
      console.error('组件错误:', error, errorInfo);
    }

    render() {
      if (this.state.hasError) {
        return (
          <div className="error">
            <h2>出错了</h2>
            <p>{this.state.error?.message}</p>
          </div>
        );
      }

      return <Component {...this.props} />;
    }
  };
}

export default withErrorBoundary;
// decorators/withLogger.jsx - 日志装饰器
function withLogger(componentName) {
  return function(Component) {
    return function WithLoggerComponent(props) {
      React.useEffect(() => {
        console.log(`[${componentName}] 组件挂载`, props);
        return () => {
          console.log(`[${componentName}] 组件卸载`);
        };
      }, []);

      React.useEffect(() => {
        console.log(`[${componentName}] Props 更新`, props);
      }, [props]);

      return <Component {...props} />;
    };
  };
}

export default withLogger;
// decorators/withPerformance.jsx - 性能监控装饰器
function withPerformance(componentName) {
  return function(Component) {
    return function WithPerformanceComponent(props) {
      const renderStartTime = React.useRef(performance.now());

      React.useEffect(() => {
        const renderEndTime = performance.now();
        const renderTime = renderEndTime - renderStartTime.current;
        console.log(`[性能] ${componentName} 渲染耗时: ${renderTime.toFixed(2)}ms`);
      });

      return <Component {...props} />;
    };
  };
}

export default withPerformance;
// UserProfileContainer.jsx - 组合使用装饰器
import UserProfile from './UserProfile';
import withAuth from './decorators/withAuth';
import withLoading from './decorators/withLoading';
import withErrorBoundary from './decorators/withErrorBoundary';
import withLogger from './decorators/withLogger';
import withPerformance from './decorators/withPerformance';
import { fetchUser } from './api';

// 组合多个装饰器
const EnhancedUserProfile = withErrorBoundary(
  withAuth('user:read')(
    withLoading(({ userId }) => fetchUser(userId))(
      withLogger('UserProfile')(
        withPerformance('UserProfile')(
          UserProfile
        )
      )
    )
  )
);

// 使用
function App() {
  return <EnhancedUserProfile userId={1} />;
}

效果

  • ✅ 单一职责:每个装饰器只做一件事
  • ✅ 高度复用:装饰器可以用于任何组件
  • ✅ 易于测试:可以独立测试业务组件和装饰器
  • ✅ 灵活组合:可以自由组合不同的装饰器

🎯 使用 Hooks 实现装饰器模式

在现代 React 中,我们更倾向于使用 Hooks:

// hooks/useAuth.js
function useAuth(requiredPermission) {
  const [hasPermission, setHasPermission] = React.useState(null);
  const [loading, setLoading] = React.useState(true);

  React.useEffect(() => {
    checkUserPermission(requiredPermission)
      .then(setHasPermission)
      .finally(() => setLoading(false));
  }, [requiredPermission]);

  return { hasPermission, loading };
}

export default useAuth;
// hooks/useDataFetching.js
function useDataFetching(fetchFn, deps = []) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    setLoading(true);
    setError(null);

    fetchFn()
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, deps);

  return { data, loading, error };
}

export default useDataFetching;
// hooks/useLogger.js
function useLogger(componentName, data) {
  React.useEffect(() => {
    console.log(`[${componentName}] 数据更新:`, data);
  }, [componentName, data]);
}

export default useLogger;
// UserProfile.jsx - 使用 Hooks 组合功能
import useAuth from './hooks/useAuth';
import useDataFetching from './hooks/useDataFetching';
import useLogger from './hooks/useLogger';
import { fetchUser } from './api';

function UserProfile({ userId }) {
  // 权限检查
  const { hasPermission, loading: authLoading } = useAuth('user:read');

  // 数据加载
  const { data: user, loading: dataLoading, error } = useDataFetching(
    () => fetchUser(userId),
    [userId]
  );

  // 日志记录
  useLogger('UserProfile', user);

  if (authLoading) {
    return <div>检查权限中...</div>;
  }

  if (!hasPermission) {
    return <div>您没有权限查看此页面</div>;
  }

  if (error) {
    return <div>错误: {error.message}</div>;
  }

  if (dataLoading) {
    return <div>加载中...</div>;
  }

  return (
    <div className="user-profile">
      <h1>{user.name}</h1>
      <p>邮箱: {user.email}</p>
      <p>年龄: {user.age}</p>
    </div>
  );
}

🏗️ 真实业务场景

场景1:API 请求装饰器(中间件)
// api/middleware/authMiddleware.js - 认证中间件
function authMiddleware(request) {
  return function decoratedRequest(config) {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers = {
        ...config.headers,
        Authorization: `Bearer ${token}`,
      };
    }
    return request(config);
  };
}

// api/middleware/loggerMiddleware.js - 日志中间件
function loggerMiddleware(request) {
  return function decoratedRequest(config) {
    console.log('API 请求:', config.url, config);
    const startTime = Date.now();

    return request(config).then(
      response => {
        const duration = Date.now() - startTime;
        console.log(`API 响应: ${config.url} (${duration}ms)`, response);
        return response;
      },
      error => {
        const duration = Date.now() - startTime;
        console.error(`API 错误: ${config.url} (${duration}ms)`, error);
        throw error;
      }
    );
  };
}

// api/middleware/retryMiddleware.js - 重试中间件
function retryMiddleware(maxRetries = 3) {
  return function(request) {
    return function decoratedRequest(config) {
      let retries = 0;

      const attemptRequest = () => {
        return request(config).catch(error => {
          if (retries < maxRetries && isRetryableError(error)) {
            retries++;
            console.log(`重试请求 (${retries}/${maxRetries}):`, config.url);
            return new Promise(resolve => setTimeout(resolve, 1000 * retries))
              .then(attemptRequest);
          }
          throw error;
        });
      };

      return attemptRequest();
    };
  };
}

function isRetryableError(error) {
  return error.response?.status >= 500 || error.code === 'ECONNABORTED';
}

// api/middleware/cacheMiddleware.js - 缓存中间件
function cacheMiddleware(request) {
  const cache = new Map();

  return function decoratedRequest(config) {
    if (config.method?.toLowerCase() !== 'get') {
      return request(config);
    }

    const cacheKey = `${config.url}?${JSON.stringify(config.params)}`;

    if (cache.has(cacheKey)) {
      console.log('从缓存返回:', cacheKey);
      return Promise.resolve(cache.get(cacheKey));
    }

    return request(config).then(response => {
      cache.set(cacheKey, response);
      return response;
    });
  };
}

// api/client.js - 组合中间件
function createApiClient() {
  // 基础请求函数
  const baseRequest = (config) => {
    return fetch(config.url, {
      method: config.method || 'GET',
      headers: config.headers,
      body: config.data ? JSON.stringify(config.data) : undefined,
    }).then(response => response.json());
  };

  // 层层装饰
  let request = baseRequest;
  request = authMiddleware(request);
  request = loggerMiddleware(request);
  request = retryMiddleware(3)(request);
  request = cacheMiddleware(request);

  return {
    get: (url, config = {}) => request({ ...config, url, method: 'GET' }),
    post: (url, data, config = {}) => request({ ...config, url, method: 'POST', data }),
  };
}

export default createApiClient();
// 使用
import apiClient from './api/client';

function UserList() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    // 自动带上认证、日志、重试、缓存功能
    apiClient.get('/users').then(setUsers);
  }, []);

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
场景2:表单验证装饰器
// decorators/withFormValidation.jsx
function withFormValidation(validationRules) {
  return function(Component) {
    return function WithValidationComponent(props) {
      const [values, setValues] = React.useState({});
      const [errors, setErrors] = React.useState({});
      const [touched, setTouched] = React.useState({});

      const validate = (fieldName, value) => {
        const rules = validationRules[fieldName];
        if (!rules) return '';

        for (const rule of rules) {
          const error = rule(value, values);
          if (error) return error;
        }
        return '';
      };

      const handleChange = (fieldName, value) => {
        setValues(prev => ({ ...prev, [fieldName]: value }));

        if (touched[fieldName]) {
          const error = validate(fieldName, value);
          setErrors(prev => ({ ...prev, [fieldName]: error }));
        }
      };

      const handleBlur = (fieldName) => {
        setTouched(prev => ({ ...prev, [fieldName]: true }));
        const error = validate(fieldName, values[fieldName]);
        setErrors(prev => ({ ...prev, [fieldName]: error }));
      };

      const handleSubmit = (e) => {
        e.preventDefault();

        // 验证所有字段
        const newErrors = {};
        Object.keys(validationRules).forEach(fieldName => {
          const error = validate(fieldName, values[fieldName]);
          if (error) newErrors[fieldName] = error;
        });

        setErrors(newErrors);
        setTouched(
          Object.keys(validationRules).reduce((acc, key) => {
            acc[key] = true;
            return acc;
          }, {})
        );

        if (Object.keys(newErrors).length === 0) {
          props.onSubmit?.(values);
        }
      };

      return (
        <Component
          {...props}
          values={values}
          errors={errors}
          touched={touched}
          onChange={handleChange}
          onBlur={handleBlur}
          onSubmit={handleSubmit}
        />
      );
    };
  };
}

// 验证规则
const required = (message = '此字段为必填') => (value) => {
  return !value ? message : '';
};

const minLength = (length, message) => (value) => {
  return value && value.length < length ? message || `最少${length}个字符` : '';
};

const email = (message = '请输入有效的邮箱') => (value) => {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return value && !regex.test(value) ? message : '';
};

export { withFormValidation, required, minLength, email };
// LoginForm.jsx
import { withFormValidation, required, minLength, email } from './decorators/withFormValidation';

function LoginForm({ values, errors, touched, onChange, onBlur, onSubmit }) {
  return (
    <form onSubmit={onSubmit}>
      <div>
        <input
          type="email"
          placeholder="邮箱"
          value={values.email || ''}
          onChange={(e) => onChange('email', e.target.value)}
          onBlur={() => onBlur('email')}
        />
        {touched.email && errors.email && (
          <span className="error">{errors.email}</span>
        )}
      </div>

      <div>
        <input
          type="password"
          placeholder="密码"
          value={values.password || ''}
          onChange={(e) => onChange('password', e.target.value)}
          onBlur={() => onBlur('password')}
        />
        {touched.password && errors.password && (
          <span className="error">{errors.password}</span>
        )}
      </div>

      <button type="submit">登录</button>
    </form>
  );
}

// 装饰表单
const EnhancedLoginForm = withFormValidation({
  email: [required('邮箱不能为空'), email()],
  password: [required('密码不能为空'), minLength(6, '密码至少6个字符')],
})(LoginForm);

// 使用
function App() {
  const handleSubmit = (values) => {
    console.log('提交表单:', values);
    // 调用登录 API
  };

  return <EnhancedLoginForm onSubmit={handleSubmit} />;
}

🎨 在主流框架中的应用

React - 高阶组件(HOC)
// React Router 的 withRouter
import { withRouter } from 'react-router-dom';

function MyComponent({ history, location, match }) {
  return <div>当前路径: {location.pathname}</div>;
}

export default withRouter(MyComponent);

// Redux 的 connect
import { connect } from 'react-redux';

function UserProfile({ user, loadUser }) {
  return <div>{user.name}</div>;
}

const mapStateToProps = state => ({
  user: state.user,
});

const mapDispatchToProps = {
  loadUser: loadUserAction,
};

export default connect(mapStateToProps, mapDispatchToProps)(UserProfile);
JavaScript Decorator 语法
// 使用 @decorator 语法(需要 Babel 支持)
function readonly(target, key, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

function log(target, key, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`调用 ${key},参数:`, args);
    const result = original.apply(this, args);
    console.log(`${key} 返回:`, result);
    return result;
  };
  return descriptor;
}

class User {
  @readonly
  name = 'John';

  @log
  sayHello(greeting) {
    return `${greeting}, ${this.name}`;
  }
}

const user = new User();
user.sayHello('Hi'); // 自动打印日志
// user.name = 'Jane'; // 报错,只读属性
Express 中间件 - 装饰器模式的典型应用
const express = require('express');
const app = express();

// 日志中间件
function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next();
}

// 认证中间件
function authenticate(req, res, next) {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  // 验证 token
  next();
}

// 组合使用中间件
app.use(logger);
app.use('/api', authenticate); // 只对 /api 路由启用认证

app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

⚖️ 优缺点分析

✅ 优点
  1. 单一职责:每个装饰器只做一件事
  2. 开闭原则:可以动态添加功能,无需修改原有代码
  3. 高度复用:装饰器可以应用于多个组件
  4. 灵活组合:可以自由组合不同的装饰器
  5. 易于测试:可以独立测试装饰器和核心组件
❌ 缺点
  1. 多层包装:过多的装饰器会导致层次过深
  2. 调试困难:错误堆栈可能很长,难以定位问题
  3. 顺序敏感:装饰器的顺序可能影响结果
  4. Props 传递:HOC 需要手动传递 props

📋 何时使用装饰器模式

✅ 适合使用的场景
  • 需要动态添加功能(权限、日志、性能监控)
  • 多个组件需要相同的功能增强
  • 中间件系统(API 请求、Express 路由)
  • 需要组合多个独立功能
  • 不想修改原有代码
❌ 不适合使用的场景
  • 只在一个地方使用的功能
  • 装饰器之间有复杂的依赖关系
  • 需要频繁修改核心逻辑
  • 性能敏感的场景(装饰器有一定开销)

🎓 装饰器模式最佳实践

// ✅ 好的实践:装饰器职责单一、可组合
const withAuth = (permission) => (Component) => { /* ... */ };
const withLoading = (fetchFn) => (Component) => { /* ... */ };
const withErrorBoundary = (Component) => { /* ... */ };

// 组合使用
const Enhanced = withAuth('user:read')(
  withLoading(fetchUser)(
    withErrorBoundary(UserProfile)
  )
);

// ❌ 坏的实践:一个装饰器做太多事
const withEverything = (Component) => {
  // 认证、加载、错误处理、日志全部混在一起
  // 难以复用和测试
};

// ✅ 使用 compose 函数优化组合
function compose(...fns) {
  return fns.reduce((a, b) => (...args) => a(b(...args)));
}

const enhance = compose(
  withAuth('user:read'),
  withLoading(fetchUser),
  withErrorBoundary
);

const Enhanced = enhance(UserProfile);

代理模式

💡 模式定义

代理模式(Proxy Pattern)为其他对象提供一种代理,以控制对这个对象的访问。代理对象在客户端和目标对象之间起到中介作用。

🤔 为什么需要代理模式?

问题场景:控制对象访问、懒加载、缓存、权限控制
❌ 不使用代理模式的痛点
// ImageGallery.jsx - 直接加载所有图片
function ImageGallery({ images }) {
  return (
    <div className="gallery">
      {images.map(image => (
        <div key={image.id} className="image-item">
          {/* 所有图片立即加载,即使不在可视区域 */}
          <img
            src={image.largeUrl} // 加载大图
            alt={image.title}
            style={{ width: '100%', height: 'auto' }}
          />
          <p>{image.title}</p>
        </div>
      ))}
    </div>
  );
}

// 使用
function App() {
  const images = [
    { id: 1, title: '图片1', largeUrl: 'large1.jpg' },
    { id: 2, title: '图片2', largeUrl: 'large2.jpg' },
    // ... 100 张图片
  ];

  return <ImageGallery images={images} />;
}

问题

  • 页面加载慢:所有图片立即加载,即使不在可视区域
  • 浪费带宽:用户可能不会浏览到所有图片
  • 内存占用高:100 张大图同时加载

✅ 使用代理模式解决(懒加载图片)
// components/LazyImage.jsx - 图片代理组件
function LazyImage({ src, thumbnail, alt, ...props }) {
  const [imageSrc, setImageSrc] = React.useState(thumbnail || null);
  const [isLoading, setIsLoading] = React.useState(true);
  const imgRef = React.useRef();

  React.useEffect(() => {
    // 使用 Intersection Observer 监听图片是否进入视口
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            // 图片进入视口,开始加载真实图片
            loadImage(src);
            observer.unobserve(entry.target);
          }
        });
      },
      {
        rootMargin: '50px', // 提前 50px 开始加载
      }
    );

    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    return () => {
      if (imgRef.current) {
        observer.unobserve(imgRef.current);
      }
    };
  }, [src]);

  const loadImage = (imageSrc) => {
    const img = new Image();
    img.src = imageSrc;
    img.onload = () => {
      setImageSrc(imageSrc);
      setIsLoading(false);
    };
    img.onerror = () => {
      setIsLoading(false);
      console.error('图片加载失败:', imageSrc);
    };
  };

  return (
    <div ref={imgRef} className="lazy-image-container">
      <img
        src={imageSrc}
        alt={alt}
        className={isLoading ? 'loading' : 'loaded'}
        {...props}
      />
      {isLoading && <div className="placeholder">加载中...</div>}
    </div>
  );
}

export default LazyImage;
// ImageGallery.jsx - 使用代理组件
import LazyImage from './components/LazyImage';

function ImageGallery({ images }) {
  return (
    <div className="gallery">
      {images.map(image => (
        <div key={image.id} className="image-item">
          {/* 使用代理组件,只在需要时加载 */}
          <LazyImage
            src={image.largeUrl}
            thumbnail={image.thumbnailUrl}
            alt={image.title}
            style={{ width: '100%', height: 'auto' }}
          />
          <p>{image.title}</p>
        </div>
      ))}
    </div>
  );
}

效果

  • ✅ 按需加载:只加载可视区域的图片
  • ✅ 节省带宽:用户不看的图片不加载
  • ✅ 性能提升:页面加载更快
  • ✅ 用户体验:先显示缩略图,再加载大图

🎯 ES6 Proxy 实现响应式数据

// reactive.js - 使用 Proxy 实现响应式
function reactive(target) {
  const handlers = {
    get(target, key, receiver) {
      console.log(`读取属性: ${key}`);

      // 如果属性值是对象,递归代理
      const result = Reflect.get(target, key, receiver);
      if (typeof result === 'object' && result !== null) {
        return reactive(result);
      }

      return result;
    },

    set(target, key, value, receiver) {
      const oldValue = target[key];
      console.log(`设置属性: ${key}, 旧值: ${oldValue}, 新值: ${value}`);

      const result = Reflect.set(target, key, value, receiver);

      // 触发更新
      if (oldValue !== value) {
        console.log(`属性 ${key} 发生变化,触发更新`);
      }

      return result;
    },

    deleteProperty(target, key) {
      console.log(`删除属性: ${key}`);
      return Reflect.deleteProperty(target, key);
    },
  };

  return new Proxy(target, handlers);
}

// 使用
const user = reactive({
  name: 'John',
  age: 25,
  address: {
    city: 'Beijing',
    street: 'xxx'
  }
});

user.name; // 读取属性: name
user.name = 'Jane'; // 设置属性: name, 旧值: John, 新值: Jane
user.address.city; // 读取属性: address -> 读取属性: city
user.address.city = 'Shanghai'; // 触发更新

🏗️ 真实业务场景

场景1:API 缓存代理
// api/cacheProxy.js - API 缓存代理
class ApiCacheProxy {
  constructor(apiClient, options = {}) {
    this.apiClient = apiClient;
    this.cache = new Map();
    this.cacheTime = options.cacheTime || 5 * 60 * 1000; // 默认 5 分钟
    this.maxCacheSize = options.maxCacheSize || 100; // 最多缓存 100 个请求
  }

  _getCacheKey(url, params) {
    return `${url}?${JSON.stringify(params || {})}`;
  }

  _isExpired(cacheEntry) {
    return Date.now() - cacheEntry.timestamp > this.cacheTime;
  }

  _cleanupCache() {
    if (this.cache.size >= this.maxCacheSize) {
      // 删除最旧的缓存
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
  }

  async get(url, params, options = {}) {
    const cacheKey = this._getCacheKey(url, params);

    // 检查缓存
    if (this.cache.has(cacheKey) && !options.forceRefresh) {
      const cacheEntry = this.cache.get(cacheKey);

      if (!this._isExpired(cacheEntry)) {
        console.log('从缓存返回:', url);
        return cacheEntry.data;
      } else {
        // 缓存过期,删除
        this.cache.delete(cacheKey);
      }
    }

    // 发起真实请求
    console.log('从 API 获取:', url);
    const data = await this.apiClient.get(url, params);

    // 存入缓存
    this._cleanupCache();
    this.cache.set(cacheKey, {
      data,
      timestamp: Date.now(),
    });

    return data;
  }

  async post(url, data, params) {
    // POST 请求会使缓存失效
    this.clearCache();
    return await this.apiClient.post(url, data, params);
  }

  clearCache() {
    console.log('清空缓存');
    this.cache.clear();
  }

  getCacheStats() {
    return {
      size: this.cache.size,
      maxSize: this.maxCacheSize,
      entries: Array.from(this.cache.entries()).map(([key, value]) => ({
        key,
        timestamp: value.timestamp,
        age: Date.now() - value.timestamp,
      })),
    };
  }
}

export default ApiCacheProxy;
// 使用缓存代理
import axios from 'axios';
import ApiCacheProxy from './api/cacheProxy';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
});

const cachedApi = new ApiCacheProxy(apiClient, {
  cacheTime: 10 * 60 * 1000, // 10 分钟
  maxCacheSize: 50,
});

function UserList() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    // 第一次从 API 获取
    cachedApi.get('/users').then(setUsers);

    // 5 秒后再次请求,会从缓存返回
    setTimeout(() => {
      cachedApi.get('/users').then(data => {
        console.log('第二次请求,从缓存获取');
      });
    }, 5000);
  }, []);

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
场景2:权限控制代理
// security/PermissionProxy.js - 权限控制代理
class PermissionProxy {
  constructor(targetObject, userPermissions) {
    this.target = targetObject;
    this.permissions = userPermissions;

    return new Proxy(targetObject, {
      get: (target, prop) => {
        // 检查方法权限
        const requiredPermission = target.getRequiredPermission?.(prop);

        if (requiredPermission && !this.hasPermission(requiredPermission)) {
          throw new Error(`没有权限执行操作: ${prop}`);
        }

        const value = target[prop];

        // 如果是函数,返回代理后的函数
        if (typeof value === 'function') {
          return (...args) => {
            console.log(`执行操作: ${prop}`);
            return value.apply(target, args);
          };
        }

        return value;
      },

      set: (target, prop, value) => {
        // 检查写入权限
        if (!this.hasPermission('write')) {
          throw new Error('没有写入权限');
        }

        console.log(`设置属性: ${prop} = ${value}`);
        target[prop] = value;
        return true;
      },
    });
  }

  hasPermission(permission) {
    return this.permissions.includes(permission);
  }
}

// 使用
class UserManager {
  constructor() {
    this.users = [];
  }

  getRequiredPermission(method) {
    const permissionMap = {
      getUsers: 'user:read',
      addUser: 'user:create',
      deleteUser: 'user:delete',
    };
    return permissionMap[method];
  }

  getUsers() {
    return this.users;
  }

  addUser(user) {
    this.users.push(user);
    return user;
  }

  deleteUser(userId) {
    this.users = this.users.filter(u => u.id !== userId);
  }
}

// 创建代理
const adminManager = new PermissionProxy(
  new UserManager(),
  ['user:read', 'user:create', 'user:delete'] // 管理员权限
);

const guestManager = new PermissionProxy(
  new UserManager(),
  ['user:read'] // 游客只有读权限
);

// 管理员可以执行所有操作
adminManager.addUser({ id: 1, name: 'John' }); // ✅ 成功
adminManager.getUsers(); // ✅ 成功

// 游客只能读取
guestManager.getUsers(); // ✅ 成功
// guestManager.addUser({ id: 2, name: 'Jane' }); // ❌ 抛出错误:没有权限
场景3:虚拟代理(懒加载模块)
// modules/lazyModuleProxy.js - 模块懒加载代理
function createLazyModuleProxy(importFn) {
  let moduleCache = null;
  let loadingPromise = null;

  return new Proxy({}, {
    get(target, prop) {
      // 如果模块已加载,直接返回
      if (moduleCache) {
        return moduleCache[prop];
      }

      // 如果正在加载,等待加载完成
      if (loadingPromise) {
        return async (...args) => {
          const module = await loadingPromise;
          return module[prop](...args);
        };
      }

      // 开始加载模块
      loadingPromise = importFn().then(module => {
        moduleCache = module;
        loadingPromise = null;
        return module;
      });

      // 返回一个异步函数
      return async (...args) => {
        const module = await loadingPromise;
        return module[prop](...args);
      };
    },
  });
}

// 使用
const heavyModule = createLazyModuleProxy(() => import('./heavyModule'));

// 第一次调用时才加载模块
async function handleClick() {
  await heavyModule.doSomething(); // 触发模块加载
}

🎨 在主流框架中的应用

Vue 3 响应式系统 - Proxy 的典型应用
// Vue 3 使用 Proxy 实现响应式
import { reactive, effect } from 'vue';

const state = reactive({
  count: 0,
  user: {
    name: 'John',
  },
});

// 自动追踪依赖
effect(() => {
  console.log('count:', state.count);
});

state.count++; // 自动触发 effect,打印 "count: 1"
Mobx - Proxy 实现状态管理
import { observable, autorun } from 'mobx';

const store = observable({
  count: 0,
  increment() {
    this.count++;
  },
});

autorun(() => {
  console.log('Count:', store.count);
});

store.increment(); // 自动触发 autorun
Immer - Proxy 实现不可变数据
import produce from 'immer';

const state = {
  users: [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' },
  ],
};

const nextState = produce(state, draft => {
  // 可以直接修改 draft,Immer 会自动创建新对象
  draft.users[0].name = 'John Doe';
  draft.users.push({ id: 3, name: 'Bob' });
});

console.log(state === nextState); // false,创建了新对象
console.log(state.users === nextState.users); // false

⚖️ 优缺点分析

✅ 优点
  1. 控制访问:可以在访问对象前进行权限检查
  2. 懒加载:延迟创建开销大的对象
  3. 缓存:缓存结果,提升性能
  4. 日志记录:记录对象的访问和修改
  5. 远程代理:隐藏对象在不同地址空间的事实
❌ 缺点
  1. 增加复杂度:引入额外的代理层
  2. 性能开销:每次访问都要经过代理
  3. 调试困难:错误堆栈可能不直观

📋 何时使用代理模式

✅ 适合使用的场景
  • 懒加载(虚拟代理):延迟创建开销大的对象
  • 权限控制(保护代理):控制对对象的访问
  • 缓存(智能代理):缓存结果,避免重复计算
  • 远程代理:访问远程对象
  • 日志记录:记录对象访问
❌ 不适合使用的场景
  • 简单对象,不需要额外控制
  • 性能敏感的场景
  • 代理逻辑比目标对象还复杂

适配器模式

💡 模式定义

适配器模式(Adapter Pattern)将一个类的接口转换成客户端期望的另一个接口,使得原本接口不兼容的类可以一起工作。

🤔 为什么需要适配器模式?

问题场景:接口不兼容、第三方库迁移、数据格式转换

假设你的项目最初使用 Axios,现在要迁移到 Fetch API:

❌ 不使用适配器模式的痛点
// 原来的代码使用 Axios
import axios from 'axios';

function UserList() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    axios.get('/api/users')
      .then(response => setUsers(response.data))
      .catch(error => console.error(error));
  }, []);

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

function ProductList() {
  const [products, setProducts] = React.useState([]);

  React.useEffect(() => {
    axios.get('/api/products')
      .then(response => setProducts(response.data))
      .catch(error => console.error(error));
  }, []);

  return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

// ... 100 个组件都使用了 axios.get

现在需要迁移到 Fetch

// 需要逐个修改所有组件 😱
function UserList() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    fetch('/api/users')
      .then(response => response.json()) // 不同!
      .then(data => setUsers(data)) // 不同!
      .catch(error => console.error(error));
  }, []);

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

// 需要修改所有 100 个组件... 😭

问题

  • 修改成本高:需要修改所有使用 Axios 的地方
  • 容易出错:可能漏掉某些地方
  • API 不一致:Axios 和 Fetch 的返回值结构不同

✅ 使用适配器模式解决
// api/httpAdapter.js - HTTP 客户端适配器
class HttpAdapter {
  constructor(client) {
    this.client = client;
  }

  async get(url, config = {}) {
    return this.request({ ...config, url, method: 'GET' });
  }

  async post(url, data, config = {}) {
    return this.request({ ...config, url, method: 'POST', data });
  }

  async put(url, data, config = {}) {
    return this.request({ ...config, url, method: 'PUT', data });
  }

  async delete(url, config = {}) {
    return this.request({ ...config, url, method: 'DELETE' });
  }

  async request(config) {
    throw new Error('Subclass must implement request method');
  }
}

// api/axiosAdapter.js - Axios 适配器
import axios from 'axios';

class AxiosAdapter extends HttpAdapter {
  async request(config) {
    try {
      const response = await axios({
        url: config.url,
        method: config.method,
        data: config.data,
        params: config.params,
        headers: config.headers,
      });

      // 统一返回格式
      return {
        data: response.data,
        status: response.status,
        headers: response.headers,
      };
    } catch (error) {
      throw {
        message: error.message,
        status: error.response?.status,
        data: error.response?.data,
      };
    }
  }
}

// api/fetchAdapter.js - Fetch 适配器
class FetchAdapter extends HttpAdapter {
  async request(config) {
    try {
      const response = await fetch(config.url, {
        method: config.method,
        headers: {
          'Content-Type': 'application/json',
          ...config.headers,
        },
        body: config.data ? JSON.stringify(config.data) : undefined,
      });

      const data = await response.json();

      if (!response.ok) {
        throw {
          message: `HTTP ${response.status}`,
          status: response.status,
          data,
        };
      }

      // 统一返回格式(与 Axios 适配器相同)
      return {
        data,
        status: response.status,
        headers: response.headers,
      };
    } catch (error) {
      throw {
        message: error.message,
        status: error.status,
        data: error.data,
      };
    }
  }
}

export { HttpAdapter, AxiosAdapter, FetchAdapter };
// api/client.js - 使用适配器
import { AxiosAdapter, FetchAdapter } from './httpAdapter';

// 可以轻松切换实现
// const httpClient = new AxiosAdapter();
const httpClient = new FetchAdapter();

export default httpClient;
// 组件代码无需修改!
import httpClient from './api/client';

function UserList() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    // 无论底层是 Axios 还是 Fetch,代码都不用改
    httpClient.get('/api/users')
      .then(response => setUsers(response.data))
      .catch(error => console.error(error));
  }, []);

  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

效果

  • ✅ 统一接口:所有 HTTP 客户端使用相同接口
  • ✅ 易于切换:修改一行代码即可切换实现
  • ✅ 零改动:组件代码无需修改
  • ✅ 易于扩展:可以轻松添加新的适配器

🏗️ 真实业务场景

场景1:LocalStorage 适配器(兼容不同存储方案)
// storage/StorageAdapter.js
class StorageAdapter {
  get(key) {
    throw new Error('Subclass must implement get method');
  }

  set(key, value) {
    throw new Error('Subclass must implement set method');
  }

  remove(key) {
    throw new Error('Subclass must implement remove method');
  }

  clear() {
    throw new Error('Subclass must implement clear method');
  }
}

// storage/LocalStorageAdapter.js
class LocalStorageAdapter extends StorageAdapter {
  get(key) {
    try {
      const value = localStorage.getItem(key);
      return value ? JSON.parse(value) : null;
    } catch (error) {
      console.error('LocalStorage get error:', error);
      return null;
    }
  }

  set(key, value) {
    try {
      localStorage.setItem(key, JSON.stringify(value));
      return true;
    } catch (error) {
      console.error('LocalStorage set error:', error);
      return false;
    }
  }

  remove(key) {
    localStorage.removeItem(key);
  }

  clear() {
    localStorage.clear();
  }
}

// storage/SessionStorageAdapter.js
class SessionStorageAdapter extends StorageAdapter {
  get(key) {
    try {
      const value = sessionStorage.getItem(key);
      return value ? JSON.parse(value) : null;
    } catch (error) {
      console.error('SessionStorage get error:', error);
      return null;
    }
  }

  set(key, value) {
    try {
      sessionStorage.setItem(key, JSON.stringify(value));
      return true;
    } catch (error) {
      console.error('SessionStorage set error:', error);
      return false;
    }
  }

  remove(key) {
    sessionStorage.removeItem(key);
  }

  clear() {
    sessionStorage.clear();
  }
}

// storage/MemoryStorageAdapter.js - 内存存储(用于测试或不支持 localStorage 的环境)
class MemoryStorageAdapter extends StorageAdapter {
  constructor() {
    super();
    this.store = new Map();
  }

  get(key) {
    return this.store.get(key) || null;
  }

  set(key, value) {
    this.store.set(key, value);
    return true;
  }

  remove(key) {
    this.store.delete(key);
  }

  clear() {
    this.store.clear();
  }
}

// storage/CookieStorageAdapter.js - Cookie 存储适配器
class CookieStorageAdapter extends StorageAdapter {
  get(key) {
    const name = key + '=';
    const decodedCookie = decodeURIComponent(document.cookie);
    const cookies = decodedCookie.split(';');

    for (let cookie of cookies) {
      cookie = cookie.trim();
      if (cookie.indexOf(name) === 0) {
        const value = cookie.substring(name.length);
        try {
          return JSON.parse(value);
        } catch {
          return value;
        }
      }
    }
    return null;
  }

  set(key, value, days = 365) {
    const date = new Date();
    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
    const expires = 'expires=' + date.toUTCString();
    const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
    document.cookie = `${key}=${stringValue};${expires};path=/`;
    return true;
  }

  remove(key) {
    document.cookie = `${key}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/`;
  }

  clear() {
    const cookies = document.cookie.split(';');
    for (let cookie of cookies) {
      const key = cookie.split('=')[0].trim();
      this.remove(key);
    }
  }
}

// storage/index.js - 统一导出
function createStorage(type = 'local') {
  switch (type) {
    case 'local':
      return new LocalStorageAdapter();
    case 'session':
      return new SessionStorageAdapter();
    case 'memory':
      return new MemoryStorageAdapter();
    case 'cookie':
      return new CookieStorageAdapter();
    default:
      return new LocalStorageAdapter();
  }
}

export default createStorage;
// 使用存储适配器
import createStorage from './storage';

const storage = createStorage('local'); // 可以轻松切换为 'session', 'memory', 'cookie'

function App() {
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    // 从存储加载用户
    const savedUser = storage.get('user');
    if (savedUser) {
      setUser(savedUser);
    }
  }, []);

  const handleLogin = (userData) => {
    setUser(userData);
    storage.set('user', userData); // 保存到存储
  };

  const handleLogout = () => {
    setUser(null);
    storage.remove('user'); // 从存储删除
  };

  return (
    <div>
      {user ? (
        <div>
          <p>欢迎, {user.name}</p>
          <button onClick={handleLogout}>退出</button>
        </div>
      ) : (
        <button onClick={() => handleLogin({ name: 'John' })}>登录</button>
      )}
    </div>
  );
}
场景2:日期库适配器(兼容 Moment.js、Day.js、date-fns)
// utils/dateAdapter.js
class DateAdapter {
  format(date, formatStr) {
    throw new Error('Subclass must implement format method');
  }

  parse(dateStr, formatStr) {
    throw new Error('Subclass must implement parse method');
  }

  add(date, amount, unit) {
    throw new Error('Subclass must implement add method');
  }

  diff(date1, date2, unit) {
    throw new Error('Subclass must implement diff method');
  }

  isBefore(date1, date2) {
    throw new Error('Subclass must implement isBefore method');
  }

  isAfter(date1, date2) {
    throw new Error('Subclass must implement isAfter method');
  }
}

// utils/MomentAdapter.js
import moment from 'moment';

class MomentAdapter extends DateAdapter {
  format(date, formatStr = 'YYYY-MM-DD') {
    return moment(date).format(formatStr);
  }

  parse(dateStr, formatStr = 'YYYY-MM-DD') {
    return moment(dateStr, formatStr).toDate();
  }

  add(date, amount, unit) {
    return moment(date).add(amount, unit).toDate();
  }

  diff(date1, date2, unit = 'days') {
    return moment(date1).diff(moment(date2), unit);
  }

  isBefore(date1, date2) {
    return moment(date1).isBefore(moment(date2));
  }

  isAfter(date1, date2) {
    return moment(date1).isAfter(moment(date2));
  }
}

// utils/DayJsAdapter.js
import dayjs from 'dayjs';

class DayJsAdapter extends DateAdapter {
  format(date, formatStr = 'YYYY-MM-DD') {
    return dayjs(date).format(formatStr);
  }

  parse(dateStr, formatStr = 'YYYY-MM-DD') {
    return dayjs(dateStr, formatStr).toDate();
  }

  add(date, amount, unit) {
    return dayjs(date).add(amount, unit).toDate();
  }

  diff(date1, date2, unit = 'day') {
    return dayjs(date1).diff(dayjs(date2), unit);
  }

  isBefore(date1, date2) {
    return dayjs(date1).isBefore(dayjs(date2));
  }

  isAfter(date1, date2) {
    return dayjs(date1).isAfter(dayjs(date2));
  }
}

// utils/NativeDateAdapter.js - 使用原生 Date 对象
class NativeDateAdapter extends DateAdapter {
  format(date, formatStr = 'YYYY-MM-DD') {
    const d = new Date(date);
    const year = d.getFullYear();
    const month = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');

    return formatStr
      .replace('YYYY', year)
      .replace('MM', month)
      .replace('DD', day);
  }

  parse(dateStr) {
    return new Date(dateStr);
  }

  add(date, amount, unit) {
    const d = new Date(date);
    if (unit === 'days') {
      d.setDate(d.getDate() + amount);
    } else if (unit === 'months') {
      d.setMonth(d.getMonth() + amount);
    } else if (unit === 'years') {
      d.setFullYear(d.getFullYear() + amount);
    }
    return d;
  }

  diff(date1, date2, unit = 'days') {
    const diffMs = new Date(date1) - new Date(date2);
    if (unit === 'days') {
      return Math.floor(diffMs / (1000 * 60 * 60 * 24));
    }
    return diffMs;
  }

  isBefore(date1, date2) {
    return new Date(date1) < new Date(date2);
  }

  isAfter(date1, date2) {
    return new Date(date1) > new Date(date2);
  }
}

// 导出
export { DateAdapter, MomentAdapter, DayJsAdapter, NativeDateAdapter };
// 使用日期适配器
import { DayJsAdapter } from './utils/dateAdapter';

const dateUtil = new DayJsAdapter(); // 可切换为 MomentAdapter 或 NativeDateAdapter

function DateRangePicker() {
  const [startDate, setStartDate] = React.useState(new Date());
  const [endDate, setEndDate] = React.useState(
    dateUtil.add(new Date(), 7, 'days') // 7 天后
  );

  const daysDiff = dateUtil.diff(endDate, startDate, 'days');

  return (
    <div>
      <p>开始日期: {dateUtil.format(startDate)}</p>
      <p>结束日期: {dateUtil.format(endDate)}</p>
      <p>相差天数: {daysDiff} 天</p>
    </div>
  );
}
场景3:API 数据适配器(后端数据格式转换)
// adapters/userAdapter.js - 用户数据适配器
class UserAdapter {
  // 将后端数据转换为前端格式
  static toClient(serverUser) {
    return {
      id: serverUser.user_id,
      name: serverUser.user_name,
      email: serverUser.email_address,
      avatar: serverUser.avatar_url,
      createdAt: new Date(serverUser.created_at),
      isActive: serverUser.status === 1,
      role: this._mapRole(serverUser.role_id),
    };
  }

  // 将前端数据转换为后端格式
  static toServer(clientUser) {
    return {
      user_id: clientUser.id,
      user_name: clientUser.name,
      email_address: clientUser.email,
      avatar_url: clientUser.avatar,
      created_at: clientUser.createdAt?.toISOString(),
      status: clientUser.isActive ? 1 : 0,
      role_id: this._mapRoleToId(clientUser.role),
    };
  }

  static toClientList(serverUsers) {
    return serverUsers.map(user => this.toClient(user));
  }

  static _mapRole(roleId) {
    const roleMap = {
      1: 'admin',
      2: 'user',
      3: 'guest',
    };
    return roleMap[roleId] || 'guest';
  }

  static _mapRoleToId(role) {
    const roleMap = {
      admin: 1,
      user: 2,
      guest: 3,
    };
    return roleMap[role] || 3;
  }
}

export default UserAdapter;
// 使用数据适配器
import UserAdapter from './adapters/userAdapter';

function UserList() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        // 后端返回的数据格式
        // { user_id: 1, user_name: 'John', email_address: 'john@example.com', ... }

        // 使用适配器转换为前端格式
        const clientUsers = UserAdapter.toClientList(data);
        setUsers(clientUsers);

        // 现在可以使用前端格式
        // { id: 1, name: 'John', email: 'john@example.com', ... }
      });
  }, []);

  const handleUpdateUser = (user) => {
    // 将前端格式转换为后端格式
    const serverUser = UserAdapter.toServer(user);

    fetch(`/api/users/${user.id}`, {
      method: 'PUT',
      body: JSON.stringify(serverUser),
    });
  };

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name} ({user.email}) - {user.role}
        </li>
      ))}
    </ul>
  );
}

🎨 在主流框架中的应用

Axios 适配器系统
// Axios 内部使用适配器模式支持浏览器和 Node.js
import axios from 'axios';

// Axios 自动选择适配器
// 浏览器环境使用 XMLHttpRequest
// Node.js 环境使用 http/https 模块

// 也可以自定义适配器
const customAdapter = (config) => {
  return new Promise((resolve, reject) => {
    // 自定义请求逻辑
    // ...
    resolve({
      data: {},
      status: 200,
      headers: {},
      config,
    });
  });
};

const instance = axios.create({
  adapter: customAdapter,
});
React 事件系统适配器
// React 内部使用适配器统一不同浏览器的事件
// 开发者使用统一的接口
function Button() {
  const handleClick = (e) => {
    // e 是 React 的合成事件,统一了所有浏览器的差异
    console.log(e.target);
  };

  return <button onClick={handleClick}>Click</button>;
}

⚖️ 优缺点分析

✅ 优点
  1. 解耦:客户端与具体实现解耦
  2. 易于切换:可以轻松切换不同的实现
  3. 复用性:适配器可以复用于多个场景
  4. 符合开闭原则:添加新适配器不影响现有代码
❌ 缺点
  1. 增加复杂度:引入额外的适配器层
  2. 性能开销:数据转换有一定开销
  3. 过度使用:简单场景不需要适配器

📋 何时使用适配器模式

✅ 适合使用的场景
  • 第三方库迁移(Axios → Fetch)
  • 多种存储方案(localStorage、sessionStorage、Cookie)
  • API 数据格式转换(后端 snake_case → 前端 camelCase)
  • 跨平台适配(Web、React Native、小程序)
  • 旧代码重构但不想修改接口
❌ 不适合使用的场景
  • 接口已经统一
  • 只有一种实现
  • 数据格式简单,不需要转换

外观模式

💡 模式定义

外观模式(Facade Pattern)为复杂的子系统提供一个简单的统一接口,隐藏子系统的复杂性,使子系统更容易使用。

🤔 为什么需要外观模式?

问题场景:复杂的 API 调用、多个步骤的操作

假设你的应用需要初始化很多服务:

❌ 不使用外观模式的痛点
// App.jsx - 初始化逻辑散落各处
import { initAuth } from './services/auth';
import { initAnalytics } from './services/analytics';
import { initNotifications } from './services/notifications';
import { initDatabase } from './services/database';
import { initLogger } from './services/logger';
import { loadUserPreferences } from './services/preferences';
import { connectWebSocket } from './services/websocket';

function App() {
  const [isInitialized, setIsInitialized] = React.useState(false);

  React.useEffect(() => {
    const initialize = async () => {
      try {
        // 初始化日志
        await initLogger({
          level: 'info',
          endpoint: '/api/logs',
        });

        // 初始化数据库
        await initDatabase({
          name: 'myapp',
          version: 1,
        });

        // 初始化认证
        const authResult = await initAuth({
          clientId: 'xxx',
          redirectUri: window.location.origin,
        });

        if (!authResult.isAuthenticated) {
          throw new Error('Authentication failed');
        }

        // 初始化分析
        await initAnalytics({
          trackingId: 'GA-XXXXX',
          userId: authResult.user.id,
        });

        // 加载用户偏好
        const preferences = await loadUserPreferences(authResult.user.id);
        applyPreferences(preferences);

        // 初始化通知
        await initNotifications({
          permission: true,
          userId: authResult.user.id,
        });

        // 连接 WebSocket
        await connectWebSocket({
          url: 'wss://api.example.com',
          token: authResult.token,
        });

        setIsInitialized(true);
      } catch (error) {
        console.error('初始化失败:', error);
        // 清理已初始化的服务
        // ...
      }
    };

    initialize();
  }, []);

  if (!isInitialized) {
    return <div>初始化中...</div>;
  }

  return <div>应用内容</div>;
}

问题

  • 初始化逻辑复杂,难以维护
  • 错误处理困难
  • 每个新项目都要复制粘贴这些代码
  • 调用顺序容易出错

✅ 使用外观模式解决
// services/AppFacade.js - 应用外观
class AppFacade {
  constructor(config = {}) {
    this.config = {
      logLevel: 'info',
      analyticsId: null,
      authConfig: {},
      ...config,
    };

    this.services = {
      logger: null,
      database: null,
      auth: null,
      analytics: null,
      notifications: null,
      websocket: null,
    };

    this.isInitialized = false;
  }

  // 统一初始化入口
  async initialize() {
    if (this.isInitialized) {
      console.warn('App already initialized');
      return;
    }

    try {
      // 按顺序初始化所有服务
      await this._initLogger();
      await this._initDatabase();
      const user = await this._initAuth();
      await this._initAnalytics(user);
      await this._loadPreferences(user);
      await this._initNotifications(user);
      await this._connectWebSocket(user);

      this.isInitialized = true;
      console.log('App initialized successfully');
    } catch (error) {
      console.error('App initialization failed:', error);
      await this.cleanup();
      throw error;
    }
  }

  // 内部初始化方法
  async _initLogger() {
    const { initLogger } = await import('./logger');
    this.services.logger = await initLogger({
      level: this.config.logLevel,
      endpoint: '/api/logs',
    });
  }

  async _initDatabase() {
    const { initDatabase } = await import('./database');
    this.services.database = await initDatabase({
      name: 'myapp',
      version: 1,
    });
  }

  async _initAuth() {
    const { initAuth } = await import('./auth');
    this.services.auth = await initAuth(this.config.authConfig);

    if (!this.services.auth.isAuthenticated) {
      throw new Error('Authentication failed');
    }

    return this.services.auth.user;
  }

  async _initAnalytics(user) {
    if (!this.config.analyticsId) return;

    const { initAnalytics } = await import('./analytics');
    this.services.analytics = await initAnalytics({
      trackingId: this.config.analyticsId,
      userId: user.id,
    });
  }

  async _loadPreferences(user) {
    const { loadUserPreferences, applyPreferences } = await import('./preferences');
    const preferences = await loadUserPreferences(user.id);
    applyPreferences(preferences);
  }

  async _initNotifications(user) {
    const { initNotifications } = await import('./notifications');
    this.services.notifications = await initNotifications({
      permission: true,
      userId: user.id,
    });
  }

  async _connectWebSocket(user) {
    const { connectWebSocket } = await import('./websocket');
    this.services.websocket = await connectWebSocket({
      url: 'wss://api.example.com',
      token: this.services.auth.token,
    });
  }

  // 统一清理
  async cleanup() {
    console.log('Cleaning up services...');

    if (this.services.websocket) {
      this.services.websocket.disconnect();
    }

    if (this.services.notifications) {
      this.services.notifications.disable();
    }

    // 清理其他服务...

    this.isInitialized = false;
  }

  // 便捷方法
  getService(name) {
    return this.services[name];
  }

  isReady() {
    return this.isInitialized;
  }
}

export default AppFacade;
// App.jsx - 简洁的使用方式
import AppFacade from './services/AppFacade';

function App() {
  const [isReady, setIsReady] = React.useState(false);
  const appFacadeRef = React.useRef(null);

  React.useEffect(() => {
    const appFacade = new AppFacade({
      logLevel: 'info',
      analyticsId: 'GA-XXXXX',
      authConfig: {
        clientId: 'xxx',
        redirectUri: window.location.origin,
      },
    });

    appFacadeRef.current = appFacade;

    // 一行代码完成所有初始化!
    appFacade.initialize()
      .then(() => setIsReady(true))
      .catch(error => console.error('初始化失败:', error));

    // 清理
    return () => {
      appFacade.cleanup();
    };
  }, []);

  if (!isReady) {
    return <div>初始化中...</div>;
  }

  return <div>应用内容</div>;
}

效果

  • ✅ 简化调用:一行代码完成复杂初始化
  • ✅ 易于维护:所有初始化逻辑集中管理
  • ✅ 错误处理:统一的错误处理和清理
  • ✅ 可复用:可以在多个项目中复用

🏗️ 真实业务场景

场景1:文件上传外观
// services/UploadFacade.js - 文件上传外观
class UploadFacade {
  constructor(config = {}) {
    this.config = {
      maxSize: 10 * 1024 * 1024, // 10MB
      allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'],
      endpoint: '/api/upload',
      chunkSize: 1024 * 1024, // 1MB chunks
      ...config,
    };
  }

  // 简单上传接口
  async upload(file, options = {}) {
    try {
      // 1. 验证文件
      this._validateFile(file);

      // 2. 压缩图片(如果是图片)
      const processedFile = await this._processFile(file);

      // 3. 生成缩略图(如果是图片)
      const thumbnail = await this._generateThumbnail(processedFile);

      // 4. 上传文件
      const uploadResult = await this._uploadFile(processedFile, options);

      // 5. 上传缩略图
      if (thumbnail) {
        await this._uploadThumbnail(thumbnail, uploadResult.id);
      }

      // 6. 返回结果
      return {
        id: uploadResult.id,
        url: uploadResult.url,
        thumbnailUrl: uploadResult.thumbnailUrl,
        size: processedFile.size,
        type: processedFile.type,
      };
    } catch (error) {
      console.error('上传失败:', error);
      throw error;
    }
  }

  // 分片上传
  async uploadLargeFile(file, onProgress) {
    this._validateFile(file);

    const totalChunks = Math.ceil(file.size / this.config.chunkSize);
    let uploadedChunks = 0;

    // 创建上传任务
    const uploadId = await this._createUploadTask(file);

    // 分片上传
    for (let i = 0; i < totalChunks; i++) {
      const start = i * this.config.chunkSize;
      const end = Math.min(start + this.config.chunkSize, file.size);
      const chunk = file.slice(start, end);

      await this._uploadChunk(uploadId, i, chunk);

      uploadedChunks++;
      onProgress?.((uploadedChunks / totalChunks) * 100);
    }

    // 合并分片
    const result = await this._mergeChunks(uploadId);
    return result;
  }

  // 批量上传
  async uploadMultiple(files, onProgress) {
    const results = [];
    let completed = 0;

    for (const file of files) {
      try {
        const result = await this.upload(file);
        results.push({ success: true, file, result });
      } catch (error) {
        results.push({ success: false, file, error });
      }

      completed++;
      onProgress?.((completed / files.length) * 100);
    }

    return results;
  }

  // 内部方法
  _validateFile(file) {
    if (file.size > this.config.maxSize) {
      throw new Error(`文件大小超过限制 (${this.config.maxSize / 1024 / 1024}MB)`);
    }

    if (!this.config.allowedTypes.includes(file.type)) {
      throw new Error(`不支持的文件类型: ${file.type}`);
    }
  }

  async _processFile(file) {
    if (!file.type.startsWith('image/')) {
      return file;
    }

    // 压缩图片
    return await this._compressImage(file);
  }

  async _compressImage(file) {
    return new Promise((resolve) => {
      const reader = new FileReader();
      reader.onload = (e) => {
        const img = new Image();
        img.onload = () => {
          const canvas = document.createElement('canvas');
          const ctx = canvas.getContext('2d');

          // 计算压缩后的尺寸
          let width = img.width;
          let height = img.height;
          const maxDimension = 1920;

          if (width > maxDimension || height > maxDimension) {
            if (width > height) {
              height = (height / width) * maxDimension;
              width = maxDimension;
            } else {
              width = (width / height) * maxDimension;
              height = maxDimension;
            }
          }

          canvas.width = width;
          canvas.height = height;
          ctx.drawImage(img, 0, 0, width, height);

          canvas.toBlob((blob) => {
            resolve(new File([blob], file.name, { type: file.type }));
          }, file.type, 0.9);
        };
        img.src = e.target.result;
      };
      reader.readAsDataURL(file);
    });
  }

  async _generateThumbnail(file) {
    if (!file.type.startsWith('image/')) {
      return null;
    }

    // 生成缩略图逻辑
    return null; // 简化实现
  }

  async _uploadFile(file, options) {
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch(this.config.endpoint, {
      method: 'POST',
      body: formData,
      headers: options.headers || {},
    });

    return await response.json();
  }

  async _uploadThumbnail(thumbnail, fileId) {
    // 上传缩略图
  }

  async _createUploadTask(file) {
    // 创建分片上传任务
    return 'upload-id-123';
  }

  async _uploadChunk(uploadId, chunkIndex, chunk) {
    // 上传单个分片
  }

  async _mergeChunks(uploadId) {
    // 合并所有分片
  }
}

export default UploadFacade;
// 使用上传外观
import UploadFacade from './services/UploadFacade';

function FileUploader() {
  const [uploading, setUploading] = React.useState(false);
  const [progress, setProgress] = React.useState(0);

  const uploadFacade = new UploadFacade({
    maxSize: 20 * 1024 * 1024, // 20MB
    allowedTypes: ['image/jpeg', 'image/png'],
  });

  const handleFileSelect = async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    setUploading(true);

    try {
      // 简单调用,内部自动处理压缩、缩略图、上传等
      const result = await uploadFacade.upload(file);
      console.log('上传成功:', result);
    } catch (error) {
      console.error('上传失败:', error);
    } finally {
      setUploading(false);
    }
  };

  const handleLargeFileSelect = async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    setUploading(true);

    try {
      // 大文件自动分片上传
      const result = await uploadFacade.uploadLargeFile(file, setProgress);
      console.log('上传成功:', result);
    } catch (error) {
      console.error('上传失败:', error);
    } finally {
      setUploading(false);
      setProgress(0);
    }
  };

  return (
    <div>
      <input type="file" onChange={handleFileSelect} disabled={uploading} />
      <input type="file" onChange={handleLargeFileSelect} disabled={uploading} />
      {uploading && <div>上传中... {progress.toFixed(0)}%</div>}
    </div>
  );
}
场景2:支付外观
// services/PaymentFacade.js - 支付外观
class PaymentFacade {
  constructor() {
    this.providers = {
      alipay: null,
      wechat: null,
      stripe: null,
    };
  }

  // 统一支付接口
  async pay(provider, amount, options = {}) {
    try {
      // 1. 验证金额
      this._validateAmount(amount);

      // 2. 创建订单
      const order = await this._createOrder(amount, options);

      // 3. 调用支付提供商
      let paymentResult;
      switch (provider) {
        case 'alipay':
          paymentResult = await this._payWithAlipay(order);
          break;
        case 'wechat':
          paymentResult = await this._payWithWechat(order);
          break;
        case 'stripe':
          paymentResult = await this._payWithStripe(order);
          break;
        default:
          throw new Error(`不支持的支付方式: ${provider}`);
      }

      // 4. 记录支付日志
      await this._logPayment(order, paymentResult);

      // 5. 发送通知
      await this._sendNotification(order, paymentResult);

      return {
        success: true,
        orderId: order.id,
        transactionId: paymentResult.transactionId,
      };
    } catch (error) {
      console.error('支付失败:', error);
      throw error;
    }
  }

  // 查询支付状态
  async queryStatus(orderId) {
    const order = await this._getOrder(orderId);
    return {
      orderId: order.id,
      status: order.status,
      amount: order.amount,
      createdAt: order.createdAt,
    };
  }

  // 退款
  async refund(orderId, amount) {
    const order = await this._getOrder(orderId);

    // 调用对应支付提供商的退款接口
    let refundResult;
    switch (order.provider) {
      case 'alipay':
        refundResult = await this._refundAlipay(order, amount);
        break;
      case 'wechat':
        refundResult = await this._refundWechat(order, amount);
        break;
      case 'stripe':
        refundResult = await this._refundStripe(order, amount);
        break;
    }

    await this._logRefund(order, refundResult);
    return refundResult;
  }

  // 内部方法
  _validateAmount(amount) {
    if (amount <= 0) {
      throw new Error('金额必须大于0');
    }
  }

  async _createOrder(amount, options) {
    return {
      id: 'ORDER-' + Date.now(),
      amount,
      status: 'pending',
      createdAt: new Date(),
      ...options,
    };
  }

  async _payWithAlipay(order) {
    // 调用支付宝 SDK
    return { transactionId: 'ALIPAY-' + Date.now() };
  }

  async _payWithWechat(order) {
    // 调用微信支付 SDK
    return { transactionId: 'WECHAT-' + Date.now() };
  }

  async _payWithStripe(order) {
    // 调用 Stripe SDK
    return { transactionId: 'STRIPE-' + Date.now() };
  }

  async _logPayment(order, result) {
    console.log('支付日志:', order, result);
  }

  async _sendNotification(order, result) {
    console.log('发送通知:', order, result);
  }

  async _getOrder(orderId) {
    // 获取订单信息
    return { id: orderId, provider: 'alipay' };
  }

  async _refundAlipay(order, amount) {
    // 支付宝退款
  }

  async _refundWechat(order, amount) {
    // 微信退款
  }

  async _refundStripe(order, amount) {
    // Stripe 退款
  }

  async _logRefund(order, result) {
    console.log('退款日志:', order, result);
  }
}

export default PaymentFacade;
// 使用支付外观
import PaymentFacade from './services/PaymentFacade';

function CheckoutPage() {
  const [paying, setPaying] = React.useState(false);
  const paymentFacade = new PaymentFacade();

  const handlePay = async (provider) => {
    setPaying(true);

    try {
      // 简单调用,内部自动处理订单创建、支付、日志、通知等
      const result = await paymentFacade.pay(provider, 99.99, {
        productId: 'PROD-123',
        userId: 'USER-456',
      });

      console.log('支付成功:', result);
    } catch (error) {
      console.error('支付失败:', error);
    } finally {
      setPaying(false);
    }
  };

  return (
    <div>
      <button onClick={() => handlePay('alipay')} disabled={paying}>
        支付宝支付
      </button>
      <button onClick={() => handlePay('wechat')} disabled={paying}>
        微信支付
      </button>
      <button onClick={() => handlePay('stripe')} disabled={paying}>
        Stripe 支付
      </button>
    </div>
  );
}

🎨 在主流框架中的应用

jQuery - 外观模式的典型应用
// jQuery 封装了复杂的 DOM 操作
$('#button').on('click', function() {
  $(this).addClass('active');
});

// 等价于原生 JavaScript
document.getElementById('button').addEventListener('click', function() {
  this.classList.add('active');
});
React Router - 简化路由配置
// React Router 提供简单的路由配置接口
import { BrowserRouter, Route, Switch } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Switch>
        <Route path="/" exact component={Home} />
        <Route path="/about" component={About} />
      </Switch>
    </BrowserRouter>
  );
}

// 内部处理了复杂的路由匹配、历史记录管理等

⚖️ 优缺点分析

✅ 优点
  1. 简化接口:为复杂系统提供简单接口
  2. 解耦:客户端与子系统解耦
  3. 易用性:降低使用难度
  4. 分层:定义系统中每一层的入口点
❌ 缺点
  1. 不符合开闭原则:增加新的子系统可能需要修改外观类
  2. 过度简化:可能隐藏了用户需要的细节
  3. 单点故障:外观类出问题会影响所有调用

📋 何时使用外观模式

✅ 适合使用的场景
  • 为复杂系统提供简单接口
  • 需要封装多个步骤的操作
  • 解耦客户端和子系统
  • 分层架构的入口点
❌ 不适合使用的场景
  • 系统本身很简单
  • 需要暴露所有细节给客户端
  • 子系统经常变化

📝 总结

结构型模式对比

模式核心目的使用场景优先级
装饰器模式动态添加功能HOC、中间件、权限控制⭐⭐⭐⭐⭐
代理模式控制对象访问懒加载、缓存、权限验证⭐⭐⭐⭐⭐
适配器模式接口转换第三方库迁移、数据格式转换⭐⭐⭐⭐
外观模式简化接口复杂系统封装、多步骤操作⭐⭐⭐⭐

学习建议

  1. 装饰器模式:React HOC 和 Hooks 都是装饰器思想,重点掌握
  2. 代理模式:理解 ES6 Proxy 和 Vue 3 响应式原理
  3. 适配器模式:在重构和迁移中非常有用
  4. 外观模式:学会封装复杂逻辑,提供简单 API

四种模式的区别

  • 装饰器:不改变接口,增强功能
  • 代理:相同接口,控制访问
  • 适配器:转换接口,解决不兼容
  • 外观:提供新接口,简化复杂系统